hcengineering/platform · error · ApiError
Forbidden
Error message
Forbidden
What it means
After decoding the request token, the export endpoint checks decodedToken.extra.readonly. If that flag is present (not undefined), the token is a read-only token and the (mutating/heavy) export operation is refused with 403 Forbidden. Presence of the flag — not its value — triggers the rejection.
Source
Thrown at services/export/pod-export/src/server.ts:311
const format = parseExportFormat(req.query.format)
const {
_class,
query,
attributesOnly
}: {
_class: Ref<Class<Doc<Space>>>
query?: DocumentQuery<Doc>
attributesOnly: boolean
} = req.body
if (_class == null) {
throw new ApiError(400, 'Missing required parameters')
}
const decodedToken = decodeToken(token)
if (decodedToken.extra?.readonly !== undefined) {
throw new ApiError(403, 'Forbidden')
}
const isAdmin: boolean = decodedToken.extra?.admin === 'true'
const accountClient = getClient(envConfig.AccountsUrl, token)
try {
const info = await accountClient.getLoginWithWorkspaceInfo()
const winfo = info.workspaces[decodedToken.workspace]
if (!isAdmin) {
if (winfo === undefined) {
res.status(401).end('Invalid workspace')
return
} else {
if (winfo.role !== AccountRole.Owner) {
res.status(401).end('Not an owner of workspace')
return
}
}View on GitHub (pinned to 63e28dc964)
Solutions
- Request a non-read-only workspace token (one without extra.readonly) from the account/token service.
- Inspect your token payload (decode it) to confirm extra.readonly is set, and regenerate the token without that restriction.
- If read-only tokens should be allowed to export, change the server check — this is a policy decision, not a client fix.
- Use an admin-capable token (extra.admin) if the operation requires elevated rights downstream.
Example fix
// before
const token = issueToken(wsId, { extra: { readonly: 'true' } })
// after
const token = issueToken(wsId, { extra: {} }) Defensive patterns
Strategy: try-catch
Validate before calling
const decoded = decodeTokenSafe(token)
if (decoded?.extra?.readonly !== undefined) {
throw new Error('Token is read-only; request a non-readonly token for exports')
} Type guard
function isWritableToken(t: { extra?: { readonly?: string } }): boolean {
return t.extra?.readonly === undefined
} Try / catch
try {
await exportWorkspace(params)
} catch (e) {
if (e instanceof ApiError && e.status === 403 && e.message === 'Forbidden') {
token = await issueWritableWorkspaceToken(wsId)
return exportWorkspace({ ...params, token })
}
throw e
} Prevention
- Keep separate tokens for read-only integrations and export jobs.
- Decode and assert token capabilities (extra.admin/extra.readonly) at job startup.
- Do not set extra.readonly on tokens minted for automation pipelines that must export.
When it happens
Trigger: Calling the export endpoint with a token that was issued with extra.readonly set (e.g. a restricted/read-only workspace token); the check runs after the 'Missing required parameters' 400, so the body must already contain _class.
Common situations: CI pipelines configured with a deliberately restricted token; sharing a read-only demo token in documentation; a recent platform change issuing tokens with extra.readonly for external integrations.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- platform.status.Forbidden
- Forbidden: read-only token
- You do not have write access to the target workspace. Owner
- Missing auth info
- Token revoked
AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29).
Data as JSON: /api/errors/530d87923ecfc276.
Report an issue: GitHub.