Seat enforcement for organization invites
Enforce Stripe seat quantity during org member invitations and acceptance
This page documents how Makerkit enforces seat-based (quantity) subscriptions when users invite and add organization members.
This is based on Better Auth's Stripe plugin behavior: seats is passed to Stripe as the subscription item quantity and can be used in app logic to limit team size.
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.
Related docs
- Customization (seat computation at checkout time)
- Providers (provider differences)