Skip to content

Adding a tenant

Standing up one client on an existing host. For a new host, see host setup.

Pending the role

The tenant_stack role is a stub — it depends on the core stack, which depends on the brain and registry images that do not exist yet. This page records the decided sequence so it is not reinvented later; the playbook command at step 6 does not work today.


The sequence

flowchart TB
    A["1 · Agree the slug<br/><b>permanent</b>"] --> B["2 · Create the tenant<br/>record in the registry"]
    B --> C["3 · Vault password<br/>+ into Bitwarden"]
    C --> D["4 · tenants/{slug}/vars.yml<br/>+ vault.yml"]
    D --> E["5 · Inventory entry"]
    E --> F{"6 · provision-tenant.yml<br/><b>registry pre-flight gate</b>"}
    F -->|"no matching record,<br/>or region mismatch"| G["FAILS CLOSED<br/>fix the registry, retry"]
    G --> B
    F -->|"match"| H["Stack up:<br/>Postgres · n8n · app<br/>· Metabase if bought"]
    H --> I["7 · DNS record<br/>created automatically"]
    I --> J["8 · Verify + reconcile"]

Step by step

1. Agree the slug — it is permanent

The slug appears in DNS, TLS certificates, database names, vault IDs, directory paths and backup paths. It does not change if the client rebrands (ADR-0022 §7).

Lowercase, alphanumeric, no hyphens where avoidable. greenhills, not Greenhills Electrical Pty Ltd.

2. Create the tenant record in the registry

Via the BOS Console. Must include region — the pre-flight gate at step 6 checks it against inventory and fails closed if they disagree (ADR-0024 §6).

This prevents a tenant existing in infrastructure but not in the map: the state where nothing knows who owns the data.

3. Vault password, then Bitwarden

Each tenant gets its own --vault-id, so one leaked password does not open every client (ADR-0027 §3).

Do this before creating any secrets

Provisioning is not complete until the new vault password is in the shared Bitwarden collection (ADR-0024 §11). A vault password only one partner holds is a bus-factor failure that stays invisible until it matters.

4. Tenant variables

infra/tenants/{slug}/vars.yml     # non-secret: region, services, limits
infra/tenants/{slug}/vault.yml    # secrets, --vault-id {slug}

services decides what gets deployed:

Value Deploys
[core, n8n] Postgres, n8n, n8n's Postgres, review app
[core, n8n, metabase] The above plus ~2 GB of Metabase

Only include metabase for clients who bought the dashboard product (ADR-0023 §5). Check the sizing worksheet before adding one to a host near capacity.

Secrets use inline encrypt_string, so variable names stay readable and the change is reviewable in a PR without decrypting.

5. Inventory entry

A tenant is an inventory host whose ansible_host points at the physical box (ADR-0024 §1):

tenants:
  hosts:
    greenhills:
      ansible_host: <bos1 ip>
      tenant_slug: greenhills
      region: au-mel
      services: [core, n8n, metabase]

This is what makes a later move to dedicated hardware a one-line change.

6. Provision ⏳

cd infra
ansible-playbook playbooks/provision-tenant.yml \
  --limit greenhills --vault-id greenhills@prompt

If the pre-flight gate fails, fix the registry record — do not reach for --force. That flag exists for genuine recovery and requires a logged reason.

7. DNS

Created automatically by the dns role using the zone-scoped Cloudflare token. The wildcard *.app.octopodia.com.au already covers the hostname, so nothing is created by hand.

Deletion is never automated

Deprovisioning marks records for removal; a human executes it. A wrong DNS deletion presents to a client as total outage and persists in resolver caches after the mistake is fixed (ADR-0022 §8).

8. Verify

Check Expected
https://{slug}.app.octopodia.com.au Loads, valid certificate
/insights Metabase, only if in services
Registry reconciliation Tenant present, region matches
Containers On the tenant's private network, not reachable from another tenant's
Backup Appears in that tenant's path after the first nightly run
Vault password In Bitwarden, both partners

Before the first paying client

Client-zero — Octopodia's own inbox — runs on the shared host for roughly a month first. We do not learn tenant provisioning on a client who is paying.

Greenhills is the first client tenant, not tenant one.