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