Documentation source
URL as the sole source of truth for active tenant
Replace JWT-driven, profile-backed "active tenant" state with a URL-first model. Per-tab and per-device independence by construction; switchTenant becomes pure navigation.
See the full design document in `documents/work/2026-04-17-tenant-url-source-of-truth/spec.md` and the accompanying ADR-0003. ## Problem A request's active tenant currently has four coupled sources: URL header, cookie, JWT claim, and profile column. The Node layer prefers URL; RLS prefers JWT. They can disagree, and when they do, data from one tenant can bleed into a page scoped to another. The 2026-04-16 prefetch incident was the headline failure; cross-device drift is a silent second failure mode caused by `auth.users.raw_app_meta_data.active_tenant_id` being a shared row. ## Solution URL is the sole authorization input for tenant. JWT claim, profile column, and "active" cookie are deleted or demoted. The database reads the URL-derived tenant via a PostgREST `db-pre-request` hook that pins a transaction-local GUC. `get_active_tenant_id()` returns the GUC, no fallback. APIs pass the tenant explicitly via URL path (`/api/v1/t/<slug>/...`) or tenant-embedded API keys (`amble_<slug>_<random>`). MCP uses OAuth 2.1 with tenant pinned in the token's audience claim (per MCP spec 2025-06-18). ## Acceptance highlights - Two-tab test: user in tenants A and B opens `/t/a` and `/t/b` — each tab stays in its own tenant across any sequence of actions. - Cross-device test: user signed into tenant A on laptop and tenant B on phone — switching on one device does not affect the other. - `/api/tenants/switch` route is deleted. Tenant switcher becomes `<Link>`. - `profiles.active_tenant_id` column is dropped. - `auth.users.raw_app_meta_data.active_tenant_id` is no longer written by any code path. - pgTAP test covers `private.set_tenant_context()` happy path and deny path. See the full spec for all acceptance criteria and the four-step rollout plan. ## Supersedes `jwt-tenant-remove-profile-fallback.mdx` — that spec removes only the profile fallback but keeps JWT as the runtime source. This one removes both.