How to implement seat-based pricing in Laravel SaaS apps

A practical guide to seat-based pricing in Laravel SaaS apps, including data model design, Stripe quantity sync, billing edge cases, and reliable production workflows.

R
Raşit Apalak
·
·
Updated
·
5 min read

Quick Answer

Quick answer: How to implement seat-based pricing in Laravel SaaS apps

A practical guide to seat-based pricing in Laravel SaaS apps, including data model design, Stripe quantity sync, billing edge cases, and reliable production workflows.

See supporting documentation

How to implement seat-based pricing in Laravel SaaS apps

Seat-based pricing is one of the most common monetization models in B2B SaaS. In Laravel apps, the challenge is not showing "price per seat", it is keeping seat count, subscription quantity, and access rules consistent under real-world events.

This guide shows a production-safe approach using Laravel + Stripe + Cashier patterns.

Quick answer: what is seat-based pricing?

Seat-based pricing means customers pay based on the number of users (or active members) in their workspace/account.

In practice, you need to keep three things aligned:

  1. who counts as a billable seat
  2. current seat quantity in your app
  3. subscription quantity in Stripe

If those drift, billing disputes and access bugs follow.

Pick your seat counting rule first

Before implementation, define one clear rule:

  • all members count as seats
  • only active members count
  • only members above a free seat threshold count

Write this rule in product copy and support docs. Most billing confusion starts when seat logic is implicit.

Data model baseline

A typical Laravel workspace model includes:

  • workspaces (or tenants)
  • workspace_user pivot
  • role/status fields on membership
  • billing plan/subscription reference

Useful derived fields (computed or cached):

  • billable_seat_count
  • included_seat_count
  • chargeable_seat_count = max(0, billable - included)

Keep the source of truth in your app and sync quantity to Stripe, not the other way around.

Stripe product and price setup

For seat-based plans in Stripe:

  • use recurring price with quantity support
  • define monthly/yearly variants
  • decide whether quantity starts at 1 or 0 (with base fee model)

Common models:

  • per_seat: total = seat_count * seat_price
  • base_plus_seat: total = base_fee + (extra_seats * seat_price)

Map these plan keys in config, never hardcode price IDs across controllers.

Core flow: membership change -> quantity sync

When a user is invited, removed, deactivated, or reactivated:

  1. apply membership change in app DB
  2. recompute billable seat count
  3. enqueue quantity sync job
  4. update Stripe subscription quantity
  5. log audit event for billing traceability

Do not update Stripe synchronously in every HTTP request path. Queue it safely and make retries idempotent.

Laravel implementation pattern

Use a dedicated service for seat calculations and sync:

  • SeatCountService computes current billable quantity
  • SeatBillingSyncService updates subscription quantity in Stripe
  • domain events trigger sync jobs (MemberAdded, MemberRemoved, MemberStatusChanged)

This keeps billing behavior deterministic and testable.

Stripe/Cashier quantity update strategy

When updating subscription quantity:

  • fetch current active subscription
  • compare current Stripe quantity vs desired app quantity
  • no-op if unchanged
  • update quantity only when different
  • record sync result in logs/audit table

Guardrails:

  • lock per workspace during sync to avoid race conditions
  • use idempotency keys where appropriate
  • retry transient Stripe failures with backoff

Access policy during billing mismatch

Decide policy for temporary mismatches:

  • strict: block invites if quantity update fails
  • tolerant: allow invite, retry sync in background, alert ops

Most early-stage teams choose tolerant + strong monitoring, then tighten later for enterprise contracts.

Edge cases you must handle

  • multiple invites accepted at the same time
  • member removed and re-added quickly
  • pending invites that should not count as seats
  • owner transfer between users
  • trial to paid transition with changed seat count
  • failed payment states (past_due) with existing members

Each edge case should have explicit behavior documented for support and product teams.

Webhooks and source-of-truth boundaries

Keep boundaries clear:

  • app DB is source of truth for seat membership
  • Stripe is source of truth for charge execution
  • webhook events update subscription status and reconcile discrepancies

If webhook indicates a changed quantity unexpectedly, reconcile through your sync service instead of ad hoc fixes.

Testing checklist for seat-based billing

At minimum, test:

  • invite member -> quantity increments
  • remove member -> quantity decrements
  • bulk member changes -> final quantity correct
  • failed Stripe update -> retry and eventual consistency
  • cancellation/reactivation paths preserve seat logic

Run these tests in CI to avoid silent revenue leakage.

Common mistakes to avoid

  • counting invited-but-not-active users as paid seats unintentionally
  • updating Stripe quantity from multiple code paths
  • no queue/idempotency around quantity sync
  • no audit trail for seat changes vs billing changes
  • unclear customer-facing seat rules on pricing page

Final recommendation

Seat-based pricing works best when the product rule, data model, and Stripe quantity sync are treated as one system.

If you are shipping quickly, start with a simple seat rule, centralize sync logic, and add robust monitoring before adding pricing complexity.

For broader billing setup foundations, read: Laravel Cashier + Stripe setup for SaaS billing

If you want a production-ready baseline that already supports advanced SaaS billing patterns, see:

Share this post:

Ready to ship faster?

Build your SaaS with a production-ready foundation

Launch with authentication, billing, tenancy, and team workflows already in place, then focus on the features that make your product unique.

Related posts