hcengineering/platform · error
Workspace mismatch
Error message
Workspace mismatch
What it means
withOwnerAsync rejects with 401 'Workspace mismatch' when the route defines req.params.workspace and the token's workspace claim (req.token.workspace) does not equal it. The token is valid but was issued for a different workspace than the one the request targets.
Source
Thrown at services/payment/pod-payment/src/middleware.ts:80
res.status(403).json({ message: 'Missing auth info' }).end()
return
}
req.loginInfo = loginInfo
next()
}
export const withOwner = (req: RequestWithAuth, res: Response, next: NextFunction): void => {
void withOwnerAsync(req, res, next)
}
const withOwnerAsync = async (req: RequestWithAuth, res: Response, next: NextFunction): Promise<void> => {
if (req.token === undefined || req.token == null) {
res.status(401).json({ message: 'Token error' }).end()
return
}
if (req.params.workspace != null && req.token.workspace !== req.params.workspace) {
res.status(401).json({ message: 'Workspace mismatch' }).end()
return
}
if (req.token.account !== systemAccountUuid && req.token.extra?.admin !== 'true') {
const accountClient = getAccountClient(req.headers.authorization?.split(' ')[1])
const loginInfo = req.loginInfo ?? (await accountClient.getLoginInfoByToken())
if (loginInfo == null) {
res.status(403).json({ message: 'Missing auth info' }).end()
return
}
if (!('role' in loginInfo)) {
res.status(401).json({ message: 'Missing workspace role' }).end()
return
}
if (loginInfo.role !== AccountRole.Owner) {
res.status(401).json({ message: 'Workspace owners only' }).end()
return
}
}View on GitHub (pinned to 63e28dc964)
Solutions
- Use the workspace id that matches the token's workspace claim, or re-authenticate to get a token for the target workspace
- In the client, refresh the token whenever the active workspace changes
- Check for stale/hardcoded workspace ids in API calls and fix the URL construction
- Verify token issuance includes the correct workspace claim for the session
Example fix
// before
await api.get(`/ws/${oldWorkspaceId}/invoices`, { headers: { Authorization: `Bearer ${oldWsToken}` } })
// after
await api.get(`/ws/${currentWorkspaceId}/invoices`, { headers: { Authorization: `Bearer ${currentWorkspaceToken}` } }) Defensive patterns
Strategy: validation
Validate before calling
// decode token workspace claim client-side and compare to target workspace
const claims = JSON.parse(atob(token.split('.')[1]))
if (claims.workspace !== targetWorkspaceId) {
await switchWorkspaceToken(targetWorkspaceId) // refresh token scoped to target workspace
} Type guard
function tokenMatchesWorkspace(token: { workspace?: string } | null, workspaceId: string): token is { workspace: string } {
return token != null && typeof token.workspace === 'string' && token.workspace === workspaceId
} Try / catch
try {
const res = await callWorkspaceApi(workspaceId)
} catch (err) {
if (err.response?.status === 401 && err.response.data?.message === 'Workspace mismatch') {
await refreshTokenForWorkspace(workspaceId)
// retry once; otherwise reset client to the token's workspace
}
} Prevention
- Refresh the token whenever the user switches active workspace in the UI
- Never hardcode workspace ids in API URLs; derive them from current app state
- Store tokens keyed by workspace, not per user only
- On 401 Workspace mismatch, surface 'switch workspace' UI instead of retrying
When it happens
Trigger: Calling /ws/:workspace/... with a :workspace path param that differs from the workspace encoded in the bearer token — e.g. client app switched workspaces but kept the old token, or a hardcoded workspace id in the URL.
Common situations: Frontend switching active workspace without refreshing the token; copying an API URL from another workspace/user; cached tokens stored per-user rather than per-workspace; workspace renamed/re-created so old token claims no longer match.
Related errors
- Missing workspace role
- platform.status.Unauthorized
- Unauthorized
- Couldn't find workspace with the provided token
- Social ID is missing
AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29).
Data as JSON: /api/errors/19e55db6b1e17373.
Report an issue: GitHub.