toeverything/AFFiNE · warning · AlreadyInSpace
already_in_space
already_in_space
Error message
You have already joined in Space ${spaceId}. What it means
Thrown during the inviteMembers candidate-collection pass when a target user (matched by email) already has a workspaceUser record for that workspace. This pre-check runs before lock acquisition so already-invited users fail fast. Reported as already_in_space (action_forbidden, HTTP 403).
Source
Thrown at packages/backend/server/src/core/workspaces/resolvers/member.ts:267
const candidates: InviteCandidate[] = [];
const seen = new Set<string>();
for (const [index, email] of emails.entries()) {
try {
const normalizedEmail = email.trim().toLowerCase();
validators.assertValidEmail(normalizedEmail);
if (seen.has(normalizedEmail)) {
throw new ActionForbidden('Duplicate invite email.');
}
seen.add(normalizedEmail);
const target = await this.models.user.getUserByEmail(normalizedEmail);
if (target) {
const originRecord = await this.models.workspaceUser.get(
workspaceId,
target.id
);
if (originRecord) {
throw new AlreadyInSpace({ spaceId: workspaceId });
}
}
candidates.push({
index,
email,
normalizedEmail,
domain: emailDomain(normalizedEmail),
target: target ? { id: target.id } : undefined,
});
} catch (error) {
results[index] = {
email,
error: mapAnyError(error),
};
}
}
View on GitHub (pinned to 26c515e050)
Solutions
- Filter out emails of existing members before calling inviteMembers (query members first).
- Read the per-entry InviteResult and skip/ignore already_in_space rows.
- Refresh the member list before re-running an invite batch.
Example fix
// before inviteMembers(workspaceId, emails); // after const present = new Set((await members(...)).map(m => m.email)); inviteMembers(workspaceId, emails.filter(e => !present.has(e.toLowerCase())));
Defensive patterns
Strategy: validation
Validate before calling
// fetch existing member emails before inviting
const present = new Set((await members(ws, { take: 1000 })).map(m => m.email?.toLowerCase()).filter(Boolean));
const toInvite = emails.filter(e => !present.has(e.trim().toLowerCase()));
if (toInvite.length) await inviteMembers(workspaceId, toInvite); Try / catch
const results = await inviteMembers(workspaceId, emails); // already_in_space rows are expected; filter them as 'success' const actionable = results.filter(r => !r.error || r.error.code !== 'already_in_space');
Prevention
- Query the current member list before constructing the invite batch.
- Treat already_in_space as a non-error in the InviteResult UI.
- Refresh the member list after each invite batch.
When it happens
Trigger: Calling inviteMembers where one of the emails belongs to a user who is already a member (any status) of the workspace.
Common situations: Re-inviting someone who already accepted; inviting a pending member again; the member list UI was stale and the user appeared absent.
Related errors
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/765e27c4055e2963.
Report an issue: GitHub.