Skip to content

MULTI_TENANCY_IMPLEMENTATION

GitHub Actions Bot edited this page Feb 21, 2026 · 2 revisions

Multi-Tenancy Implementation Guide

Version: v0.6.0+ Date: February 1, 2026 Status: ✅ Complete

Overview

This document describes the complete multi-tenancy implementation for nself-admin, including tenant isolation, organization hierarchy, and custom domain SSL automation.

Features Implemented

1. Tenant Context Management

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 error

2. Database Collections with Tenant Isolation

File: /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)

3. Organization Hierarchy

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')

4. Automatic SSL Certificate Provisioning

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

5. Tenant Switcher UI Component

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

6. Test Data Generator

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-isolation

Database Schema

Collections

tenants (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

Tenant Isolation Enforcement

Cookie-Based Context

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

Middleware Enforcement

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 })
}

Database-Level Isolation

All queries automatically scope to the tenant:

  • Organizations: Always filtered by tenantId
  • Members: Always filtered by tenantId or orgId
  • Teams: Always filtered by orgId (which has a tenantId)
  • Domains: Always filtered by tenantId

Testing Multi-Tenant Isolation

Manual Testing

  1. Generate Test Data:
curl -X POST http://localhost:3021/api/tenant/test-isolation

This 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
  1. Verify Isolation:
curl http://localhost:3021/api/tenant/test-isolation

Returns:

  • Total organizations per tenant
  • Verification that no cross-tenant data leakage
  • Hierarchical organization counts
  1. 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"}'
  1. Clean Up:
curl -X DELETE http://localhost:3021/api/tenant/test-isolation

Automated Testing

TODO: Add Jest tests for:

  • Tenant context isolation
  • Organization hierarchy queries
  • SSL provisioning workflow
  • Member access control

SSL Certificate Workflow

Provisioning Flow

  1. Domain Added: Tenant adds custom domain
  2. DNS Verification: Domain ownership verified via DNS records
  3. SSL Provision: Automatic certificate provisioning via Let's Encrypt
  4. Certificate Tracking: Expiration date stored in database
  5. Auto-Renewal: Certificates renewed 30 days before expiry

CLI Integration

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

Security Considerations

Tenant Isolation

  • ✅ Cookie-based tenant context (secure, httpOnly)
  • ✅ Middleware enforcement on all routes
  • ✅ Database-level filtering by tenantId
  • ✅ No cross-tenant data access

Organization Hierarchy

  • ✅ Parent-child relationships stored in database
  • ✅ Circular reference prevention (TODO: add validation)
  • ✅ Cascade delete (TODO: implement)

SSL Certificates

  • ✅ Domain verification before SSL provisioning
  • ✅ Automatic renewal before expiration
  • ✅ Secure storage of certificate paths
  • ⚠️ Certificate private keys stored on server (as designed)

Audit Logging

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

API Routes Reference

Tenant Management

  • 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

Domain Management

  • 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

Organization Management

  • 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

Testing

  • POST /api/tenant/test-isolation - Generate test data
  • GET /api/tenant/test-isolation - Verify isolation
  • DELETE /api/tenant/test-isolation - Clean up test data

Migration Guide

From Single-Tenant to Multi-Tenant

If you have existing data without tenant isolation:

  1. Create Default Tenant:
const defaultTenant = await createTenant({
  id: 'tenant_default',
  name: 'Default Tenant',
  slug: 'default',
  status: 'active',
  plan: 'enterprise',
  // ... other fields
})
  1. Migrate Organizations:
const orgs = await listOrganizations()
for (const org of orgs) {
  await updateOrganization(org.id, {
    tenantId: 'tenant_default',
  })
}
  1. Set Tenant Context:
await setCurrentTenantId('tenant_default')

Future Enhancements

Planned for v0.7.0+

  • 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

Troubleshooting

Tenant Context Not Found

Symptom: API returns 403 "Tenant context required"

Solution:

  1. Check if tenant cookie is set: nself_current_tenant
  2. Switch to a valid tenant using TenantSwitcher
  3. Or call /api/tenant/switch with a valid tenant ID

Organization Not Found

Symptom: Cannot query organization that exists in database

Solution:

  1. Verify tenant context matches organization's tenantId
  2. Check if organization was created under a different tenant
  3. Use listOrganizations(tenantId) to verify

SSL Provisioning Failed

Symptom: SSL certificate not generated

Solution:

  1. Verify domain is verified first
  2. Check DNS records are properly configured
  3. Ensure nself CLI is installed and accessible
  4. Check audit logs for detailed error message

Cross-Tenant Data Leakage

Symptom: Seeing data from another tenant

Solution:

  1. Check all API routes use enforceTenantContext
  2. Verify database queries include tenantId filter
  3. Check tenant cookie is properly isolated per user

Support

For issues or questions:

Clone this wiki locally