Seat enforcement for organization invites
Enforce seat limits on team invitations in Next.js Prisma applications
Block organizations from exceeding purchased seats when inviting members. The SeatEnforcementService validates members + pending invites against subscription.seats, throwing SeatLimitReachedError at the limit. Enforcement triggers automatically before invite, resend, and accept operations.
This page is part of the Billing & Subscriptions documentation.
Seat enforcement validates organization member count plus pending invitations against purchased seat quantity (subscription.seats) before permitting invite operations.
Definitions
| Term | Description |
|---|---|
| Purchased seats | subscription.seats on the active subscription record (Stripe quantity) |
| Used seats | Current organization member count |
| Reserved seats | Pending, unexpired invitations |
Seat value interpretation
number: finite seat quantity (enforced)null: unlimited seats (not enforced)undefined: seats not provided / provider doesn't surface it (not enforced)
When enforcement applies
Seat enforcement is only enabled when:
- The action is in organization context
- The organization has an active or trialing subscription
- That subscription has
subscription.seatsas a number
Implementation
Seat enforcement is implemented via SeatEnforcementService in @kit/organization-core/services. This service is called by InvitationsService before each invitation operation.
Service location
packages/organization/core/src/services/seat-enforcement.service.tsService methods
import { createSeatEnforcementService } from '@kit/organization-core/services';const seatService = createSeatEnforcementService(db);// Before creating a new invitationawait seatService.assertCanInviteMember({ organizationId });// Before resending an invitation (handles already-reserved case)await seatService.assertCanResendInvitation({ organizationId, invitationId });// Before accepting an invitation (final gate)await seatService.assertCanAcceptInvitation({ organizationId });Usage in InvitationsService
The InvitationsService calls these methods automatically:
// packages/organization/core/src/services/invitations.service.tsclass InvitationsService { async inviteMember({ organizationId, ... }) { await this.seatEnforcement.assertCanInviteMember({ organizationId }); // ... create invitation via Better Auth } async resendInvitation({ organizationId, invitationId, ... }) { await this.seatEnforcement.assertCanResendInvitation({ organizationId, invitationId }); // ... resend invitation via Better Auth } async acceptInvitation({ invitationId, ... }) { await this.seatEnforcement.assertCanAcceptInvitation({ organizationId }); // ... accept invitation via Better Auth }}Enforcement points
Invite member (create invitation)
Before creating a new invitation:
- Compute
membersCount + pendingInvitesCount - Reject if
membersCount + pendingInvitesCount + 1 > purchasedSeats
Resend invitation
Special handling for resends:
- If invitation is pending + unexpired (already reserved): resending does not consume another seat
- If invitation is expired: resending creates a new pending invite and follows the invite rule (
+1)
Accept invitation
Final gate before adding a member:
- Reject if
membersCount + 1 > purchasedSeats
Error handling
When seat limit is reached, the service throws SeatLimitReachedError:
import { SeatLimitReachedError } from '@kit/organization-core/services';try { await seatService.assertCanInviteMember({ organizationId });} catch (error) { if (error instanceof SeatLimitReachedError) { // error.code === 'SEAT_LIMIT_REACHED' // error.details contains: organizationId, purchasedSeats, membersCount, etc. }}UX guidance
When an invite is blocked:
- Show a clear message: "Seat limit reached. Upgrade seats to invite more members."
- Provide a CTA: "Manage billing" →
/settings/billing
Error code
Server actions surface the stable error code: SEAT_LIMIT_REACHED
Provider support
| Provider | Seat enforcement |
|---|---|
| Stripe | Supported (seat quantity maps to Stripe subscription quantity) |
| Polar | Not implemented (no seats quantity), enforcement does not run |
Requiring a subscription
By default, Makerkit does not block invites when an organization has no subscription (seat enforcement only applies when a subscription with numeric seats exists).
If your product requires billing before teams can invite members, you can add a custom policy to the invitation registries in packages/organization/policies/src/registry.ts.
Common Pitfalls
- Assuming enforcement applies without a subscription: Enforcement only runs when an organization has an active/trialing subscription with numeric seats. No subscription = no enforcement.
- Forgetting expired invitations count differently: Resending an expired invitation counts as a new invite (+1 seat), but resending a pending one doesn't.
- Not handling
SeatLimitReachedErrorin UI: Catch this error and show a clear message with upgrade CTA. Silent failures confuse users. - Expecting Polar to enforce seats: Polar doesn't have quantity billing. Seat enforcement is Stripe-only.
- Unlimited seats confusion: When
seatsisnull, enforcement doesn't run - butundefined(no seats field) also skips enforcement. Be explicit about your intent. - Race conditions with concurrent invites: Two admins inviting simultaneously could exceed limits. The service checks at invite time, not commit time.
Frequently Asked Questions
How do I require billing before any invites?
Why can users still accept invites after seats are full?
How do I show remaining seats in the UI?
Can I customize the error message?
Does removing a member free up a seat?
Related docs:
- Customization (seat computation at checkout time)
- Providers (provider differences)
- Billing Configuration (setting plan limits)