02 β Tenant Provisioning & Orchestration
How a new tenant comes into existence, who decides its deployment shape, and how its compute and database are brought up. This is a control-plane flow (core-api) that delegates database creation to the data plane (tenant-api).
Entry point: createTenant
TenantProvisioningService.createTenant() β src/core-api/src/services/tenant/TenantProvisioningService.ts:84.
The flow, in order:
- Validate the request (
validateTenantRequest). - Create the tenant record (
createTenantRecord,:443) in the core DB.has_dedicated_databasedefaults to true. - Add the primary domain if provided (
domainService.addCustomDomain). - Decide the deployment strategy β
deploymentStrategy.determineDeploymentType(characteristics)(:125). The characteristics fed in (:116-123) are:planType,complianceRequired,memberCount,monthlyApiCalls, andregion. β returns'shared'or'container_per_tenant'. - Provision the deployment (
provisionDeployment,:484) β forcontainer_per_tenant, create the dedicated compute (below). - Record deployment info (
updateTenantDeploymentInfo,:525): setdeployment_strategy,has_dedicated_database, and write aTenantDeploymentrow. - Generate + encrypt + store the license (
generateAndEncryptLicenseβstoreLicense,:134-135). See 04. - Create the tenant database (
createTenantDatabase,:1108) β but only whenhas_dedicated_databaseis true; shared tenants reuse the region DB. This step delegates to the tenant-api (below). - Create the bootstrap admin user (
createBootstrapUser). - Notify the tenant-api to load the license into cache (fire-and-forget).
The deployment decision: shared vs container-per-tenant
determineDeploymentType maps tenant characteristics to one of two deployment types:
| Deployment type | What it means | When |
|---|---|---|
shared | The tenant is served by a pooled tenant-api that handles many tenants; its rows live in the region's shared database, scoped by tenant_id. No dedicated compute. | Default for small/standard tenants β cheapest. |
container_per_tenant | A dedicated tenant-api instance is brought up for this tenant, pinned to it, with its own tenant_<uuid> database. | Compliance-required, large, or high-traffic tenants β isolation & independent scaling. |
The two drivers that push a tenant to container_per_tenant are noisy-neighbour / scale and compliance / data residency.
Provisioning dedicated compute
provisionDeployment(tenantId, deploymentType, request) β :484.
- For
shared: returns immediately, no compute to create (:485-488). - For
container_per_tenant(:490): builds a container config and callsorchestrationService.createTenantContainer(config)(:509). The config (:494-507):
image: ip4x/tenant-api:latest
environment: TENANT_ID = <uuid> # pins DB init + tenant scope
TENANT_DB_NAME = tenant_<uuid> # the dedicated DB
STORAGE_PREFIX = <uuid>/
NODE_ENV = ...
resources: memory 512m, cpu 0.5 # per-tenant resource cap
It returns { containerId, endpoint }.
β οΈ Known gap. The container env above setsTENANT_ID,TENANT_DB_NAME,STORAGE_PREFIX,NODE_ENVbut does not setREDIS_PREFIX. All instances share one Redis; BullMQ keys queues by name, so twocontainer_per_tenantinstances without distinctREDIS_PREFIXvalues would bind the same queue names and steal each other's jobs. SettingREDIS_PREFIX: tenantIdin this config closes it. See 03 β queue isolation.
The orchestrator
createTenantContainer is implemented by KubernetesOrchestrationService β src/core-api/src/services/deployment/KubernetesOrchestrationService.ts. It uses @kubernetes/client-node and creates namespaced Kubernetes objects per tenant: a Deployment (the image + env + resource limits), a Service, and an Ingress (via NetworkingV1Api). It also exposes updateTenantContainer / read operations for the deployment lifecycle.
This means the per-tenant-instance model is not greenfield β it is implemented in skeleton. The remaining work to operate it at scale is (a) the REDIS_PREFIX gap above, (b) wiring per-region database hosts (below), and (c) confirming the Ingress/TLS/namespace/secret path end to end.
Database creation is delegated to the data plane
createTenantDatabase(tenantId) β :1108 β does not run CREATE DATABASE from core-api. Instead it:
- Resolves the tenant's API endpoint (
getTenantApiEndpointβ the region'stenant_api_url, or the dedicated container's endpoint). - Makes an internal HTTP call:
POST {tenantApiEndpoint}/internal/database/initializewith headerx-internal-secretand a 10-minute timeout (:1122-1134). - The tenant-api then runs
CREATE DATABASE tenant_<uuid>and all migrations itself, via itsinit.ts initialize()(the same migrator behindnpm run db:migrate:prod).
This is the crux: each tenant-api instance owns its own schema. A newly provisioned dedicated instance boots, is told to initialize, and brings up its own database. There is no central migration fan-out β the data plane is self-sufficient. See 03 for the migration mechanics.
Regions as the residency unit
A Region (src/core-api/src/database/schema.ts, region_key e.g. za-north, eu-west-1) carries a tenant_api_url (the regional data-plane endpoint) and a shared_database_name. A tenant's region_id decides which regional tenant-api serves it and where its data lives. For full data residency, a dedicated tenant's database should be created on the in-region Postgres and its instance run in that region's cluster β the per-region database host is noted in the schema as a future field (db_connection_string ... per region) and is the main residency seam still to wire.
Sequence summary
core-api createTenant
ββ create tenant record (has_dedicated_database=true default)
ββ determineDeploymentType β shared | container_per_tenant
ββ provisionDeployment
β ββ (container_per_tenant) KubernetesOrchestrationService.createTenantContainer
β β Deployment + Service + Ingress, env TENANT_ID/TENANT_DB_NAME/...
ββ generate + encrypt + store license (β Tenant.encrypted_license)
ββ createTenantDatabase
β ββ HTTP POST {tenantApiEndpoint}/internal/database/initialize
β β tenant-api runs CREATE DATABASE tenant_<uuid> + migrations (DATA PLANE)
ββ createBootstrapUser
ββ notify tenant-api to cache license
Key source paths: src/core-api/src/services/tenant/TenantProvisioningService.ts, src/core-api/src/services/deployment/KubernetesOrchestrationService.ts, src/core-api/src/database/schema.ts (Region, Tenant, TenantDeployment), src/tenant-api/src/database/init.ts.