Cross-tenant admin views without waiting for the upstream auth provider
The blocker
KAHN’s operator-as-admin opens the SPA atkahn.host. The
nav says alex@devarno.cloud · admin. The agents view says “no
agent runs” — but the dogfood tenant has 44 runs visible at the
API layer. The screenshots show a working surface that’s
displaying empty data correctly: the operator’s session resolves
to their personal tenant, not the dogfood tenant.
Root cause: the auth provider’s handoff JWT emits {sub, email, name, role, nonce} but not org_id. The SPA + backend
correctly fall back to tenant = sub → operator’s personal
tenant. RLS does its job. Nothing is broken.
The standard fix is to ask the auth provider’s owner to add
org_id to the JWT. That’s a cross-repo, operator-mediated
change. It works; it’s just slow.
The shipped fix
Three coordinated PRs that don’t wait on the auth provider:- Truthful empty-state copy — the SPA’s empty state names the actual root cause (“your session may be scoped to a different tenant; admin selector forthcoming”) instead of misleading text about CI runs.
- Backend admin tenant override —
X-KAHN-Tenant: <uuid>request header honored by the backend’sresolve_effective_ tenant()iffprincipal.role == "admin". UUID-validated, tenant-existence-validated, structured-log-audited. Read-only by construction (mutations use a separate dependency). - Frontend admin selector — dropdown in the nav populated
from
GET /api/admin/tenants. Visible only to admin role. On change, dispatcheshashchangeto re-paint the current view; every subsequent fetch carries the new header.
kahn-internal (dogfood) → SPA
re-fetches with X-KAHN-Tenant: a0e539b3-... → backend resolves
the override → app.current_tenant set → RLS gates rows correctly
→ operator sees the populated agents view.
Why this works without the upstream change
Two layers of authorization compose cleanly:- JWT verification (unchanged) →
principal.role. - Application-layer override (new) →
principal.role == "admin"enables the header path; non-admin silently ignored. - RLS (unchanged) → gates rows on
app.current_tenant, which is set from the override-resolved UUID.
tests/integration/test_agent_rls.py) proves the composition.
When the upstream auth provider eventually adds org_id, this
KAHN-side override becomes a power-user feature — operators can
still impersonate any tenant, and the JWT becomes the default
binding for non-admin sessions. Nothing has to be torn down.
What it cost
- PR 1: ~3 hours including the planner pass. 4 files, 334 insertions.
- PR 2: ~2 hours. 4 files, 505 insertions including 13 unit tests + 3 integration tests.
- PR 3: ~2 hours. 5 files, 382 insertions including 3 vitest cases + 2 Playwright cases.
- Cross-repo deps: zero. The auth provider’s roadmap remains the auth provider’s roadmap.
Where this generalises
Any RLS-scoped multi-tenant SaaS where:- The JWT is verified upstream but doesn’t carry tenant claims yet.
- The principal’s role is available via the JWT.
- Admin operators need read-side cross-tenant visibility for support / debugging / dogfood inspection.
atlas/doctrines/admin-tenant-override-without-jwt-update.doctrine.md.
Cross-references
- KAHN PRs: #33 (immediate), #34 (backend), #35 (frontend).
- Implementation files:
backend/kahn/cloud_auth.py::resolve_effective_tenant,frontend/src/auth.ts::setActiveTenant,frontend/src/components/nav.ts(selector). - Companion learning:
atlas/learnings/2026-04-27-kahn-northstar- delivery-shape.md— the broader sequential-PR pattern.