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

TermDescription
Purchased seatssubscription.seats on the active subscription record (Stripe quantity)
Used seatsCurrent organization member count
Reserved seatsPending, 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:

  1. The action is in organization context
  2. The organization has an active or trialing subscription
  3. That subscription has subscription.seats as 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.ts

Service methods

import { createSeatEnforcementService } from '@kit/organization-core/services';
const seatService = createSeatEnforcementService(db);
// Before creating a new invitation
await 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.ts
class 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

ProviderSeat enforcement
StripeSupported (seat quantity maps to Stripe subscription quantity)
PolarNot 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.