toeverything/AFFiNE · error · SpaceAccessDenied
space_access_denied
space_access_denied
Error message
You do not have permission to access Space ${spaceId}. What it means
Thrown by assertWorkspace in the quota realtime service (packages/backend/server/src/core/quota/realtime.ts:135). Before touching workspace quota-state realtime rooms it loads models.workspaceUser.getActive(workspaceId, userId); when no active membership row exists it throws SpaceAccessDenied to keep quota events workspace-private.
Solutions
- Confirm the workspaceId is one of the user's active workspaces before subscribing.
- Re-authenticate and re-open the realtime session after membership changes.
- Check that a workspaceUser row exists and is active for the pair.
Example fix
// before
await realtime.subscribe(realtimeWorkspaceQuotaStateRoom(workspaceId));
// after
const role = await models.workspaceUser.getActive(workspaceId, userId);
if (!role) throw new SpaceAccessDenied({ spaceId: workspaceId });
await realtime.subscribe(realtimeWorkspaceQuotaStateRoom(workspaceId)); Defensive patterns
Strategy: validation
Validate before calling
const role = await models.workspaceUser.getActive(workspaceId, userId);
if (!role) {
throw new Error(`user ${userId} is not a member of ${workspaceId}`);
} Type guard
const isSpaceAccessDenied = (e: unknown): e is SpaceAccessDenied => e instanceof SpaceAccessDenied;
Try / catch
try {
await quotaRealtime.publish(event);
} catch (e) {
if (e instanceof SpaceAccessDenied) {
// resync membership, then drop the event silently
return;
}
throw e;
} Prevention
- Validate workspace membership before opening quota realtime rooms.
- Tear down realtime subscriptions as soon as the user's workspace list changes.
When it happens
Trigger: Subscribing to or publishing workspace.quota-state realtime events for a workspace where the user has no active member role: removed member with an open socket, wrong workspaceId, or a test firing events for an arbitrary workspace.
Common situations: User removed from workspace while their realtime connection is still alive, stale client caching an old workspaceId, or multi-workspace clients mixing ids after switching workspaces.
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
- space_access_denied
- authentication_required
- authentication_required
- AUTHENTICATION_REQUIRED
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/0ca5eb676291180e.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/quota/realtime.ts:135
);
}
@OnEvent('workspace.quota_state.changed', { suppressError: true })
async onWorkspaceQuotaStateChanged({
workspaceId,
}: Events['workspace.quota_state.changed']) {
this.publisher?.publish(
'workspace.quota-state.changed',
{ workspaceId },
{ changed: true },
{ room: realtimeWorkspaceQuotaStateRoom(workspaceId) }
);
}
private async assertWorkspace(userId: string, workspaceId: string) {
const role = await this.models.workspaceUser.getActive(workspaceId, userId);
if (!role) {
throw new SpaceAccessDenied({ spaceId: workspaceId });
}
}
private serializeState<T extends Record<string, unknown>>(state: T) {
return Object.fromEntries(
Object.entries(state).map(([key, value]) => [
key,
typeof value === 'bigint' ? Number(value) : value,
])
);
}
}
View on GitHub (pinned to b4c8548c09)