Overview
Project Zoe uses row-level multi-tenancy: every table that holds tenant-specific data carries atenantId foreign key. A single PostgreSQL database (and schema) serves all tenants — there are no separate schemas or databases per church.
Tenant entity
ATenant record represents one church organisation. It is the root of all data relationships:
worshipharvest.
How tenant context flows
- Login — the client sends a
churchNamefield in the login body. TheTenantHeaderMiddlewareresolves this to aTenantrecord and attaches it to the request. - All protected endpoints — the
JwtAuthGuardensures the JWT is valid. The resolved tenant from the JWT payload scopes every query toWHERE tenant_id = ?. TenantAwareRepository— a thin wrapper around TypeORM’sRepositorythat automatically injects thetenantIdcondition into everyfind*call.
Creating a tenant
Tenants are created via the CLI command:Tenant isolation guarantees
- All TypeORM entities that hold tenant-specific data are indexed on
(tenant, id)— queries are fast and never cross tenant boundaries. - The
@Index(['tenant', 'id'])decorator appears on every major entity. - The public registration and login endpoints are the only routes that accept unauthenticated requests — everything else requires a valid JWT that carries the tenant claim.