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, because 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, because 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 around 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 tailscale 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, and 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, with a 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, held by 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.