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

rinvex/laravel-subscriptions

A flexible subscriptions management package for Laravel. Create plans and features, subscribe Eloquent models, track usage, enforce limits, and manage recurring billing with customizable models and a clean API.

View on GitHub
Deep Wiki
Context7

Getting Started

Heads-up before anything else: this package is archived/abandoned (the README itself is marked [ABANDONED]). It still works on modern Laravel for most people, but treat it as vendored code: pin an exact version in composer.json, commit composer.lock, and plan to fork it into your own namespace if you need patches. Do not expect upstream fixes for new Laravel/PHP versions.

Install and scaffold

composer require rinvex/laravel-subscriptions

php artisan vendor:publish --tag=rinvex.subscriptions.config
php artisan vendor:publish --tag=rinvex.subscriptions.migrations
php artisan migrate

The publish tags are the first thing to verify — if vendor:publish produces nothing, the tag names differ in your installed version. Run php artisan vendor:publish interactively and look for the rinvex provider entry.

Wire the trait onto your subscriber model (usually App\Models\User):

use Rinvex\Subscriptions\Traits\HasSubscriptions;

class User extends Authenticatable
{
    use HasSubscriptions;
}

Look here first, in this order:

  1. config/rinvex.subscriptions.php — model class overrides, table names.
  2. The published migrations — plans, plan_features, plan_subscriptions, plan_subscription_usage. These four tables are the domain model; read them before reading the code.
  3. Rinvex\Subscriptions\Models\Plan and PlanSubscription — the public API is small and lives almost entirely there.

First use case — a plan, a subscription, a gate:

use Rinvex\Subscriptions\Models\Plan;

$pro = Plan::create([
    'name'             => 'Pro',
    'description'      => 'Pro tier',
    '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',
]);

// Features are separate rows keyed by slug
$pro->features()->create([
    'name'                 => 'Projects',
    'slug'                 => 'projects',
    'value'                => '5',
    'resettable_period'    => 1,
    'resettable_interval'  => 'month',
]);

$pro->features()->create([
    'name'  => 'Priority support',
    'slug'  => 'priority-support',
    'value' => '1',
]);

$subscription = $user->newSubscription('main', $pro);

// Consume / inspect
$subscription->feature('projects')->canBeUsed();   // bool
$subscription->feature('projects')->consume(2);    // decrements remaining
$subscription->feature('priority-support')->isEnabled();

Implementation Patterns

Seed plans and features, never create them ad hoc. Plans are reference data. Put them in a seeder or a dedicated PlanInstaller service so local/staging/prod agree, and make the seeder idempotent (updateOrCreate on slug) because features hang off plan IDs.

Wrap the whole subscription lifecycle in one service class. The package gives you primitives (newSubscription, changePlan, cancel, renew, consume), not a workflow. Centralize them:

class SubscriptionService
{
    public function start(User $user, Plan $plan): PlanSubscription
    {
        return DB::transaction(function () use ($user, $plan) {
            $user->subscription('main')?->cancelImmediately();

            return $user->newSubscription('main', $plan);
        });
    }
}

Do this because you will always need side effects the package doesn't fire — sending receipts, provisioning, audit logs. There are no meaningful domain events here; your wrapper is the event source.

Feature checks belong in Gates/Policies, not in controllers. The package's checks are cheap and side-effect free, so they map cleanly:

Gate::define('create-project', function (User $user) {
    $sub = $user->subscription('main');

    return $sub && $sub->active() && $sub->feature('projects')->canBeUsed();
});

Then @can('create-project') in Blade and $this->authorize() in controllers. Add a EnsureSubscribed middleware that resolves subscription('main'), rejects null/inactive, and caches the resolved subscription on the request.

Metered usage: separate "can I?" from "do it." canBeUsed() is a read; consume() is the write. Check first, then consume after the resource is actually created (or inside the same DB transaction) so a failed operation doesn't burn quota.

$feature = $user->subscription('main')->feature('projects');

abort_unless($feature->canBeUsed(), 402, 'Project limit reached.');

DB::transaction(function () use ($feature, $request) {
    $project = $request->user()->projects()->create($request->validated());
    $feature->consume(1);
});

Trial and grace handling. New subscriptions get starts_at, ends_at derived from the plan's invoice period, and trial_ends_at from the trial period. Use onTrial() and onGracePeriod() in your UI to switch between "full access", "trial expiring", and "renew now" states rather than hand-rolling date math.

Renewals are your job. There is no scheduler shipped with this package. Write a command that finds expired plan_subscriptions rows and calls renew(), wire it into Kernel::schedule(), and make it idempotent (guard on ends_at). Renewal is where grace periods and dunning live — keep the logic in your service, not the command.

Plan changes and proration. changePlan($plan) re-points the subscription and rebuilds feature usage. It does not prorate money. Compute credit/charge yourself against your billing provider before or after the swap, and decide explicitly whether to pass through the usage reset.

Testing. Carbon::setTestNow() is the only sane way to test trials, grace periods, and resettable features. Freeze time at subscription start, assert onTrial(), jump past trial_ends_at, assert it flips. Use RefreshDatabase and seed a single minimal plan in a test helper.

Extending the models. The config file exposes the model class names. Subclass Plan/PlanSubscription, declare the same relationships, and swap the config — this is the intended extension point and the main reason the package survives its abandonment.

Gotchas and Tips

  • Feature slugs are constrained. They must match a strict pattern (letters, digits, _, -). Spaces, dots, or slashes throw InvalidArgumentException. Pick slugs like api-calls, not API Calls.

  • Values are stored as strings, including booleans. A boolean-ish feature is '1' / '0'. Passing a real true works via the mutator, but reading $feature->value gives you a string — always go through isEnabled(), never compare value === true.

  • isEnabled() is not canBeUsed(). isEnabled() is for boolean/flag features; canBeUsed() is for metered features. Using the wrong one silently gives wrong answers instead of erroring.

  • resettable_period/resettable_interval are what make usage roll over. Leave both null and the quota is lifetime, not per-period. Set them and the usage row carries a valid_until; the reset happens lazily when you next read/consume, not on a schedule — so a user who never touches the feature keeps a stale row indefinitely.

  • cancel() ≠ cancelImmediately(). cancel() sets canceled_at and preserves access until ends_at (this is the "grace period" you show a reactivate button for); cancelImmediately() ends access now. Getting these backwards is the most common support ticket in SaaS.

  • Changing plan wipes usage. Feature usage is rebuilt on changePlan(). If you want to preserve metered counters across an upgrade, snapshot them into your own table first.

  • Deleting a plan breaks its subscriptions. Rows reference plan_id. Prefer an is_active flag on plans over deletion, or at minimum guard with a restrictOnDelete foreign key. Soft deletes on Plan will make subscription->plan return null and produce confusing downstream failures.

  • Currency and price are decorative. This package stores price, currency, and signup_fee but does no arithmetic, no tax, no proration, no invoice generation. Whatever your provider (Stripe et al.) says is authoritative; keep those columns in sync for display only.

  • Config caching bites the model overrides. After editing the models array in config, run php artisan config:clear — stale overrides produce "class not found" or, worse, silently instantiate the base model so your overrides never fire.

  • No events, no observers out of the box. You get no SubscriptionCreated/SubscriptionCancelled hooks. Either fire your own from the service layer or attach model observers to PlanSubscription — the latter is fragile because the package manipulates timestamps internally.

  • Datetime columns are Carbon-cast, but timezone-naive in practice. If your app timezone differs from the DB's, trial/grace boundaries drift by hours. Store and compare in UTC and set app.timezone to UTC.

  • Debugging "why is my feature at zero": query plan_subscription_usage directly by subscription_id + feature_slug. The absence of a row means infinite/never-used; a row with a past valid_until means it will reset on next touch; a row with usage at the limit means you need consume on the plan feature definition (value), not the usage row.

  • Fork path, if you go there: the surface area is genuinely small (four models, two traits). The cheapest long-term move is to copy these into app/Support/Subscriptions, drop the rinvex namespace, and keep the migrations identical so existing data survives.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager