Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cashier Register Laravel Package

renoki-co/cashier-register

Cashier Register is a Laravel package that extends Laravel Cashier for Stripe with plan and feature management, so you can define subscription tiers, gate features, enforce per-plan limits, and build a SaaS billing flow with minimal code.

View on GitHub
Deep Wiki
Context7

Getting Started

renoki-co/cashier-register sits on top of Laravel Cashier (Stripe) and gives you a first-class model for features (things you meter: API calls, seats, exports) and plans (bundles of features with a quota value per period). Day-to-day, it replaces hand-rolled usage columns on the users table with a queryable, auditable quota ledger.

Minimal setup:

composer require renoki-co/cashier-register
php artisan vendor:publish --provider="RenokiCo\CashierRegister\CashierRegisterServiceProvider"
php artisan migrate

Prerequisites worth confirming before you start: Cashier is installed and configured, your billable model uses Billable, and the package's quota trait (see the README's HasQuotas section) is wired onto the billable model if you want the $user->... sugar in addition to the subscription-level API.

Where to look first, in order:

  1. config/cashier-register.php — table names and model overrides. Decide now whether you'll subclass Feature/Plan, because retrofitting custom models after usage data exists is painful.
  2. The published migrations — read the pivot (plan ↔ feature with value) and the usage table. Understanding that quota lives on the pivot, not on the feature, prevents most day-one confusion.
  3. RenokiCo\CashierRegister\Models\Feature and Plan — the whole domain is these two plus the usage rows.

The first use case to build is almost always a metered endpoint: seed a Pro plan with an api-calls feature at 10,000, attach it to your billable model via Cashier, then guard the endpoint with a quota check and consume on success.

Implementation Patterns

Seed plans and features idempotently. Quotas are catalog data and belong in seeders, not in admin UI code you write twice:

$feature = Feature::updateOrCreate(
    ['slug' => 'api-calls'],
    ['name' => 'API Calls']
);

$plan = Plan::updateOrCreate(
    ['stripe_id' => 'pro'],
    ['name' => 'Pro']
);

$plan->features()->syncWithoutDetaching([
    $feature->id => ['value' => 10000],
]);

syncWithoutDetaching keeps re-running seeders safe; a plain attach will happily create duplicate pivot rows and your quota lookups will start returning whichever row the query hits first.

Guard-then-consume in the request path. Check before doing the expensive work, consume after it succeeds:

$subscription = $user->subscription('default');

abort_unless($subscription->canConsumeQuota('api-calls', 1), 429);

$result = $this->doExpensiveThing();

$subscription->consumeQuota('api-calls', 1);

Consuming before the work means a failed job still burns quota; checking without consuming means free traffic.

Keep Stripe as the source of truth for plan assignment. Use $user->subscription('default')->swap($plan->stripe_id) (or Cashier's own swap) rather than setting the local plan column directly, and reconcile plan changes from customer.subscription.updated webhooks. A plan written only locally will silently disagree with what the customer is actually being billed for.

Decide your overage policy up front. Two valid postures: hard-block (canConsumeQuota gates everything, overage is an error state), or soft-meter (always consume, flag isOverQuota(), bill the excess). The soft-meter posture is where the 6.0.2 changes matter — see below.

Use the two usage accessors for two different questions. getUsedQuota() answers "how much of this period's allowance is gone" (resets with the billing period). getTotalUsedQuota() — new in 6.0.2 — answers "how much has this subscription ever consumed," read from the non-resetting used_total column. Use the first for quota gating and progress bars, the second for lifetime reporting, overage invoicing, and analytics.

Middleware for the common case. Wrap the guard in a middleware keyed by feature slug so controllers stay clean, and attach it per-route-group rather than globally so webhook and health endpoints don't consume quota.

Gotchas and Tips

  • Upgrade to 6.0.2 before trusting used_total. Releases before 6.0.2 double-incremented used_total once a subscription was already over quota (PR #50). If you're on 6.0.0/6.0.1 and bill from used_total, audit or recompute those values after upgrading — the fix stops future drift, it does not repair existing rows.
  • getTotalUsedQuota() does not exist before 6.0.2. Pin "renoki-co/cashier-register": "^6.0.2" in composer.json; a ^6.0 constraint can resolve to a version without the method and you'll get a BadMethodCallException in production only.
  • Quota values live on the pivot. $feature->value does not exist in the way people expect. Always read the quota through the subscription/plan helper, and remember a feature can carry a different value on each plan.
  • Slugs are your contract. Usage rows point at features; renaming a slug after launch breaks historical lookups even though the row survives. Treat slugs as immutable, and add a new feature + migration path instead of repurposing one.
  • Concurrency. canConsumeQuota followed by consumeQuota is a check-then-act race. Under load, wrap consume in a transaction with lockForUpdate on the usage row, or accept a small overage overshoot by design rather than pretending the guard is atomic.
  • Stale relations after consuming. If you've eager-loaded subscription.features or cached the quota for a request, the numbers won't move after a consume. Call ->fresh() or clear the cache entry on the write path.
  • Period boundaries are your problem. Consumption resets are driven by the billing cycle (renewal webhooks) — make sure your timezone and cycle anchor match Stripe, or users will see quotas reset "a few hours early."
  • Custom models must extend the package models (Plan extends RenokiCo\CashierRegister\Models\Plan) and be registered in cashier-register.php; extending the wrong base or forgetting the config entry gives you a model that silently bypasses package behavior.
  • Debugging recipe: in Tinker, pull $user->subscription('default'), then inspect getUsedQuota(), getTotalUsedQuota(), and the raw usage row. If they disagree with your dashboard, the dashboard is usually reading the pivot quota while the code is reading a cached relation.
  • Deleting features is dangerous. Usage rows reference them; prefer deactivating a feature and detaching it from plans over deletion, unless your foreign keys cascade and you're comfortable losing history.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource