Febasidocs
Concepts

Multi-tenancy

How Febasi Auth isolates tenants — logically, physically, and atomically — without leaking identity across organizations.

The Auth is multi-tenant from the database up. Every user, role, permission, refresh token, and audit log carries a tenant_id, and every authenticated request is scoped to exactly one tenant.

There are two isolation strategies, and they coexist:

Logical isolation (default)

Single shared database. Tenant boundaries enforced by composite foreign keys and (tenant_id, …) unique indexes.

Physical isolation (opt-in)

Each tenant has its own dedicated PostgreSQL database, registered in the central catalog with an encrypted connection string.

Tenants

A tenant is identified by a unique code — a short, URL-safe slug like febasi, acme-corp, oboi. The code is what clients send during login.

FieldTypeNotes
idUUIDInternal primary key.
codestringGlobally unique. Used at login.
namestringDisplay name.
statusenumACTIVE or INACTIVE.
auth_configJSONBPassword policy, token expirations, etc.
jwt_secret_*variesOptional per-tenant JWT signing material.

Tenants are managed under /api/v1/tenants/... — see Endpoints for the full list.

Logical isolation

Without any extra setup, every tenant is isolated through the tenant_id column on every user-facing table:

tenants
users (tenant_id, email)
roles (tenant_id, name)
permissions (tenant_id, scope, action)
refresh_tokens (tenant_id, user_id)
auth_logs (tenant_id, user_id)

Two safety nets back this up:

  1. Composite unique indexes. A user's email, username, and cpf_cnpj are unique per tenant, never globally. Tenant A and tenant B can both have a joao@febasi.com.br.
  2. Cross-tenant access guard. Every controller checks that the resource it's about to read or modify belongs to the caller's tenant. Crossing that boundary returns CROSS_TENANT_ACCESS (HTTP 403) — Febasi Auth never silently leaks rows.

Physical isolation

For tenants that need full data residency (regulatory requirements, large isolation guarantees, or a "noisy neighbor" they want to escape), the Auth API supports per-tenant dedicated databases.

Register a database

PUT /api/v1/tenants/{tenantId}/database
{
  "connectionString": "postgresql://user:pass@host:5432/tenant_acme",
  "databaseType": "postgres",
  "poolSize": 10,
  "idleTimeoutSeconds": 30,
  "connectionTimeoutSeconds": 5
}

The connection string is encrypted with AES-256-GCM before being stored, using the service-wide ENCRYPTION_KEY. It is never returned in plaintext.

Wait for validation

The new entry lands as pending_validation. The service connects, runs a schema check, and either flips it to active or failed with the error recorded on the row.

Health checks

Active databases are pinged periodically. Three consecutive failures mark the tenant database inactive and the central database is used as a fallback for read-only metadata. The /health endpoint reports activePools and cachedConfigs so you can monitor the state of the routing layer.

How tenant context is resolved

Auth methodSource of tenant_id
JWTThe tenantId and tenantCode claims inside the access token.
Client KeyThe X-Tenant-Code header sent alongside the API key.
LoginThe tenantCode field in the request body.

A global middleware validates the resolved tenant on every request. If the tenant is INACTIVE or unknown, the request is rejected with TENANT_NOT_FOUND before any business logic runs.

What this means for clients

  • You will only ever receive data from your tenant — no filters needed client-side.
  • If you build an admin UI that switches tenants, you must obtain a fresh token per tenant. Refresh tokens are tenant-scoped.
  • Per-tenant JWT secrets are independent. A token from tenant A cannot be validated as a token from tenant B even if you somehow swap the secret.

Want even stronger isolation?

Combine physical isolation with per-tenant JWT secrets. The same cryptographic blast radius as having one Auth per tenant — at a fraction of the operational cost.

On this page