toeverything/AFFiNE · error · SpaceAccessDenied
space_access_denied
space_access_denied
Error message
You do not have permission to access Space ${spaceId}. What it means
PermissionService.assertWorkspace evaluates the user's OpenAccess decisions for a workspace-level action (via canWorkspace) and throws SpaceAccessDenied (no_permission / space_access_denied, message 'You do not have permission to access Space <id>.') when the decision is not allowed. It is the standard guard behind workspace controllers, the sync gateway, and quota realtime handlers.
Solutions
- Verify the user's membership/role for the workspace before making the call (canWorkspace instead of assertWorkspace)
- Re-authenticate and refresh the session if membership changed recently
- Request the needed role from the workspace owner, or use an account with sufficient permission
- For internal/service callers, send the proper internal auth headers or pass allowLocal where the API supports it
Example fix
// before
await permission.assertWorkspace({ userId, workspaceId, action: 'Doc.Write' });
// after
const allowed = await permission.canWorkspace({ userId, workspaceId, action: 'Doc.Write' });
if (!allowed) {
return respondWithSignInOrRequestAccess(workspaceId); // graceful denial instead of thrown error
}
await doWorkspaceWrite(); Defensive patterns
Strategy: validation
Validate before calling
const allowed = await permission.canWorkspace({ userId, workspaceId, action });
if (!allowed) {
return respondAccessDenied(workspaceId); // redirect to sign-in / request access
}
await performWorkspaceAction(workspaceId); Type guard
function isSpaceAccessDenied(e: unknown): boolean {
return (e as { extensions?: { code?: string } }).extensions?.code === 'space_access_denied';
} Try / catch
try {
await workspaceAction(workspaceId);
} catch (e) {
if (isSpaceAccessDenied(e)) {
return handleNoMembership(e.extensions.spaceId); // re-auth, request access, or drop workspace locally
}
throw e;
} Prevention
- Prefer canWorkspace checks (boolean) over assertWorkspace when you want graceful degradation
- Re-validate membership after role changes and token refreshes
- Pass proper auth context on every workspace-scoped request; anonymous (undefined userId) is always denied
When it happens
Trigger: Calling a workspace API with a userId that is not a member (or an anonymous/undefined userId when auth is missing); a member performing an owner/admin-only action (e.g. deleting the workspace); membership revoked or invitation expired while the client keeps a valid-looking session; sync-gateway requests missing the required session context (allowLocal not honored).
Common situations: Tokens from one environment used against another; permission cache staleness after role changes; scripts hitting internal endpoints without the internal-auth header; frontends forgetting to pass auth context on new endpoints.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- doc_action_denied
- doc_action_denied
- invalid_invitation
- mention_user_doc_access_denied
- space_access_denied
AI-assisted analysis of toeverything/AFFiNE@591f874dad (2026-08-18).
Data as JSON: /api/errors/9e63b5f9bfeb0584.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/permission/service.ts:110
workspaceId: string;
action: PermissionWorkspaceAction;
allowLocal?: boolean;
}) {
const output = await this.workspacePermissions({
...input,
actions: [input.action],
});
return output.decisions[0]?.allowed ?? false;
}
async assertWorkspace(input: {
userId?: string;
workspaceId: string;
action: PermissionWorkspaceAction;
allowLocal?: boolean;
}) {
if (!(await this.canWorkspace(input))) {
throw new SpaceAccessDenied({ spaceId: input.workspaceId });
}
}
async docPermissions(input: {
userId?: string;
workspaceId: string;
docId: string;
actions: PermissionDocAction[];
allowLocal?: boolean;
}) {
const output = await this.evaluateLoaded({
userId: input.userId,
workspaceId: input.workspaceId,
docs: [{ docId: input.docId, actions: input.actions }],
allowLocal: input.allowLocal,
});
const doc = output.docs[0];
return {View on GitHub (pinned to 591f874dad)