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

  1. Filter out emails of existing members before calling inviteMembers (query members first).
  2. Read the per-entry InviteResult and skip/ignore already_in_space rows.
  3. 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

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.