| name | project-mtp-tenancy-model | ||
|---|---|---|---|
| description | Multi-Tenant Platform uses database-per-client (siloed) isolation. Single API + single frontend deployment. Client code via JWT/header resolves connection string at request time. Subdomain per client. | ||
| metadata |
|
For the multi-tenant-platform repo at D:\Projects\mtp\mtp-docs (GitHub: paramhq/mtp-docs):
User stated requirement (verbatim, paraphrased): Each client has its own Postgres database. Incoming requests carry a client code (HTTP header or JWT claim). Services resolve the per-client Postgres connection string from the client code at request time. One API deployment serves all clients; one React frontend deployment serves all clients. Clients accessed via subdomains (<client>.app.example.com). User said: "you may call it multi tenant or may not call it, but this is what my requirement is".
Why: Architectural constraint, not negotiable. Industry terminology for this pattern: database-per-tenant / siloed tenancy / isolated tenancy. The product surface stays multi-tenant; the isolation model is per-DB.
How to apply:
- ASP.NET Core middleware extracts client code from JWT claim (e.g.
client_code) OR HTTP header (e.g.X-Client-Code). JWT preferred for authenticated requests; subdomain hostname can be a fallback. - Middleware looks up connection string from a registry. Registry options:
appsettings.json(small N), AWS Parameter Store / Secrets Manager (production), or a central control DB (platform_control) containingclient_code → connection_stringmapping. - Per-request scoped
DbContextconfigured with the resolved connection string. EF Core: useIDbContextFactoryor overrideOnConfiguringwith scoped service. - Dapper / Npgsql: connection factory injected per request.
- Connection pooling: Npgsql pools per connection string. Many clients = many pools — set sensible
Maximum Pool Sizeper string + monitor. - Subdomain routing: reverse proxy (CloudFront / Nginx / ALB) routes
<client>.app.example.comto the same API origin; client code derivable from subdomain as fallback.
FEATURE_FLOW_BINDING/FEATURE_FLOW_VERSION/RULE_SET/RULE/FORM_SCHEMA_FIELD/ extension data — all live inside each client DB, not in a shared table keyed byTenantId.- No
TENANTtable, noTenantIdcolumns. Tenant context = which DB the request is connected to. - No row-level security needed for client isolation. Different DB = different data.
- Cross-client analytics: aggregation across N DBs, run from the API tier; no single SQL query spans clients.
- Migration tooling must replay schema migrations against every client DB.
- i18n catalogs: per-client overrides live in client DB; base catalogs are deployment-level static files.
- Per-client schema variation: discouraged. Keep schema identical across clients; vary behaviour via config tables (rules, UI schema, extension fields) inside each client DB.
- Three-issue-kind result pattern (page 05)
- Validation strategy: FluentValidation + Strategy + JSON rule engine (pages 01-04)
- Flow-version concept (still applies, just scoped per client DB)
- React UI patterns
- i18next + IStringLocalizer
- "Multi-tenant" still describes the product surface (one deployment, many clients). Keep repo name.
- For implementation discussions prefer "client" over "tenant" since each client has its own DB. Avoids confusion with shared-DB multi-tenancy.
- "Client code" = short stable string identifier per client (e.g.
acme,beta-uni).
The registry of client_code → connection_string itself lives somewhere. Recommended: a small central Postgres DB (platform_control) with tables: client (id, code, name, db_connection_string, subdomain, active, created_at), and tied service for management. This is the only place where a TenantId-like concept exists, but it's an operational concern, not a feature concern.
- API code never hardcodes a client. Client comes from request context.
- Service-layer code never opens a connection directly with a hardcoded string. Always via the resolved per-request factory.
- Tests run against a disposable test DB. Integration tests provision/teardown per-test client DBs.
Related: pairs with [[project-mtp-postgres-ltree]] (Postgres + ltree decision).