hcengineering/platform · error · PlatformError
Forbidden
Forbidden
Error message
Forbidden
What it means
During workspace selection (selectWorkspace in server/account/src/utils.ts), the account service verifies the caller actually holds a role in the target workspace. When db.getWorkspaceRole returns null for both the requesting account and (if the workspace allows it) the read-only guest account, it means the account is not a member of the workspace it tried to select, so a Forbidden PlatformError is thrown and no workspace token is issued.
Source
Thrown at server/account/src/utils.ts:871
role: AccountRole.Admin
}
}
let role = await db.getWorkspaceRole(accountUuid, workspace.uuid)
if (role == null && extra?.admin === 'true') {
role = AccountRole.Admin
}
let account = await db.account.findOne({ uuid: accountUuid })
if ((role == null || account == null) && workspace.allowReadOnlyGuest) {
accountUuid = readOnlyGuestAccountUuid
role = await db.getWorkspaceRole(accountUuid, workspace.uuid)
account = await db.account.findOne({ uuid: accountUuid })
}
if (role == null) {
ctx.error('Not a member of the workspace being selected', { workspaceUrl, accountUuid })
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
if (accountUuid !== systemAccountUuid && account == null) {
throw new PlatformError(new Status(Severity.ERROR, platform.status.AccountNotFound, {}))
}
if (accountUuid !== systemAccountUuid && meta !== undefined) {
void setTimezone(ctx, db, accountUuid, account, meta)
}
if (role === AccountRole.ReadOnlyGuest) {
if (extra == null) {
extra = {}
}
extra.readonly = 'true'
}
const wsStatus = await db.workspaceStatus.findOne({ workspaceUuid: workspace.uuid })View on GitHub (pinned to 63e28dc964)
Solutions
- Confirm the account is actually a member: check the workspace role via the account API or db.getWorkspaceRole and re-invite/assign the user to the workspace if missing.
- Make sure the client passes the workspaceUrl of a workspace it already has a token/membership for, not an arbitrary one.
- If guests should be able to join, enable allowReadOnlyGuest on the workspace so the fallback read-only guest account path is used.
- After role changes (removal/re-assignment), obtain a fresh token by logging in again instead of reusing cached credentials.
Example fix
// before: selecting a workspace without membership
await client.selectWorkspace(token, 'https://other-org.example.com')
// after: verify membership first, fall back to correct workspace
const ws = await client.listWorkspaces(token) // workspaces this account belongs to
if (!ws.some(w => w.url === targetUrl)) {
throw new Error(`Not a member of ${targetUrl}; request an invite first.`)
}
await client.selectWorkspace(token, targetUrl) Defensive patterns
Strategy: validation
Validate before calling
const ws = await accountClient.listWorkspaces(token) // workspaces the account belongs to
if (!ws.some(w => w.url === targetWorkspaceUrl)) {
throw new Error(`Account is not a member of ${targetWorkspaceUrl}`)
}
await accountClient.selectWorkspace(token, targetWorkspaceUrl) Type guard
function isMemberWorkspace(ws: { url: string } | null | undefined, url: string): ws is { url: string } {
return ws != null && ws.url === url
} Try / catch
try {
await accountClient.selectWorkspace(token, url)
} catch (e) {
if ((e as PlatformError).status.code === platform.status.Forbidden) {
redirect('/no-access') // prompt to request an invite
} else throw e
} Prevention
- Only select workspaces returned by the account service for the current account
- Refresh membership state after invitations/removals instead of caching
- Handle Forbidden by showing a 'request access' UI rather than retrying
- For guest flows, enable allowReadOnlyGuest on the workspace beforehand
When it happens
Trigger: Calling the selectWorkspace endpoint with a workspaceUrl whose workspace exists but where db.getWorkspaceRole(accountUuid, workspace.uuid) returns null and the workspace does not allow read-only guests (or the guest account also has no role). Happens when a token for one workspace is reused to select another workspace the user was never invited to, or after the user's membership was removed.
Common situations: A client caches an old workspace URL/token after being removed from the workspace; a user types or bookmarks another org's workspace URL; a deployment/migration moved data so role assignments (workspace membership) are missing; integrations hardcoding a workspaceUrl the service account has no role in.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- addSession: rejected by account service
- Workspace not found
- Current user is not allowed to create trainings
- platform.status.SocialIdAlreadyExists
- account.status.WorkspaceNotFound
AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29).
Data as JSON: /api/errors/2128749179e96155.
Report an issue: GitHub.