Skip to content

Latest commit

 

History

History
53 lines (42 loc) · 4.41 KB

File metadata and controls

53 lines (42 loc) · 4.41 KB
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
type
project

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:

Implementation pattern

  1. 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.
  2. 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) containing client_code → connection_string mapping.
  3. Per-request scoped DbContext configured with the resolved connection string. EF Core: use IDbContextFactory or override OnConfiguring with scoped service.
  4. Dapper / Npgsql: connection factory injected per request.
  5. Connection pooling: Npgsql pools per connection string. Many clients = many pools — set sensible Maximum Pool Size per string + monitor.
  6. Subdomain routing: reverse proxy (CloudFront / Nginx / ALB) routes <client>.app.example.com to the same API origin; client code derivable from subdomain as fallback.

Implications for existing docs (need retrofit)

  • 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 by TenantId.
  • No TENANT table, no TenantId columns. 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.

What does NOT change

  • 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

Terminology

  • "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).

Connection-string registry — central control DB

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.

Hard rules

  • 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).