Configuration

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 schema names are derived from the org slug at registration time and never change, even if the org name changes.

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:

  1. A slug is chosen (derived from the organization name, must be unique)
  2. An org record is created in public.orgs
  3. A new PostgreSQL schema tenant_{slug} is created and all tables are migrated into it
  4. 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.

PlanDescription
trialDefault for new registrations. Includes a trial end date.
starterEntry-level paid plan.
proStandard paid plan.
enterpriseTop 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

VariableDescription
SYSTEM_ADMIN_ORGSlug of the system admin organization. Admins in this org get cross-tenant admin access.
DATABASE_URLPostgreSQL connection string for the shared database.
JWT_SECRETSecret used to sign access tokens.
APP_URLBase 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 / SECRETGoogle OAuth credentials.
MICROSOFT_CLIENT_ID / SECRETMicrosoft OAuth credentials.
REPLY_HMAC_SECRETSecret used to sign email reply tokens.