Multi-tenancy
Manager provides customer-organization isolation through a Project → Applications + Environments + Tenants model. Applications are defined once per project, enabled per tenant, and deployed per environment — one deployed proxy and one deployed backend serve all tenants of an application.
Shipped in the Wave 3 release train
This is the model the Manager implements as of the Wave 3 re-architecture. It replaces the earlier model where an Application belonged to exactly one Tenant and tenants doubled as environments (tenant.type = local|dev|qas|run). If you are upgrading, read the Wave 3 Migration Guide — the schema, the config route, and the proxy config all change in one coordinated release.
What is Multi-tenancy in Manager?
Tenants represent customer organizations (accounts); environments carry the dev/qas/run dimension separately:
- Application defined once — one
Applicationrow per product, living at the project level, not cloned per tenant. - Tenant = customer organization —
tenant.typeis gone. A tenant is an account, and membership (user2tenant) answers "which organizations can this user act in". - Environments are first-class — a
dev | qas | runEnvironmententity per project carries the deployment dimension. Staging and production of one application are one Application row with two deployments. - Per-tenant enablement — a
Tenant2Applicationjoin controls which tenants have which applications enabled and carries per-(tenant, application) configuration overrides. - Single deployment — one running proxy and one running backend per (application, environment) serve every enabled tenant. Onboarding a customer is a tenant row plus an enablement, not a new deployment.
- Tenant switching — users who belong to several organizations switch between them in-app; the proxy selects and enforces the active tenant via
/_tenanton a signed, membership-checked cookie.
Hierarchy
Project (Product / Organization)
├─ Applications (defined once per project)
├─ Environments (dev · qas · run — first-class entity)
│ └─ Deployment (application × environment:
│ domains, deployment spec, machine user,
│ registered backend)
└─ Tenants (customer organizations)
└─ Tenant2Application enablement
(enabled flag + per-(tenant, app) config overrides)The config cascade is project ← tenant ← tenant2application; the Tenant2Application row is the leaf.
Example Structure
Project: ACME Platform
├─ Applications: Portal, Mobile API (each defined once)
├─ Environments:
│ ├─ dev → portal.dev.acme.internal
│ ├─ qas → qas.portal.acmecorp.com
│ └─ run → portal.acmecorp.com
└─ Tenants (customers):
├─ Globex Inc (Portal enabled)
├─ Initech (Portal + Mobile API enabled)
└─ Umbrella Corp (Portal enabled, custom theme override)Three customers, two applications, three environments — and still only one Portal deployment per environment. Onboarding a fourth customer is a Tenant row plus a Tenant2Application enablement, not a new deployment.
Data Isolation
Database Level
Each tenant's data is isolated at the database level:
- Tenant ID on tenant-scoped records
- Query-level filtering in GraphQL resolvers (there is no database row-level security)
- No cross-tenant queries allowed
- Audit trail per project, covering activity in its tenants
Application Level
Applications are tenant-aware:
- The active tenant arrives on every request (
X-Tenant-ID, injected by the proxy after a membership-checked selection). - Configuration is resolved per (application, tenant) — see Configuration Layers.
- Background jobs fan out per enabled tenant from a single trigger delivery: the Manager fires a trigger once per deployment, and the backend iterates its enabled tenants itself (it lists them via
GET /api/machine/application/:id/tenants). There are no per-tenant trigger rows.
User Access
Users are explicitly members of tenants:
- No default tenant access — membership is granted explicitly.
- Tenant roles are configurable per project over a fixed platform permission vocabulary (see Access Model). The platform seeds
member,admin, andownerper project so simple installs never touch the machinery. - Audit logs track membership and role changes.
Self-service is phased
Wave 3 ships the rationalized role model and opt-in role/permission forwarding. Delegated tenant administration, invitations, and end-user self-service are Wave 4 (gated on the security release). See Access Model.
Configuration Inheritance
Settings cascade from project to tenant to the per-(tenant, application) row (see Configuration Layers):
- Project Settings — defaults for everything in the project.
- Tenant Settings — defaults for one customer organization.
- Tenant × Application Settings — the leaf: settings on the
Tenant2Applicationrow for this customer's use of this application.
More specific settings win: tenant2application overrides tenant, tenant overrides project.
Example
Project Level (ACME Platform):
{
"theme": "corporate-blue",
"locale": "en",
"support_email": "support@acme.com"
}Tenant Level (Umbrella Corp):
{
"theme": "umbrella-dark",
"features": ["analytics", "monitoring"]
}Tenant × Application Level (Umbrella Corp × Portal):
{
"locale": "es"
}Effective Configuration (what Portal sees for Umbrella Corp users):
{
"theme": "umbrella-dark", // from Tenant
"locale": "es", // from Tenant × Application (override)
"support_email": "support@acme.com", // from Project
"features": ["analytics", "monitoring"] // from Tenant
}The backend fetches this via GET /api/frontend-config/:application_id/:tenant_id; the handler merges project → tenant → tenant2application settings and returns 404 if the application is not enabled for that tenant. The proxy injects the same resolution into window.__PRODUCTIFY__.
Deprecated config route
The old tenant-less route GET /api/frontend-config/:application_id is kept for one release, serving project + application defaults, and is deprecated. Move consumers to the tenant-qualified route. See Migration Guide.
Domains & Routing
Domains belong to a deployment (application × environment). The proxy's per-application vhost configuration is rendered from Manager data and pushed to the running proxy via Caddy's admin API — adding a domain or application is a Manager operation, not a Caddyfile edit:
portal.dev.acme.internal # Portal × dev
qas.portal.acmecorp.com # Portal × qas
portal.acmecorp.com # Portal × runAll tenants of an environment share its domain; the active tenant is selected per user rather than per hostname. That is the shared topology, and it is the default. A tenant can instead be made dedicated, at which point it gets its own hostname per application (app.tenant.<base>) and its own upstream (app--tenant), and the tenant is pinned by the rendered vhost rather than chosen by cookie — see Shared vs. dedicated tenants, which also covers the DNS records a dedicated tenant requires. An unknown host is answered by a rendered catch-all that returns 421 Misdirected Request — never a default application. See Deployment: Generated Artifacts and Application Routing.
The renderer stamps a minimum proxy version into the pushed config; a proxy plugin that does not understand the config rejects it loudly rather than silently misrouting — the version handshake that keeps an old proxy and a new Manager from disagreeing.
Tenant Switching
Users who belong to more than one tenant pick and switch organizations without re-authenticating. The mechanism is built on the signed, membership-checked cookie shipped in the security release:
- On first visit, the proxy redirects multi-tenant users to a hosted selector (
GET /_tenant/select?redirect=…, server-rendered and theme-neutral); single-tenant users are auto-selected. - The selection is stored in an HMAC-signed cookie scoped per application (
pfy_tenant_<app_id>), re-validated against membership on every request. The proxy never trusts a caller-suppliedtenant_id. - Applications read
available_tenants(id + name) fromwindow.__PRODUCTIFY__to render their own in-app switcher, andPOST /_tenant {tenant_id}to switch. GET /_tenantreturns{current, available: [{id, name, role}]}from the authoritative membership endpoint (cached).- After a switch, the proxy injects the new
X-Tenant-IDon the next request; applications refetch config and realtime layers resubscribe.
On a dedicated host there is nothing to switch: the cookie is never read, the catalog holds exactly the pinned tenant, and POST /_tenant answers 409. See Shared vs. dedicated tenants.
The membership endpoint GET /api/proxy/user/:user_id/tenants?application_id=… (proxy-secret gated) is the single source of truth for both the selector and per-request enforcement: it returns the tenants the user is a member of and that have the application enabled.
Backend Protocol
One registration per (application, environment) via an environment-scoped machine user — not one per tenant. Triggers stay per-application and fire once per deployment; the backend fans out over its enabled tenants itself. Config clients gain a tenant dimension (getFrontendConfig(appId, tenantId) / FrontendConfigForTenant). See Backend Registration.
Use Cases
Multi-Tenant SaaS
One deployment, many customers:
Project: SaaS Product
├─ Applications: Web App, API
├─ Environments: qas, run
└─ Tenants: Customer A, Customer B, Customer C- Onboard a customer with two Manager rows (
Tenant+Tenant2Applicationenablement). - Sell different applications to different tenants via enablement.
- Per-customer configuration overrides on the tenant2application leaf, without config drift.
Multi-Environment Delivery
Environments separate dev, qas, and run of the same application row:
Project: E-commerce Platform
├─ Application: Shop
└─ Environments: dev → qas → runCopying configuration between environments replaces the old cross-tenant application promote — there is nothing to clone.
Multi-Region Deployment
For region-specific data residency, run one Manager per region and route customers to their regional instance; within each region the tenant model above applies unchanged.
Best Practices
Project & Tenant Design
- One project per product/organization.
- Tenants are customers — one tenant per customer organization.
- Environments carry dev/qas/run — never model environments as tenants.
- Set project-level defaults; use the tenant2application leaf only for real per-customer differences.
Security
- Isolate the run environment — production access is separate from dev/qas.
- Verify tenant context in all operations (the injected
X-Tenant-ID— now backed by a signed, membership-checked cookie — is the source of truth). - Audit cross-tenant access attempts.
- Review memberships and role definitions regularly — role edits are audit-logged.
Tenant Administration
Creating Tenants
Tenants are created within a project — via the Manager UI, pfy tenant create, or the GraphQL API. See Managing Tenants.
Enabling Applications
- Open the Application → Tenants tab (or Tenant → Applications).
- Enable the application for the tenant (
pfy application enable --tenant <slug>). - Optionally add tenant × application configuration overrides.
An application that is not enabled for a tenant is invisible to that tenant's users — in the selector, in configuration (404), and on the ESB.
Isolation Guarantees
What is Isolated
- Database records — tenant-scoped data separated by tenant ID.
- Configuration — per tenant and per tenant × application.
- User access — explicit membership, and tenant selection is now membership-checked and signed.
- Audit logs — activity tracked per project.
- API access — filtered by the injected tenant context.
What is Shared
- Application code — same codebase and same running instance for all tenants.
- Infrastructure — shared servers (unless dedicated).
- Database instance — same DB, different data.
Troubleshooting
User Cannot Access Tenant
Reason: the user is not a member of the tenant, or the application is not enabled for it.
Solution:
- Check Tenant → Members; add the user with a role.
- Check the application is enabled for the tenant (
pfy application list-tenants).
Data Appearing in Wrong Tenant
Critical issue — report immediately.
- Verify the tenant ID in the database.
- Check application code filters on the injected
X-Tenant-ID. - Review audit logs for unauthorized access.
See Also
- Wave 3 Migration Guide — old-model → new-model mapping
- Managing Tenants — tenant CRUD
- Shared vs. dedicated tenants — deployment topology, DNS, pilet pins
- Configuration Layers — settings inheritance
- Access Model — roles, permissions, precedence
- Generated Artifacts — install.yaml + rendered deployments