| Term | Meaning |
|---|---|
| Tenant | One customer's space, e.g. t_aries (“Aries Freight Systems”). The id is permanent: it becomes the partition key in every tenant-scoped table, so it is never renamed or reused. |
| Tenant registry | DynamoDB table ai-sandbox-tenants, owned and written only by obs, encrypted with the platform's own key. Every tenant-aware service reads it. |
groups | The Cognito groups whose members may work in the tenant. New tenants get admins and tenant-<slug>. |
admin_groups | The groups whose members may administer it (invite people, set roles). New tenants get admins and tenant-<slug>-admin. Administrators can always see their tenant too. |
| purpose / role | production or test (test tenants are labelled “· test”); customer or corpus (shared reference data). |
GET /api/cartridges returns tenants: [{id, name, purpose, role}],
computed from your live Cognito groups (looked up at request time, so a group change applies without signing out) against the registry.localStorage, per person). A remembered tenant you can no longer see is dropped.PCSDK.auth.tenant(), PCSDK.auth.onTenantChange(cb), and
PCSDK.auth.tenantHeaders(). PCSDK.api adds x-tenant to every call automatically.requireTenant in shared/modules/tenant.js compares the requested tenant
with the groups in the verified token. Asking for a tenant you may not see is a 403 — it never quietly falls back to one you can.set_config('app.tenant_id', …);
its tables have row-level security FORCEd, and its app role apcore_app cannot bypass it. A query without a tenant returns nothing.One rule, admits() in shared/modules/tenant.js, used by every service:
groups → sees it.admin_groups → sees it (an administrator who couldn't open their own tenant would be a contradiction).admins) → see it only if groups names admins. A tenant can leave
admins out to wall its data off from the platform team; admin_groups' default of admins does not reopen it.'*' in groups → anyone signed in may work in it (a shared workspace). '*' in admin_groups is ignored: opening data never opens administration.ALLOW_DEFAULT_TENANT=false, DEFAULT_TENANT_ID empty).admins). Go to Access → Tenants and create the tenant: id
(t_ + lowercase letters, digits, _), name, purpose, role. Its groups are derived from the id — they can't be typed in,
so nobody can create a tenant whose audience is somebody else's group.tenant-<slug>, tenant-<slug>-admin), writes the registry row
(refusing an id that already exists), and records a tenant-created admin event with who did it.tenant-<slug> to work in it, tenant-<slug>-admin to administer it.| Live today | |
|---|---|
| Tenants | t_aries — Aries Freight Systems (production, customer) |
| Groups | admins, tenant-aries, tenant-aries-admin, cart-doc-vault |
| Registry | ai-sandbox-tenants in account 753974371195 |
| App / service | Status | How |
|---|---|---|
| Console | Done | Lists your tenants, top-bar picker, passes the choice to cartridges. |
| ap-invoice (“AI Automation”) | Done | Follows the Console's tenant (reloads on switch); backend requireTenant; data keyed by tenant_id. |
| Decision core | Done | Re-checks x-tenant (accepts legacy X-Tenant-Id); Postgres row-level security per tenant. |
| obs | Done | Owns the registry and the Access → Tenants / Users screens. |
| Mock Email | Done (Phase 3) | You see the outbox of the Console's tenant. Machine mail with no registered tenant (obs's job alerts) lands in the _platform outbox, which is not a tenant; only platform admins can open it, from the Platform mail switch. |
| Doc Vault | Done (Phase 3) | A new project is stamped with the Console's tenant, and you see only that tenant's projects (owned or shared with you). Projects with no tenant — everything created before Phase 3, and system projects such as Foundation Audit's — are platform projects, shown with a server icon to platform admins only. Another tenant's project answers 404, as if it did not exist. |
| Mock TMS | Done (Phase 3) | The Tenants tab lists tenants from the registry (only those your groups admit, never a corpus tenant) and opens on the Console's tenant; the ledger route refuses any other with a 403. The simulator's own built-in book stays shared: it is the TMS being simulated, not customer data. |
| Foundation Audit | Platform-only (by design) | An operator tool with no customer data: it edits the grounding docs and shows the audit log. It does not follow the tenant. The reports it publishes land in a Doc Vault platform project. |
| Platform tools showing the tenant | Later (Phase 4) | Logs, audit and obs views labelled by tenant. |
PCSDK.api (it sends x-tenant), or spread PCSDK.auth.tenantHeaders() into your own fetch.
Re-load tenant data in PCSDK.auth.onTenantChange, or simply reload the page.shared/modules/tenant.js into lambda/_shared/ (a local_file in your backend.tf, like the Console's),
give the role dynamodb:Scan on the registry, set TENANTS_TABLE, then call
requireTenant({ jwtPayload, requested: headers['x-tenant'], isAdmin, groups }) and return its status/error when refused.tenant_id in every key or row, and never read without it._shared/ copy — change shared/modules/tenant.js and let the plan restage it.Reference: shared/modules/tenant.js (who may see), shared/modules/tenant-admin.js (who may administer),
shared/modules/README.md, and the walkthrough section “Tenant-scoped data” in docs/cartridge-walkthrough.md.
| Situation | Result |
|---|---|
| Registry can't be read | The service keeps its last good list; if it never had one, the list is empty and nobody is admitted. A failed read never invents a tenant. |
Someone edits x-tenant in the browser | 403 from the backend; logged as tenant-refused. |
| Someone is removed from a group | The Console stops listing the tenant on the next load; backends refuse it as soon as their token no longer carries the group (at the latest when it renews). |
| Database query without a tenant | Returns no rows: row security reads a missing app.tenant_id as “no tenant”. |