Skip to content

Portal team scoping ​

One portal user can belong to several customer teams. Keys, usage, balances, plans, members and audit events belong to a team, so an authenticated request must name the team it intends to read or change.

Required header ​

Send the active portal workspace UUID on every authenticated team-scoped request:

http
X-MicroAuth-Workspace-ID: 9f0d7db9-b189-4d87-8fcc-9c24ee560c6c

The API validates all three facts for each request:

  1. the UUID identifies a portal workspace;
  2. the workspace belongs to the tenant resolved from the current portal host;
  3. the signed-in portal user is still a member of that workspace.

A valid cookie is not enough to select a team. Missing, malformed or unauthorized IDs fail before the handler reads or mutates team data.

Browser storage ​

Keep the active workspace ID in sessionStorage. The browser scopes this storage to the current portal origin and tab, and each portal origin resolves one tenant. Do not put the selection in localStorage, a cross-tab singleton, or a server-side "active team" preference.

ts
const storageKey = 'microauth.portal.workspace-id'
const workspaceId = sessionStorage.getItem(storageKey)

const response = await fetch('/api/v1/billing', {
  credentials: 'include',
  headers: {
    'X-MicroAuth-Workspace-ID': workspaceId ?? '',
  },
})

After login, call GET /api/v1/me. If the stored ID is not one of the returned memberships, choose a deterministic valid team and update sessionStorage. Revalidate after every team switch. Clear the selection and all team-private state on logout or before moving to a different portal origin.

GET /api/v1/me may accept the header when present. It can select the first valid membership when the header is absent so the UI can bootstrap. Other team-scoped routes should require the explicit header.

Switching teams ​

POST /api/v1/teams/switch sends the current workspace header plus the target team_id. The API validates membership in both teams and returns a success message. The browser then updates its own tab's storage. Switching does not write a global Redis preference, so two tabs can safely use different teams.

Private plan data ​

GET /api/v1/branding is public and cacheable only when it contains non-sensitive branding and public plan information. It must not include a private plan assigned to one team.

Fetch selectable plans from authenticated GET /api/v1/plans with the workspace header. That response can include public plans plus private plans assigned to the selected team, and it must send:

http
Cache-Control: private, no-store

Never keep this response in a global branding store or reuse it after a tenant, user or workspace change.

Team-scoped route checklist ​

Apply explicit workspace validation to keys, usage, billing, plans, members, team settings and audit reads and mutations. Mutation requests also keep the normal session-cookie CSRF protection. The workspace header selects scope; it does not replace authorization, role checks or CSRF validation.

In the current OpenAPI contract, /api/v1/plans, keys, usage, billing, members, audit, team switching, team creation, and team renaming require the header. /api/v1/me alone accepts it optionally for bootstrap. Login, registration, password recovery, and public branding are user-scoped or public flows and do not use it.

MicroAuth is a product of Zyref, LLC.