Skip to main content

ADR 011 — Tenant Admin Self-Service Portal

Status: Accepted (Phase 5) Context date: 2026-07

Context

Tenant administrators (platform_role = "tenant_admin") previously had no self-service portal. Viewing their own tenant's rooms, users, recordings, or billing data required asking a platform operator to look it up on their behalf. This created an operational bottleneck for the platform team and a poor experience for tenant admins who needed routine visibility into their own resources.

The admin gateway and dashboard already existed for platform operators, backed by session-based authentication. Rather than building a separate tenant portal from scratch, the goal was to extend the existing admin surface with scoped, read-mostly access for tenant admins, reusing the Auth service's existing JWT and JWKS infrastructure.

Decision

1. Dual-mode authentication on the admin gateway

The admin gateway now accepts two authentication mechanisms:

  • admin_session cookie -- the existing session flow for platform operators. Unchanged.
  • JWT Bearer token -- tenant admins authenticate through the Auth service and receive a signed JWT. The gateway validates these tokens against the Auth service's JWKS endpoint.

Both paths produce an AdminContext (see below) that downstream handlers consume uniformly. The gateway inspects the incoming request for a session cookie first; if absent, it falls back to the Authorization: Bearer <token> header.

2. AdminContext replaces AdminUser

The old AdminUser context struct carried only the fields relevant to platform operators. A new AdminContext struct replaces it, carrying:

FieldTypeDescription
UserIDUUIDAuthenticated user's ID
EmailstringAuthenticated user's email
Rolestringplatform_admin or tenant_admin
TenantIDUUIDThe tenant the user belongs to (always set for tenant admins; may be empty for platform admins)
Modestringsession or jwt, indicating which auth path was used

All handler signatures that previously accepted AdminUser now accept AdminContext. This is a breaking internal change but affects no external API contracts.

3. Tenant scope enforcement

Every handler enforces tenant boundaries for tenant admin requests:

  • List operations -- the handler forces a tenant_id = <ctx.TenantID> filter, regardless of any tenant filter the caller supplies. A tenant admin cannot list resources across tenants.
  • Get operations -- after fetching the resource, the handler verifies that the resource's tenant matches ctx.TenantID. A mismatch returns 403.
  • Destructive and admin-only operations -- a RequirePlatformOperator middleware gates these routes entirely. Tenant admins receive 403 before the handler executes.

Routes restricted to platform operators:

MethodRouteReason
POST/tenantsTenant provisioning
POST/tenants/:id/suspendTenant lifecycle
POST/tenants/:id/activateTenant lifecycle
POST/users/:id/suspendUser moderation
POST/users/:id/activateUser moderation
DELETE/recordings/:idDestructive data operation
GET/healthInfrastructure visibility
GET/auditCross-tenant audit log access

4. Dashboard dual-login and scoped UI

The dashboard login page presents two tabs:

  • Platform Admin -- the existing session-based login flow against the admin gateway.
  • Tenant Admin -- authenticates against the Auth service's token endpoint. On success, the returned JWT is stored in a tenant_token HttpOnly cookie (Secure, SameSite=Strict). Subsequent dashboard API calls include this cookie, which the Next.js API routes forward as a Bearer token to the gateway.

The dashboard sidebar adapts based on the authenticated role:

  • Hidden for tenant admins: Infrastructure and Audit Log sections.
  • Visible for tenant admins: Rooms, Users, Recordings, and Billing, all scoped to their tenant.

5. No new database tables

This feature introduces no new database schema. It reuses:

  • The Auth service's existing JWKS endpoint for JWT validation.
  • The existing platform_role field on users to distinguish tenant_admin from platform_admin.
  • Existing tenant-scoped queries across all resource tables (rooms, users, recordings, billing).

The only net-new persistence is the tenant_token HttpOnly cookie in the browser, which is stateless from the server's perspective.

Consequences

  • Reduced operational load -- tenant admins no longer need to contact platform operators for routine data lookups. This removes the most common support request category.
  • Minimal blast radius -- the change is additive. Platform operator flows are untouched. The RequirePlatformOperator middleware is a whitelist on destructive routes rather than a blacklist on tenant admin routes, so new routes default to requiring platform access until explicitly opened.
  • Single dashboard codebase -- maintaining one dashboard with role-based visibility is simpler than maintaining two separate frontends, at the cost of conditional rendering logic in the sidebar and route guards.
  • JWT expiry and refresh -- tenant admin sessions are bounded by the JWT's exp claim. If the Auth service's token TTL is short, tenant admins may be logged out more frequently than platform operators whose server-side sessions have independent TTL management. A token refresh flow may be needed if this proves disruptive.
  • Audit trail gap -- tenant admin actions currently flow through the same gateway audit middleware, so they are logged. However, the audit log itself is not visible to tenant admins. If tenant admins need to audit their own actions, a scoped audit view is a future consideration.

Verification

  • Gateway integration tests cover both auth paths and confirm that tenant admin requests are scoped correctly (list returns only own-tenant resources, cross-tenant get returns 403, platform-only routes return 403).
  • Dashboard E2E tests confirm that the tenant admin login flow stores the tenant_token cookie and that restricted sidebar items are not rendered.
  • Manual verification that a tenant admin JWT issued by the Auth service is accepted by the gateway and that an expired or tampered token is rejected.