toeverything/AFFiNE · warning · AlreadyInSpace
already_in_space
already_in_space
Error message
You have already joined in Space ${spaceId}. What it means
Produced during inviteByEmail's pre-check loop when the target email belongs to an existing user that already has a workspaceUser record in this workspace (models.workspaceUser.get returns a row). The invite is skipped and the error is returned inside that email's InviteResult entry (results[index].error), not thrown to GraphQL. Code already_in_space, data.spaceId = workspaceId.
Solutions
- Refresh the member list before inviting and filter out emails of current members (including pending ones).
- Treat results[i].error.name === 'already_in_space' as informational — surface 'already a member' per row rather than a hard failure.
- If the pending invite should be resent, revoke/recreate that member entry instead of re-inviting the same email.
Example fix
// before await inviteByEmail(ws.id, ['bob@x.com']); // bob already member -> per-item error // after const members = await getMembers(ws.id); // emails of all statuses const memberEmails = new Set(members.map(m => m.email.toLowerCase())); await inviteByEmail(ws.id, emails.filter(e => !memberEmails.has(e.toLowerCase())));
Defensive patterns
Strategy: validation
Validate before calling
// filter out existing members (any status) before inviting
const memberEmails = new Set(
(await membersQuery({ workspaceId })).map(m => m.email.toLowerCase())
);
const toInvite = emails.filter(e => !memberEmails.has(e.trim().toLowerCase())); Type guard
function isAlreadyInSpaceResult(result: { error?: unknown }): boolean {
return String((result.error as { name?: string } | undefined)?.name ?? '').toLowerCase() === 'already_in_space';
} Try / catch
// per-item result — handle it where you render results
const results = await inviteByEmail(workspaceId, toInvite);
for (const r of results) {
if (isAlreadyInSpaceResult(r)) markRow(r.email, 'Already a member');
} Prevention
- Refresh the member list (including Pending) right before a bulk invite.
- Treat already_in_space as success for UI purposes — the desired state exists.
When it happens
Trigger: Inviting an email whose user is already a member (any status — Pending, Accepted, AllocatingSeat, UnderReview counts, since the row exists). E.g. re-inviting someone whose invite is still pending, or inviting an existing member under a different email alias that resolves to the same user.
Common situations: Re-sending invites because the first email 'looked lost' but is still Pending; inviting a user who already joined via link; stale member lists in the client UI leading to duplicate invites.
Related errors
AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18).
Data as JSON: /api/errors/765e27c4055e2963.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/workspaces/resolvers/member.ts:208
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 2af30773ae)