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

Laravel Subscriptions Laravel Package

laravelcm/laravel-subscriptions

Laravel Subscriptions is a flexible plans and subscription management system for Laravel, with tools to run SaaS services efficiently. It provides migrations, config, and Eloquent models; payments are out of scope.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish

composer require laravelcm/laravel-subscriptions
php artisan vendor:publish --tag="laravel-subscriptions:migrations"
php artisan vendor:publish --tag="laravel-subscriptions:config"   # if present in your version
php artisan migrate

Run php artisan vendor:publish with no args first if a tag name doesn’t resolve — this fork has moved between rinvex-* and laravel-subscriptions:* tag namespaces across versions, and the interactive list is faster than guessing.

2. Attach the trait to your subscriber model

use Laravelcm\Subscriptions\Traits\HasSubscriptions;

class User extends Authenticatable
{
    use HasSubscriptions;
}

That’s the only wiring required. All plan/feature tables arrive from the published migrations, so start by reading those migration files — they are the single best source of truth for column names (invoice_period, invoice_interval, trial_ends_at, ends_at, resettable_period, resettable_interval, etc.) and they will save you an hour of guessing.

3. Seed one plan with one meterable feature

use Laravelcm\Subscriptions\Models\Feature;
use Laravelcm\Subscriptions\Models\Plan;

$plan = Plan::create([
    'name'             => 'Pro',
    'description'      => 'Pro plan',
    'price'            => 29.00,
    'signup_fee'       => 0,
    'currency'         => 'USD',
    'invoice_period'   => 1,
    'invoice_interval' => 'month',
    'trial_period'     => 14,
    'trial_interval'   => 'day',
    'grace_period'     => 3,
    'grace_interval'   => 'day',
]);

$feature = Feature::create([
    'name' => 'Projects',
    'slug' => 'projects',
]);

$plan->features()->attach($feature->id, [
    'value'               => 10,
    'resettable_period'   => 1,
    'resettable_interval' => 'month',
]);

4. Your first real use case: subscribe and gate a feature

$user->subscribeTo($plan, 'main');

if ($user->subscription('main')->canUseFeature('projects')) {
    $user->subscription('main')->recordFeatureUsage('projects');
    // create the project...
}

That loop — subscribe → check → record — is the 80% path. Get it working end-to-end before touching trials, grace periods, or plan changes.

Implementation Patterns

Model your plan catalog as data, not code. Plans and features belong in seeders (PlanSeeder, FeatureSeeder), keyed by a stable slug so re-running the seeder is idempotent. Never hardcode plan IDs in your application; look up by slug. Plans change, prices change, IDs move between environments.

Use named subscriptions deliberately. Every subscriber can hold multiple named subscriptions ('main', 'addons', 'storage'). Always pass the name explicitly rather than relying on the 'main' default — it documents intent and keeps greps honest.

Feature gating middleware keeps controllers clean:

// app/Http/Middleware/EnsureFeatureIsAvailable.php
public function handle($request, Closure $next, string $feature)
{
    $subscription = $request->user()->subscription('main');

    abort_unless($subscription?->canUseFeature($feature), 403, 'Plan limit reached.');

    return $next($request);
}

Then Route::post(...)->middleware('feature:projects').

Metering quotas correctly means check and record in the same transaction — the package does not do this for you:

DB::transaction(function () use ($user) {
    $subscription = $user->subscription('main')->lockForUpdate()->first();
    abort_unless($subscription->canUseFeature('projects'), 403);
    $subscription->recordFeatureUsage('projects');
    // ...persist the resource
});

lockForUpdate() closes the window where two concurrent requests both pass the check.

Keep the billing gateway as the source of truth. The package is a state mirror, not a billing engine. Stripe/Paddle handles money, invoices, dunning, and failed payments; your webhook handler translates those facts into package calls:

// on subscription.created
$user->subscribeTo($plan, 'main', $gatewayStartDate);

// on invoice.paid / subscription renewed
$user->subscription('main')->renew();

// on subscription.cancelled
$user->subscription('main')->cancelNow();

Store the gateway ID in your own column (or a small pivot table) — the package does not know anything about Stripe objects.

Cancellation semantics. cancel() sets ends_at to the end of the current period (access continues until then); cancelNow() ends it immediately. In your UI, surface this distinction — “cancel at period end” vs. “cancel immediately” are very different user experiences and the package models both.

Trials and grace periods are passive state, not enforcement. onTrial() and onGracePeriod() just read timestamps; you decide what to restrict. A common pattern: full access during trial, read-only during grace, hard block after ends_at.

Test with a helper trait. Build one CreatesPlans test helper that seeds a free plan, a metered plan, and a trial plan, then assert against behaviour (canUseFeature returns false at the limit) rather than internal columns. Add RefreshDatabase and you get fast, gateway-free tests.

Gotchas and Tips

Feature usage never resets itself. The resettable_period/resettable_interval columns are metadata, not a scheduler. You must schedule the package’s reset command in routes/console.php / Kernel::schedule(), or implement your own reset. If quotas mysteriously never replenish in production, this is almost always the cause.

value on plan features is a string column. canUseFeature and remainingUsage do numeric comparisons, so '10' vs 10 mostly works — but 'unlimited', '0', and null are a different story. Check how your version treats unlimited features before building pricing pages on it.

Slug generation is automatic and global. Feature::create(['name' => 'Projects']) generates a slug — but if a feature slug must match across all plans (it usually should), guard against duplicates and never rename a slug in production. Renaming silently breaks every canUseFeature('old-slug') call.

Month-end and DST arithmetic. Periods are computed with Carbon add(). A subscription started Jan 31 billed monthly lands on Feb 28 and can drift thereafter. If your billing terms require “same day of month,” compute renewal dates yourself and pass explicit start dates rather than trusting addMonth().

Don’t compute prices on the subscription. Price lives on the plan. If the plan’s price changes, historical subscriptions shouldn’t retroactively change — and they won’t, because invoices live in your gateway. Keep a snapshot table if you need an audit trail.

Plan deletion with live subscribers. Deleting a plan leaves subscriptions pointing at a missing row. Soft-delete plans, or block deletion when active subscriptions exist; the package does not enforce referential integrity beyond the FK (which may be restrict or nullOnDelete depending on your migrations).

changePlan() is not a billing operation. It mutates local state (and in some versions recalculates usage). Do the proration math and charge/credit in your gateway first, then call changePlan(). On a downgrade that pushes usage over the new limit, decide explicitly what happens to recordFeatureUsage counters.

Config quirks worth knowing before you debug:

  • autoload_migrations — if true, the package migrations run from the vendor path and your published copies become dead weight. Pick one and be consistent; conflicting table definitions cause confusing migrations in CI.
  • Table names are configurable. If you’re integrating into an existing schema, set these before the first migrate; renaming later means a manual migration.
  • Some versions let you swap the model classes in config. Use it to add your own scopes/relations rather than forking the package.

Extension points to reach for instead of modifying vendor code: subclass PlanSubscription, Plan, Feature and repoint the config; add your own Feature subclass with a scopeMetered(); wrap subscribeTo in a domain service (SubscriptionService::start()) so you have one place to add logging, events, and gateway calls. Avoid editing vendor/ — this package is actively released and your patches will evaporate.

Debugging checklist when a subscription “isn’t working”: dd($subscription->toArray()) and look at starts_at, ends_at, trial_ends_at, and the canceled_at-equivalent column. Nine times out of ten the state is fine and a stale canUseFeature result is coming from a null subscription (wrong name, or the trait wasn’t on the right model), not from the package.

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.
wandesnet/mercadopago-laravel
tsfcorp/email
larafony/core
laravel-enso/currencies
tartan/laravel-online-payment
gam/laravel-sat-catalogs
cloudcreativity/laravel-stripe
abdulelahragih/querybuilder
wedevbr/celcoin-laravel
surazdott/nepali-date
devsquad-cockpit/laravel
coding-socks/laravel-upload-handler
artisanbarista/laravel-shield
shopwwi/laravel-cache
instance-code/repository
flooris/prestashop-models
lab2view/laravel-generator
lenderspender/laravel-wizard
joecampo/laravel-notification-basecamp
square1/laravel-collection-rolling-average