Skip to content

Tenant Provisioning

Tenant provisioning is how a new organisation becomes a live tenant: the account control plane mints the org’s identity, places it on a cell (and assigns a region- and plan-matched AI cell), creates the DNS records, and tells the cell to stand up the org runtime with its first Owner. This page describes the operational mechanics — the POST /api/provision/org seam, cell placement, and how a tenant hostname is exposed at {slug}.consystence.com.

For the topology these tenants land on, see Cell topology. For the public API surface, see Provisioning API. For the managed cloud, see Cloud deployment.

Provisioning is split across two roles, deliberately:

  • account.consystence.com is the authority and chokepoint. It owns entitlement, rate limiting, the global ceiling, tenant identity, slug uniqueness, DNS/routing, and the first Owner. It decides whether and where to provision.
  • The cell is the executor. It creates the org runtime, applies per-org caps, and recognises the elevated roles. It provisions exactly what the authenticated account service tells it to. A cell never self-provisions on an unknown-tenant request.

The control plane is a thin directory/router that stays out of the data path and out of the auth path — each cell validates Entra External ID tokens directly, so after DNS resolution all tenant traffic is cell-direct. See Authentication and Org & site model.

KeyOwnerRole
Tenant UUID (org_*)Minted at the accountImmutable org identity; directory PK; provision idempotency key; cell-side synthetic org.Id and cross-repo correlation key
slugAccount (unique per canonical domain)Routing label only — the DNS subdomain. Not the membership key
Entra oidEntra External IDMembership key everywhere; rename-safe. email is an attribute / invitation channel, never a key

The cell’s operating key stays the slug internally; it resolves UUID → slug and does not re-key its grains. The slug is unique per canonical domain in the account directory (e.g. acme may exist once on .com and once on .dev).

Provisioning runs as a durable, idempotent sequence anchored by the tenant’s provisioning state. Every downstream step is idempotent, so a stalled run is safe to resume on retry.

sequenceDiagram
    participant Op as Operator / signup
    participant Acc as account.<br/>(control plane)
    participant CF as Cloudflare DNS
    participant Cell as Cell<br/>POST /api/provision/org
    Op->>Acc: request org (slug, orgType, first admin)
    Acc->>Acc: Wall — entitlement + rate limit + global ceiling
    Acc->>Acc: Placement — orgType to canonicalDomain + cell
    Acc->>Acc: Intent — mint tenant UUID, mark provisioning
    Acc->>Acc: Approval gate — park pending operator approval (launch posture)
    Acc->>CF: Upsert {slug} CNAME to cell ingress
    Acc->>Cell: POST /api/provision/org (X-Api-Key)
    Cell-->>Acc: { tenantSlug, orgId, alreadyExisted }
    Acc->>Acc: Confirm — mark active
  1. Wall (front-door half). Entitlement check, a signup→provision rate limit, and a global ceiling. Rejections happen here and never reach the cell.
  2. Placement. A resolver maps orgType to a canonical domain and selects the active cell: enterprise-family org types → .com (slug-gated, manual approval, namespace protection); integrator-family org types and individual sandboxes → .dev (caller-chosen slug validated as a DNS label). Placement also assigns a region- and plan-matched AI cell — fail-closed: no matching AI cell fails the provision. canonicalDomain, region, and cellId are computed account-side and are deliberately absent from the request — the cell does not decide where it sits.
  3. Intent. Mint the tenant UUID and mark the tenant as provisioning. Slug uniqueness is enforced per canonical domain, which serialises a concurrent same-slug request.
  4. Approval gate (launch posture). Unless auto-approval is enabled, the new tenant parks pending operator approval after intent — before any DNS or cell side-effects — and the sequence resumes when a platform operator approves it.
  5. DNS. Upsert the {slug} CNAME to the cell’s ingress via the Cloudflare seam (idempotent). Where the plan includes AI, a per-tenant {slug}.consystence.io alias for the AI surface is provisioned alongside.
  6. Cell. Call POST /api/provision/org (idempotent by UUID).
  7. Confirm. On created or already_existed, mark the tenant active.

The customer-driven journey on account.consystence.com reaches the same wall → intent → approval → DNS → cell → confirm sequence:

  1. Create a free org — name only, no slug, no payment; the creator is seeded as Owner.
  2. Get a paid tier (owner-only), either by requesting a tier for a platform operator to approve, or through hosted Stripe Checkout. During the launch window self-serve checkout may be centrally disabled, in which case the operator-approved tier request is the available path.
  3. Provision the paid tenant (owner-only, caller-chosen slug). Each paid tenant is backed by its own subscription-minted entitlement, so an org may hold multiple paid tenants, and a lapsed subscription affects only its own tenant. New paid provisioning is blocked only while any of the org’s subscriptions is past-due. In-place tier upgrades and trial conversion are supported.

An org can also provision one free trial (simulation) tenant ahead of a paid tier. Sandbox tenants skip the org entirely: any signed-in account provisions an Individual .dev tenant via the console or POST /api/provision, gated by the free quota rather than an entitlement — the personal quota counts only org-less tenants.

POST /api/provision/org on the cell. See Provisioning API and the API reference for the canonical schema.

This is the most sensitive surface on the platform. The account → cell call authenticates with the cell-issued provisioning credential established at cell enrolment — an X-Api-Key minted by the cell’s key service and verified by a dedicated provisioning policy.

It is not the interactive platform-admin policy — that is a human identity; the account service presents its machine credential. A not-yet-enrolled cell fail-closes: a bad or absent key returns 401/403 with no fallback. See Authentication model and API keys.

The cell receives only what it needs to create and seed the org (plus three forward-compatibility fields it currently ignores):

{
"tenantId": "org_ab12cd34",
"slug": "acme",
"orgType": "Enterprise",
"licenceTier": "Enterprise",
"firstAdmin": { "email": "[email protected]", "oid": "4b57..." },
"domainBindings": ["acme.example"]
}
  • tenantId — account-minted UUID; identity and idempotency key.
  • slug — routing label only.
  • orgType — carried for forward-compatibility; the cell accepts-but-ignores it today. Per-org cap profiles keyed on org type are target design — no cell-side cap is keyed on org type today. The caps the cell does enforce derive from elsewhere: licence-seat caps from the account-signed activation artifact, and AI-usage quotas (monthly tier caps and a per-minute rate limit) from per-tenant limits — neither from orgType.
  • licenceTier — carries the org’s tier-ladder rung on the wire (a Basic — null-tier — org can never provision a tenant); a flag the cell currently ignores, not full licence activation. See Licensing.
  • firstAdmin — the first Owner; account-authoritative (see below).
  • domainBindings — reserved as a signup convenience for email-domain joiners; not a security boundary. Also accepted-but-ignored by the cell today — the only binding seeded is derived from the first admin’s email domain (best-effort, shared consumer domains denylisted).
{ "tenantSlug": "acme", "orgId": "org_ab12cd34", "alreadyExisted": false }

orgId echoes the adopted tenant UUID (the cell equality-guards orgId == tenantId). alreadyExisted: false returns 201 Created; true is the idempotent hit and returns 200 OK. The account treats both identically and flips the tenant to Active.

Keyed on the tenant UUID, which is minted once at intent and reused on every retry of the same request — never re-minted. A repeat POST for an existing UUID returns the existing org (alreadyExisted: true), never a duplicate and never a re-seed.

ConditionStatusNote
Bad / empty request, invalid or reserved slug400Validation
Bad / absent X-Api-Key401 / 403Fail-closed; no fallback
Slug already bound to a different tenant409A real conflict — the idempotent same-UUID repeat is 200, never 409
Cell rate limit exceeded429The cell’s own defence-in-depth limit, partitioned per presented key — independent of the account-side wall
Seed failure / internal error500Server defect. The org and its Owner land atomically before any later fallible step, so an org is never left adminless
Entitlement / global ceilingEnforced account-side at the wall; never reaches the cell

The first Owner is account-born. The account writes Owner = the provisioning oid into its elevated-role store and delivers it to the cell inside the tenant seed; the cell recognises Owner/Admin for its cached gate checks but does not originate them. In the self-serve path the Owner’s oid is known and bound at provision time; for operator provisioning (firstAdmin.oid absent) the cell today seeds the first Owner itself as an email-keyed claimable membership, re-keyed to the Entra oid on first verified login — restricting claimable-by-email to non-elevated members is target design, not yet implemented. An org must never be created-but-adminless — org creation and the Owner landing together (or completing on idempotent retry) is the invariant. Org roles are exactly owner, admin, member; see Roles. Per-tenant isolation is covered in Tenant isolation.

Deletion is the owner-gated inverse of provisioning, driven from the account console: /tenants/{slug}/delete presents a type-the-slug-to-confirm page whose submit (POST /tenants/{slug}/delete/confirm) targets the tenant by its immutable UUID and enforces ownership. Confirmation drives the cross-repo teardown: the cell’s DELETE /api/provision/org/{slug} (idempotent — 200 removed, 204 already gone), removal of the {slug} DNS record (and the {slug}.consystence.io AI alias where provisioned), and the directory cascade that deletes the tenant row, every member’s association to it, and its elevated roles. The cell DELETE sits behind the same provisioning credential and cell-side rate limit as the POST.

A tenant becomes reachable once a Cloudflare DNS record for {slug}.consystence.com is pointed at the owning cell’s cloud ingress. Cloudflare proxies the tenant subdomain and terminates TLS at the edge; the cell then maps the request subdomain to the organisation and resolves the tenant. Adding a tenant is therefore a DNS operation against the platform’s Cloudflare zone, not a change to the tenant’s own runtime. Plan-gated tenants also receive a {slug}.consystence.io alias for the AI surface, provisioned with the tenant and removed at deprovision. See Cloud deployment.

Placement assigns each tenant to exactly one cell, and a tenant never spans cells. Today a single production cloud cell serves tenants, enrolled with the account control plane, which provisions onto it end-to-end. The target at public launch is two cells: a dedicated public sandbox cell (.dev, isolating free-tier blast radius) and an enterprise cell (.com, region-pinned for data residency). Provisioning into either is the same wire contract; only the placement decision differs. For the invariants and DNS routing, see Cell topology; for the broader picture, see the architecture overview.