Multi-Tenant Architecture
Every organization on Servelo is an isolated tenant with its own database schema, users, settings, and data. No tenant can access another tenant's data.
Schema isolation
Servelo uses PostgreSQL schema-per-tenant isolation. Each organization gets its own schema named tenant_{slug} (with hyphens replaced by underscores). For example, an org with slug your-company has its data in the tenant_your_company schema.
All tenant tables (users, tickets, clients, quotes, revenue, expenses, settings, etc.) are created inside the tenant schema. The shared public schema holds only cross-tenant records: the orgs table, magic links, refresh tokens, and the user-org index for OAuth routing.
Tenant resolution
The server identifies the tenant from the request's Host header. The subdomain maps to the org slug:
your-company.serveloapp.com โ slug: your-company โ schema: tenant_your_company
In local development, the slug can be passed via the X-Tenant-Slug header or a query parameter when the host is localhost.
Creating a new tenant
New organizations can register at the root domain. During registration:
- A slug is chosen (derived from the organization name, must be unique)
- An org record is created in
public.orgs - A new PostgreSQL schema
tenant_{slug}is created and all tables are migrated into it - The first admin user is created and a session is started
Plans
Each org has a plan: trial, starter, pro, or enterprise. Plans are managed by the system admin and currently control billing tier only. Feature gating by plan is not yet enforced in the application.
| Plan | Description |
|---|---|
| trial | Default for new registrations. Includes a trial end date. |
| starter | Entry-level paid plan. |
| pro | Standard paid plan. |
| enterprise | Top tier. |
Suspending an org
A system admin can suspend an organization by setting is_active = false on the org record. Suspended orgs cannot be accessed, all API requests return a 403. Magic link verification and token refresh are also blocked. The org's data is fully preserved and access can be restored by reactivating.
System admin org
One organization is designated as the system admin org via the SYSTEM_ADMIN_ORG environment variable (set to the org slug). Admin users in that org gain access to the System Admin panel in the application, which provides cross-org management capabilities.
The system admin org itself cannot be deleted or suspended through the admin panel. The master admin account (the admin user in that org) cannot be modified through the panel either.
OAuth user routing
When a user signs in via Google or Microsoft OAuth, Servelo needs to know which tenant they belong to before they are authenticated (since the OAuth callback doesn't include the tenant). The public.user_org_index table maps email addresses to org slugs, allowing the server to route the user to the correct schema after OAuth completes.
Users are added to user_org_index when they are invited or register. A user can exist in multiple tenants (multiple entries with different org slugs).
Key environment variables
| Variable | Description |
|---|---|
SYSTEM_ADMIN_ORG | Slug of the system admin organization. Admins in this org get cross-tenant admin access. |
DATABASE_URL | PostgreSQL connection string for the shared database. |
JWT_SECRET | Secret used to sign access tokens. |
APP_URL | Base URL of the app (e.g. https://serveloapp.com). Used in email links. |
AWS_* | S3 and SES credentials for file storage and email sending. |
GOOGLE_CLIENT_ID / SECRET | Google OAuth credentials. |
MICROSOFT_CLIENT_ID / SECRET | Microsoft OAuth credentials. |
REPLY_HMAC_SECRET | Secret used to sign email reply tokens. |