{"record":{"id":"a6bc660940e6299e","repo":"toeverything/AFFiNE","slug":"member-quota-exceeded","errorCode":"member_quota_exceeded","errorMessage":"You have exceeded your workspace member quota.","messagePattern":"You have exceeded your workspace member quota\\.","errorType":"exception","errorClass":"MemberQuotaExceeded","httpStatus":402,"severity":"error","filePath":"packages/backend/server/src/core/quota/service.ts","lineNumber":118,"sourceCode":"      await this.quotaState.reconcileWorkspaceQuotaState(workspaceId);\n\n    return {\n      memberCount: state.memberCount,\n      memberLimit: state.seatLimit,\n    };\n  }\n\n  async tryCheckSeat(workspaceId: string, excludeSelf = false) {\n    const quota = await this.getWorkspaceSeatQuota(workspaceId);\n\n    return quota.memberCount - (excludeSelf ? 1 : 0) < quota.memberLimit;\n  }\n\n  async checkSeat(workspaceId: string, excludeSelf = false) {\n    const available = await this.tryCheckSeat(workspaceId, excludeSelf);\n\n    if (!available) {\n      throw new MemberQuotaExceeded();\n    }\n  }\n\n  formatWorkspaceQuota(\n    quota: Omit<WorkspaceQuotaType, 'humanReadable'>\n  ): WorkspaceQuotaHumanReadableType {\n    return {\n      name: quota.name,\n      blobLimit: formatSize(quota.blobLimit),\n      storageQuota: formatSize(quota.storageQuota),\n      storageQuotaUsed: formatSize(quota.usedStorageQuota),\n      historyPeriod: formatDate(quota.historyPeriod),\n      memberLimit: quota.memberLimit.toString(),\n      memberCount: quota.memberCount.toString(),\n      overcapacityMemberCount: quota.overcapacityMemberCount.toString(),\n    };\n  }\n","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/quota/service.ts#L100-L136","documentation":"Thrown by QuotaService.checkSeat when tryCheckSeat reports the workspace's active member count has reached or exceeded its seatLimit. It is the hard gate that prevents adding new members (or accepting invites) once the billing plan's seat quota is exhausted.","triggerScenarios":"Inviting or accepting an invite when (memberCount - excludeSelf) >= memberLimit; member count reconciled upward by quota state sync just before an invite; plan downgrade reduced memberLimit below current memberCount.","commonSituations":"Self-host or cloud workspace on a Free/Pro plan hitting the seat cap; owner tries to invite after a teammate's pending invite was accepted; automation scripts inviting batches without pre-checking seats.","solutions":["Call tryCheckSeat(workspaceId, true) before issuing the invite and surface the result to the user.","Upgrade the workspace plan to raise memberLimit, or remove inactive members first.","If the count is stale, trigger quotaState.reconcileWorkspaceQuotaState to refresh before retrying."],"exampleFix":"// before\nawait this.quota.checkSeat(workspaceId, true);\n\n// after\nconst hasSeat = await this.quota.tryCheckSeat(workspaceId, true);\nif (!hasSeat) {\n  throw new MemberQuotaExceeded('Upgrade your plan to add more members.');\n}","handlingStrategy":"validation","validationCode":"const hasSeat = await quota.tryCheckSeat(workspaceId, true);\nif (!hasSeat) {\n  return { canInvite: false, reason: 'seat-quota-exceeded', upgradeUrl };\n}","typeGuard":"// n/a","tryCatchPattern":"try {\n  await quota.checkSeat(workspaceId, true);\n} catch (e) {\n  if (e instanceof MemberQuotaExceeded) {\n    // surface upgrade prompt instead of failing the invite form\n    return res.status(402).json({ code: 'member_quota_exceeded', upgradeUrl });\n  }\n  throw e;\n}","preventionTips":["Call tryCheckSeat before rendering the invite button so the UI can show seat availability.","Reconcile quota state (reconcileWorkspaceQuotaState) before the seat check if counts may be stale.","Prompt for plan upgrade when memberCount approaches memberLimit."],"tags":["quota","billing","members","nestjs"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}