What is a tenant?
A tenant maps to a single construction company or organizational unit. The coretenants table stores a minimal record:
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key, auto-generated |
name | text | Unique display name for the organization |
created_at | timestamptz | When the tenant was created |
tenant_id foreign key that ties rows back to their owning tenant. Row Level Security (RLS) policies on each table use helper functions (is_member_of, is_admin_of) to ensure users can only query rows that belong to a tenant they are a member of.
The system ships with a well-known Default Tenant (
id = 00000000-0000-0000-0000-000000000001) used as a fallback for super-admin access. Normal users are never automatically assigned to it — assignment is always explicit via invitation or onboarding.Tenant creation flow
New tenants are created during the onboarding flow. Any authenticated user can create a tenant; the user who creates it automatically becomes its firstowner.
User signs up or logs in
The user authenticates via Supabase Auth. A
profiles record is created linked to auth.users.User creates a tenant
The onboarding page calls
INSERT INTO tenants (name) VALUES (...). The RLS policy for inserts requires only that auth.uid() IS NOT NULL, so any authenticated user can create a tenant.Owner membership is bootstrapped
After the tenant row is inserted, the user inserts a membership record for themselves with
role = 'owner'. The RLS policy for this “bootstrap” case allows the insert only when the tenant has no existing members yet, preventing takeovers of already-claimed tenants.Tenant is assigned the Starter plan
A row is inserted into
tenant_subscriptions linking the new tenant to the starter plan. See Subscription Plans for details.Tenant isolation model
Isolation is enforced at the database layer through PostgreSQL RLS. The key properties are:- Read isolation: All
SELECTpolicies checkis_member_of(tenant_id), so a user can never read another tenant’s rows — even with a direct API call. - Write isolation:
INSERTandUPDATEpolicies check eitheris_member_oforis_admin_of, depending on the required privilege level. - Cascade deletes: All tenant-scoped tables use
ON DELETE CASCADEon thetenant_idforeign key, so deleting a tenant removes all associated data. - No cross-tenant queries: There are no application-level joins across tenants. The
tenant_idcolumn is always filtered explicitly.
Active tenant selection
When a user belongs to multiple tenants, Sintesis uses a cookie (active_tenant_id) to track which tenant is currently active. The resolveTenantMembership function in lib/tenant-selection.ts implements the resolution order:
- The tenant matching the
active_tenant_idcookie value (if it exists and the user is a member) - The first membership in the ordered list
- For super-admins without any memberships, the Default Tenant is used as a fallback
Switching the active tenant
Users and super-admins can switch the active tenant by calling the switch API endpoint. This sets theactive_tenant_id cookie to the new tenant ID.
SameSite=lax.
Super-admin tenant management
The/admin/tenants page is only accessible to users with the admin membership role or the is_superadmin flag on their profile.
- Super-admin view
- Admin/Owner view
Super-admins see all tenants across the entire platform. The page uses the Supabase admin client to bypass RLS and list every tenant, sorted by creation date.
| Field | Source |
|---|---|
| Tenant name | tenants.name |
| Tenant ID | tenants.id |
| Creation date | tenants.created_at |
| Owner name | Joined from memberships where role = 'owner' + profiles.full_name |
| Pending invitations | Count of invitations with status = 'pending' |
active_tenant_id cookie to any tenant they have access to — useful for debugging tenant-specific issues without requiring the impersonation flow.
API secrets
The/admin/tenant-secrets route (access-controlled to admin role) is where API credentials used by third-party integrations are stored per tenant. This route is separate from general tenant settings to enforce the principle of least privilege — only admins can view or rotate secrets.
