Skip to Content
Multi-Tenancy Support in Jolli

Last Updated: 12/23/2025


Core Concepts Tenant → Customer/organization with isolated data Org → Sub-division within a tenant with its own PostgreSQL schema

Tenant (acme) ├── Default Org (schema=“public”) ├── Engineering Org (schema=“org_engineering”) └── Marketing Org (schema=“org_marketing”) How to Enable Multi-Tenancy Environment Variables:

MULTI_TENANT_ENABLED=true MULTI_TENANT_REGISTRY_URL=postgres://… # Registry database BASE_DOMAIN=jolli.app # For subdomain routing USE_TENANT_SWITCHER=true # Enable tenant switcher UI TENANT_TOKEN_MASTER_SECRET=… # Master secret for TOKEN_SECRET derivation URL Routing URL Tenant Org jolli.app jolli default acme.jolli.app acme default engineering.acme.jolli.app acme engineering docs.acme.com (custom domain) acme (configured) Backend Usage

import { requireTenantContext, getTenantContext } from "../tenant/TenantContext"; router.get("/data", async (req, res) => { // Get current tenant/org context const { tenant, org, database, schemaName } = requireTenantContext(); // Database is already scoped to the org's schema const data = await database.jobDao.getAll(); res.json({ tenant: tenant.slug, org: org.slug, data }); }); Tenant-Specific Config: import { getConfig } from "../config/Config"; const config = getConfig(); // Returns tenant-overridden values for keys like: // ANTHROPIC_API_KEY, MAX_SEATS, AUTH_EMAILS, etc. Frontend Usage import { useOrg, useAvailableOrgs } from "../contexts/OrgContext"; import { useTenant } from "../contexts/TenantContext"; function MyComponent() { const { org, tenant, isMultiTenant } = useOrg(); const availableOrgs = useAvailableOrgs(); const { availableTenants, baseDomain } = useTenant(); if (!isMultiTenant) { return <div>Single-tenant mode</div>; } return ( <div> <h1>{tenant?.displayName}</h1> <p>Current org: {org?.displayName}</p> {/* Org Switcher */} <select onChange={(e) => { window.location.href = `https://${e.target.value}.${tenant?.slug}.${baseDomain}`; }}> {availableOrgs.map(o => ( <option key={o.id} value={o.slug}>{o.displayName}</option> ))} </select> </div> ); }

Key Files File Purpose TenantContext.ts AsyncLocalStorage for request-scoped context TenantMiddleware.ts Resolves tenant from URL/headers TenantOrgConnectionManager.ts Connection pooling per tenant-org DomainUtils.ts Domain/subdomain resolution OrgContext.tsx Frontend org context & hooks TenantContext.tsx Frontend tenant switcher context Data Isolation Uses PostgreSQL schema isolation:

— Each org gets its own schema SET search_path TO “org_engineering”, public;

— All queries automatically use the org’s schema SELECT * FROM users; — Reads from org_engineering.users