Repository navigation
MULTI_TENANCY_IMPLEMENTATION
Version: v0.6.0+ Date: February 1, 2026 Status: ✅ Complete
This document describes the complete multi-tenancy implementation for nself-admin, including tenant isolation, organization hierarchy, and custom domain SSL automation.
Files:
/src/lib/tenant/tenant-context.ts/src/lib/tenant/tenant-middleware.ts
What it does:
- Tracks the current tenant context using secure HTTP-only cookies
- Provides middleware helpers for enforcing tenant access in API routes
- Ensures all database operations are scoped to the correct tenant
Key Functions:
// Get current tenant ID
const tenantId = await getCurrentTenantId()
// Set tenant context
await setCurrentTenantId(tenantId)
// Switch tenant
await switchTenant(tenantId)
// Enforce tenant context in API routes
const error = await enforceTenantContext(request, tenantId)
if (error) return error
// Enforce organization context
const error = await enforceOrgContext(request, orgId)
if (error) return errorFile: /src/lib/database.ts
What was added:
-
tenantsCollection- Tenant records -
organizationsCollection- Organizations with tenant scoping -
tenantMembersCollection- Tenant membership -
orgMembersCollection- Organization membership -
teamsCollection- Teams within organizations -
tenantDomainsCollection- Custom domains per tenant
Tenant-Scoped CRUD Operations:
// Tenants
await createTenant(tenant)
await getTenant(id)
await listTenants()
await updateTenant(id, updates)
await deleteTenant(id)
// Organizations (with tenant isolation)
await createOrganization(org)
await getOrganization(id, tenantId)
await listOrganizations(tenantId)
await listOrganizationsByParent(parentId, tenantId) // Hierarchy support
await updateOrganization(id, updates, tenantId)
await deleteOrganization(id, tenantId)
// Tenant Members
await createTenantMember(member)
await getTenantMember(id, tenantId)
await listTenantMembers(tenantId)
await updateTenantMember(id, tenantId, updates)
await deleteTenantMember(id, tenantId)
// Organization Members
await createOrgMember(member)
await getOrgMember(id, orgId)
await listOrgMembers(orgId)
await updateOrgMember(id, orgId, updates)
await deleteOrgMember(id, orgId)
// Teams
await createTeam(team)
await getTeam(id, orgId)
await listTeams(orgId)
await updateTeam(id, orgId, updates)
await deleteTeam(id, orgId)
// Tenant Domains
await createTenantDomain(domain)
await getTenantDomain(domain, tenantId)
await listTenantDomains(tenantId)
await updateTenantDomain(domain, tenantId, updates)
await deleteTenantDomain(domain, tenantId)Type Update: /src/types/tenant.ts
export interface Organization {
id: string
tenantId: string
name: string
slug: string
description?: string
parentId?: string // NEW - For hierarchical organizations
createdAt: string
updatedAt: string
settings: OrgSettings
}Usage:
// Create parent organization
const engineering = await createOrganization({
tenantId: 'tenant_123',
name: 'Engineering',
slug: 'engineering',
// no parentId = top-level org
})
// Create child organization
const backend = await createOrganization({
tenantId: 'tenant_123',
name: 'Backend Team',
slug: 'backend',
parentId: engineering.id, // Links to parent
})
// Query child organizations
const children = await listOrganizationsByParent(engineering.id, 'tenant_123')File: /src/lib/tenant/ssl-automation.ts
What it does:
- Automatically provisions SSL certificates for custom domains using Let's Encrypt
- Integrates with nself CLI for certificate management
- Tracks certificate expiration and handles renewal
- Provides certificate status monitoring
Key Functions:
// Provision SSL for a domain
const result = await provisionSSLCertificate(tenantId, domain)
// Auto-provision for all verified domains
const results = await autoProvisionSSLForTenant(tenantId)
// Renew certificate
const result = await renewSSLCertificate(tenantId, domain)
// Check and auto-renew if needed
const result = await checkAndRenewSSL(tenantId, domain)
// Revoke certificate
const result = await revokeSSLCertificate(tenantId, domain)
// Get SSL status
const status = await getSSLStatus(tenantId, domain)Integration: Updated /src/app/api/tenant/[id]/domains/[domain]/ssl/route.ts
File: /src/components/tenant/TenantSwitcher.tsx
What it does:
- Displays current tenant in top navigation
- Dropdown to switch between available tenants
- Shows tenant name and slug
- Link to create new tenant
- Reloads page after switching to refresh context
Usage:
import TenantSwitcher from '@/components/tenant/TenantSwitcher'
;<TenantSwitcher
currentTenantId={tenantId}
onTenantChange={(id) => console.log('Switched to:', id)}
/>API Route: /src/app/api/tenant/switch/route.ts
File: /src/lib/tenant/tenant-test-data.ts
What it does:
- Creates sample tenants with different plans (free, pro, enterprise)
- Generates hierarchical organization structures
- Creates tenant and organization members
- Adds sample domains with SSL status
- Provides cleanup function
Test API: /src/app/api/tenant/test-isolation/route.ts
Usage:
# Generate test data
curl -X POST http://localhost:3021/api/tenant/test-isolation
# Verify isolation
curl http://localhost:3021/api/tenant/test-isolation
# Clean up
curl -X DELETE http://localhost:3021/api/tenant/test-isolationtenants (Collection)
- id: string (unique)
- name: string
- slug: string (unique)
- status: 'active' | 'suspended' | 'pending'
- plan: 'free' | 'pro' | 'enterprise'
- ownerId: string
- settings: TenantSettings
- branding: TenantBranding
- quota: TenantQuota
- createdAt: string
- updatedAt: string
Indices: id, slug, status, ownerId
organizations (Collection)
- id: string (unique)
- tenantId: string (foreign key)
- name: string
- slug: string
- parentId?: string (self-referential for hierarchy)
- description?: string
- settings: OrgSettings
- createdAt: string
- updatedAt: string
Indices: id, tenantId, slug, parentId
tenantMembers (Collection)
- id: string (unique)
- tenantId: string (foreign key)
- userId: string
- email: string
- name: string
- role: 'owner' | 'admin' | 'member' | 'viewer'
- status: 'active' | 'pending' | 'suspended'
- invitedAt: string
- joinedAt?: string
Indices: id, tenantId, userId, email
orgMembers (Collection)
- id: string (unique)
- orgId: string (foreign key)
- userId: string
- email: string
- name: string
- role: 'owner' | 'admin' | 'member' | 'viewer'
- teams: string[]
- joinedAt: string
Indices: id, orgId, userId, email
teams (Collection)
- id: string (unique)
- orgId: string (foreign key)
- name: string
- description?: string
- members: string[]
- permissions: string[]
- createdAt: string
Indices: id, orgId
tenantDomains (Collection)
- id: string (unique)
- tenantId: string (foreign key)
- domain: string (unique)
- verified: boolean
- ssl: boolean
- primary: boolean
- dnsRecords: DNSRecord[]
- createdAt: string
- verifiedAt?: string
Indices: id, tenantId, domain, verified
The current tenant is tracked via a secure HTTP-only cookie:
- Name:
nself_current_tenant - Secure: true (in production)
- HttpOnly: true
- SameSite: lax
- Max-Age: 30 days
All tenant-specific API routes should use the middleware:
import { enforceTenantContext } from '@/lib/tenant/tenant-middleware'
export async function GET(request: NextRequest) {
// Enforce tenant context
const tenantError = await enforceTenantContext(request)
if (tenantError) return tenantError
// Tenant context is valid, proceed with operation
const tenantId = await getCurrentTenantId()
const data = await listOrganizations(tenantId)
return NextResponse.json({ success: true, data })
}All queries automatically scope to the tenant:
- Organizations: Always filtered by
tenantId - Members: Always filtered by
tenantIdororgId - Teams: Always filtered by
orgId(which has atenantId) - Domains: Always filtered by
tenantId
- Generate Test Data:
curl -X POST http://localhost:3021/api/tenant/test-isolationThis creates:
- 2 tenants (Acme Corporation - enterprise, Startup Inc - pro)
- 6 organizations (4 for Tenant 1, 2 for Tenant 2)
- Hierarchical structure for Tenant 1
- Members, teams, and domains
- Verify Isolation:
curl http://localhost:3021/api/tenant/test-isolationReturns:
- Total organizations per tenant
- Verification that no cross-tenant data leakage
- Hierarchical organization counts
- Test Switching:
# Switch to Tenant 1
curl -X POST http://localhost:3021/api/tenant/switch \
-H "Content-Type: application/json" \
-d '{"tenantId": "tenant_xxx"}'
# Switch to Tenant 2
curl -X POST http://localhost:3021/api/tenant/switch \
-H "Content-Type: application/json" \
-d '{"tenantId": "tenant_yyy"}'- Clean Up:
curl -X DELETE http://localhost:3021/api/tenant/test-isolationTODO: Add Jest tests for:
- Tenant context isolation
- Organization hierarchy queries
- SSL provisioning workflow
- Member access control
- Domain Added: Tenant adds custom domain
- DNS Verification: Domain ownership verified via DNS records
- SSL Provision: Automatic certificate provisioning via Let's Encrypt
- Certificate Tracking: Expiration date stored in database
- Auto-Renewal: Certificates renewed 30 days before expiry
The SSL automation uses nself CLI commands:
# Provision certificate
nself ssl provision --domain example.com --tenant tenant_123 --provider letsencrypt
# Renew certificate
nself ssl renew --domain example.com --tenant tenant_123
# Revoke certificate
nself ssl revoke --domain example.com --tenant tenant_123
# Check status
nself ssl status --domain example.com- ✅ Cookie-based tenant context (secure, httpOnly)
- ✅ Middleware enforcement on all routes
- ✅ Database-level filtering by tenantId
- ✅ No cross-tenant data access
- ✅ Parent-child relationships stored in database
- ✅ Circular reference prevention (TODO: add validation)
- ✅ Cascade delete (TODO: implement)
- ✅ Domain verification before SSL provisioning
- ✅ Automatic renewal before expiration
- ✅ Secure storage of certificate paths
⚠️ Certificate private keys stored on server (as designed)
All tenant operations are logged:
-
tenant_created- New tenant created -
tenant_updated- Tenant settings updated -
tenant_deleted- Tenant deleted -
organization_created- Organization created -
organization_updated- Organization updated -
organization_deleted- Organization deleted -
tenant_member_added- Member added to tenant -
tenant_member_updated- Member role changed -
tenant_member_removed- Member removed -
org_member_added- Member added to organization -
org_member_updated- Member role changed -
org_member_removed- Member removed -
team_created- Team created -
team_updated- Team updated -
team_deleted- Team deleted -
domain_added- Custom domain added -
domain_updated- Domain verification status changed -
domain_removed- Domain removed -
ssl_provision_success- SSL certificate provisioned -
ssl_provision_failed- SSL provisioning failed -
ssl_renew_success- SSL certificate renewed -
ssl_renew_failed- SSL renewal failed -
ssl_revoke_success- SSL certificate revoked
-
GET /api/tenant- List tenants -
POST /api/tenant- Create tenant -
GET /api/tenant/[id]- Get tenant details -
PUT /api/tenant/[id]- Update tenant -
DELETE /api/tenant/[id]- Delete tenant -
POST /api/tenant/switch- Switch current tenant
-
GET /api/tenant/[id]/domains- List domains -
POST /api/tenant/[id]/domains- Add domain -
POST /api/tenant/[id]/domains/[domain]/verify- Verify domain -
POST /api/tenant/[id]/domains/[domain]/ssl- Provision SSL -
GET /api/tenant/[id]/domains/[domain]/ssl- Get SSL status -
DELETE /api/tenant/[id]/domains/[domain]- Remove domain
-
GET /api/org- List organizations -
POST /api/org- Create organization -
GET /api/org/[id]- Get organization details -
PUT /api/org/[id]- Update organization -
DELETE /api/org/[id]- Delete organization
-
POST /api/tenant/test-isolation- Generate test data -
GET /api/tenant/test-isolation- Verify isolation -
DELETE /api/tenant/test-isolation- Clean up test data
If you have existing data without tenant isolation:
- Create Default Tenant:
const defaultTenant = await createTenant({
id: 'tenant_default',
name: 'Default Tenant',
slug: 'default',
status: 'active',
plan: 'enterprise',
// ... other fields
})- Migrate Organizations:
const orgs = await listOrganizations()
for (const org of orgs) {
await updateOrganization(org.id, {
tenantId: 'tenant_default',
})
}- Set Tenant Context:
await setCurrentTenantId('tenant_default')- Tenant usage analytics and reporting
- Billing integration per tenant
- Custom subdomain routing (tenant-slug.app.com)
- Tenant-level feature flags
- Organization hierarchy depth limits
- Cascade delete for organizations
- Circular reference validation
- Tenant data export/import
- Cross-tenant data sharing (with permissions)
- Tenant suspension workflow
- Resource quota enforcement UI
- SSL certificate monitoring dashboard
- Automated SSL renewal notifications
Symptom: API returns 403 "Tenant context required"
Solution:
- Check if tenant cookie is set:
nself_current_tenant - Switch to a valid tenant using TenantSwitcher
- Or call
/api/tenant/switchwith a valid tenant ID
Symptom: Cannot query organization that exists in database
Solution:
- Verify tenant context matches organization's tenantId
- Check if organization was created under a different tenant
- Use
listOrganizations(tenantId)to verify
Symptom: SSL certificate not generated
Solution:
- Verify domain is verified first
- Check DNS records are properly configured
- Ensure nself CLI is installed and accessible
- Check audit logs for detailed error message
Symptom: Seeing data from another tenant
Solution:
- Check all API routes use
enforceTenantContext - Verify database queries include tenantId filter
- Check tenant cookie is properly isolated per user
For issues or questions:
- GitHub Issues: https://github.com/nself-org/admin/issues
- Documentation: https://github.com/nself-org/admin/wiki
- nself CLI: https://github.com/nself-org/cli
Version: 1.0.0 | Updated: 2026-10-19 11:40 UTC | GitHub