{"record":{"id":"907e929ce543442f","repo":"toeverything/AFFiNE","slug":"no-more-seat","errorCode":"no_more_seat","errorMessage":"No more seat available in the Space ${spaceId}.","messagePattern":"No more seat available in the Space (.+?)\\.","errorType":"exception","errorClass":"NoMoreSeat","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/workspaces/resolvers/member.ts","lineNumber":286,"sourceCode":"              email: candidate.normalizedEmail,\n              registered: false,\n            });\n          }\n\n          const existingMember = await this.models.workspaceUser.get(\n            workspaceId,\n            target.id\n          );\n          if (existingMember) {\n            throw new AlreadyInSpace({ spaceId: workspaceId });\n          }\n\n          if (!isTeam) {\n            const needMoreSeat =\n              quota.memberCount + successfulCandidates.length + 1 >\n              quota.memberLimit;\n            if (needMoreSeat) {\n              throw new NoMoreSeat({ spaceId: workspaceId });\n            }\n          }\n\n          // no need to check quota, directly go allocating seat path\n          if (isTeam) {\n            const role = await this.models.workspaceUser.set(\n              workspaceId,\n              target.id,\n              WorkspaceRole.Collaborator,\n              {\n                status: WorkspaceMemberStatus.AllocatingSeat,\n                source: WorkspaceMemberSource.Email,\n                inviterId: me.id,\n              }\n            );\n            await this.allocateAvailableTeamSeats(\n              workspaceId,\n              quota.memberLimit","sourceCodeStart":268,"sourceCodeEnd":304,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/49536827790702ad0ab6d3be3405d891a6b19f83/packages/backend/server/src/core/workspaces/resolvers/member.ts#L268-L304","documentation":"Produced during inviteByEmail's locked processing loop for non-team workspaces: seat math (quota.memberCount + successfulCandidates.length + 1 > quota.memberLimit) shows adding this candidate would exceed the plan's member limit. The candidate is skipped and NoMoreSeat is written to its InviteResult.error. Code no_more_seat, HTTP 400 semantics; note the running successfulCandidates count means earlier candidates in the same batch may have consumed the last seats.","triggerScenarios":"Inviting N emails to a free/pro (non-team) workspace that has fewer than N free seats; a batch where the first emails fill the remaining seats and later ones hit this error per-item.","commonSituations":"Hitting the member limit of a free plan mid-onboarding; inviting a large list without checking the member quota first; seat count drifting from the UI because pending invites also count toward memberCount.","solutions":["Check the workspace member quota (memberLimit - memberCount) before inviting and cap the batch accordingly.","Free seats by removing inactive members, or upgrade the workspace plan / apply a license to raise memberLimit.","Read results[i].error.name === 'no_more_seat' per row and prompt the owner to upgrade instead of retrying — retrying cannot succeed."],"exampleFix":"// before\nawait inviteByEmail(ws.id, 10_emails); // 3 seats left -> later rows: no_more_seat\n\n// after\nconst { memberCount, memberLimit } = await getWorkspaceSeatQuota(ws.id);\nconst free = memberLimit - memberCount;\nif (emails.length > free) {\n  await confirm(`Only ${free} seats left — invite first ${free} and upgrade?`);\n}\nawait inviteByEmail(ws.id, emails.slice(0, free));","handlingStrategy":"fallback","validationCode":"// pre-check seat availability before a non-team invite batch\nconst { memberCount, memberLimit } = await getWorkspaceSeatQuota(workspaceId);\nconst freeSeats = memberLimit - memberCount;\nconst sendable = emails.slice(0, Math.max(0, freeSeats));\nif (emails.length > sendable.length) promptUpgradeFor(emails.length - sendable.length);","typeGuard":"function isNoMoreSeatResult(x: unknown): boolean {\n  const err = (x as { error?: unknown })?.error ?? x; // works for per-item results and thrown errors\n  return String((err as { name?: string }).name ?? '').toLowerCase() === 'no_more_seat';\n}","tryCatchPattern":"const results = await inviteByEmail(workspaceId, emails);\nfor (const r of results) {\n  if (isNoMoreSeatResult(r)) { showUpgradePrompt(); break; } // retrying cannot help\n}","preventionTips":["Check memberLimit - memberCount before bulk invites.","Remember pending invites count toward seats — base the check on live quota, not the UI list."],"tags":["affine","graphql","invite","quota","seat-limit","plan-limit"],"backgroundTag":"quota-limit-exceeded","analyzedSha":"49536827790702ad0ab6d3be3405d891a6b19f83","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}