PHPackages                             crumbls/subscriptions - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. crumbls/subscriptions

ActiveLibrary[Payment Processing](/categories/payments)

crumbls/subscriptions
=====================

A flexible plans and subscription management system for Laravel. Manage your SaaS plans, features, and subscriber usage with ease.

v2.0.0(2mo ago)3208↑100%1[2 PRs](https://github.com/Crumbls/subscriptions/pulls)1MITPHPPHP ^8.3CI passing

Since Feb 21Pushed 2w agoCompare

[ Source](https://github.com/Crumbls/subscriptions)[ Packagist](https://packagist.org/packages/crumbls/subscriptions)[ Docs](https://github.com/Crumbls/subscriptions)[ RSS](/packages/crumbls-subscriptions/feed)WikiDiscussions main Synced today

READMEChangelog (3)Dependencies (28)Versions (7)Used By (1)

Crumbls Subscriptions
=====================

[](#crumbls-subscriptions)

A flexible plans and subscription management system for Laravel. Manage SaaS plans, features, and subscriber usage tracking without coupling to any payment provider.

> **Inspired by** [rinvex/laravel-subscriptions](https://github.com/rinvex/laravel-subscriptions), which is now abandoned. This package modernizes the codebase for Laravel 11/12/13, PHP 8.3+, and current Laravel conventions.

Features
--------

[](#features)

- Plan management with trial, grace, and invoice periods
- Standalone features with many-to-many plan relationships
- Per-plan feature values (e.g. Basic gets 5 users, Pro gets 50)
- Feature-based usage tracking with automatic resets
- Polymorphic subscriptions — attach to any model
- Grace period support — subscriptions stay active during grace window
- Lifecycle events — hook into created, canceled, renewed, plan changed
- Translatable plan/feature names and descriptions (via Spatie)
- Sortable plans and features (via Spatie)
- Configurable table names and swappable models
- Route middleware for feature gating
- Artisan command to prune expired subscriptions

Requirements
------------

[](#requirements)

PackagePHPLaravel`2.x``8.3`, `8.4``11.x`, `12.x`, `13.x`All combinations in the matrix above are exercised in CI on every push and pull request.

Installation
------------

[](#installation)

```
composer require crumbls/subscriptions
```

Publish config and migrations (optional):

```
php artisan vendor:publish --tag=subscriptions-config
php artisan vendor:publish --tag=subscriptions-migrations
```

Run migrations:

```
php artisan migrate
```

Migrations autoload by default. Set `autoload_migrations` to `false` in `config/subscriptions.php` to disable this.

Database Structure
------------------

[](#database-structure)

```
plans                       features
├── id                      ├── id
├── slug                    ├── slug
├── name (json)             ├── name (json)
├── description (json)      ├── description (json)
├── price                   ├── resettable_period
├── signup_fee              ├── resettable_interval
├── currency                ├── sort_order
├── invoice_period          └── timestamps + soft deletes
├── invoice_interval
├── trial_period            plan_features (pivot)
├── trial_interval          ├── id
├── grace_period            ├── plan_id → plans
├── grace_interval          ├── feature_id → features
├── is_active               ├── value
├── active_subscribers_limit├── sort_order
├── sort_order              └── timestamps
└── timestamps + soft deletes

plan_subscriptions          plan_subscription_usage
├── id                      ├── id
├── subscriber_id           ├── subscription_id → plan_subscriptions
├── subscriber_type         ├── feature_id → features
├── plan_id → plans         ├── used
├── slug                    ├── valid_until
├── name (json)             └── timestamps + soft deletes
├── description (json)
├── trial_ends_at
├── starts_at
├── ends_at
├── cancels_at
├── canceled_at
└── timestamps + soft deletes

```

Features are standalone entities. The `value` (limit) is set per-plan on the `plan_features` pivot. This lets a single "Users" feature have different limits across plans.

Usage
-----

[](#usage)

### Add subscriptions to a model

[](#add-subscriptions-to-a-model)

```
use Crumbls\Subscriptions\Traits\HasPlanSubscriptions;

class Tenant extends Model
{
    use HasPlanSubscriptions;
}
```

Works on any Eloquent model — User, Tenant, Team, Organization, etc.

### Create a plan

[](#create-a-plan)

```
use Crumbls\Subscriptions\Models\Plan;

$plan = Plan::create([
    'name' => 'Pro',
    'description' => 'Pro plan',
    'price' => 9.99,
    'signup_fee' => 1.99,
    'invoice_period' => 1,
    'invoice_interval' => 'month',
    'trial_period' => 15,
    'trial_interval' => 'day',
    'grace_period' => 7,
    'grace_interval' => 'day',
    'currency' => 'USD',
]);
```

Intervals accept: `hour`, `day`, `week`, `month`, `year`.

### Create features

[](#create-features)

Features are standalone — create them once, attach to many plans:

```
use Crumbls\Subscriptions\Models\Feature;

$users = Feature::create([
    'name' => 'Users',
    'slug' => 'users',
    'resettable_period' => 0, // never resets — running count
]);

$apiCalls = Feature::create([
    'name' => 'API Requests',
    'slug' => 'api-requests',
    'resettable_period' => 1,
    'resettable_interval' => 'month', // resets monthly
]);

$ssl = Feature::create([
    'name' => 'SSL',
    'slug' => 'ssl',
]);
```

### Attach features to plans with values

[](#attach-features-to-plans-with-values)

The `value` is set per-plan on the pivot — this is how different plans get different limits:

```
// Basic: 5 users, 100 API calls, no SSL
$basic->features()->attach($users, ['value' => '5']);
$basic->features()->attach($apiCalls, ['value' => '100']);

// Pro: 50 users, 10000 API calls, SSL enabled
$pro->features()->attach($users, ['value' => '50']);
$pro->features()->attach($apiCalls, ['value' => '10000']);
$pro->features()->attach($ssl, ['value' => 'true']);

// Enterprise: unlimited users, unlimited API calls, SSL enabled
$enterprise->features()->attach($users, ['value' => '999999']);
$enterprise->features()->attach($apiCalls, ['value' => '999999']);
$enterprise->features()->attach($ssl, ['value' => 'true']);
```

### Factories

[](#factories)

Every model ships with a factory for testing and seeding:

```
use Crumbls\Subscriptions\Models\Plan;
use Crumbls\Subscriptions\Models\Feature;
use Crumbls\Subscriptions\Models\PlanSubscription;

Plan::factory()->create();                        // a generic paid plan
Plan::factory()->free()->create();                // price = 0
Plan::factory()->paid()->withTrial(14)->create(); // 14-day trial
Plan::factory()->withGrace(7)->create();          // 7-day grace window
Plan::factory()->limitedTo(100)->create();        // capped at 100 subscribers
Plan::factory()->inactive()->create();            // is_active = false

Feature::factory()->resettableMonthly()->create();
Feature::factory()->resettableDaily()->create();

PlanSubscription::factory()
    ->for($user, 'subscriber')
    ->ended()
    ->canceled()
    ->create();
```

### Subscribe

[](#subscribe)

```
$tenant->subscribe('main', $plan);

// Or the longer-named original:
$tenant->newPlanSubscription('main', $plan);

// Both take an optional start date:
$tenant->subscribe('main', $plan, now()->addDay());
```

Subscription slugs are unique **per subscriber**, not globally, so both a `User` and a `Tenant` can hold a `main` subscription at the same time.

### Check subscription status

[](#check-subscription-status)

```
$tenant->subscribedTo($plan->id);               // bool
$tenant->hasActiveSubscription();                // any active sub?
$subscription = $tenant->planSubscription('main');
$subscription = $tenant->currentSubscription();  // most recent active

$subscription->active();         // true if not ended, or on trial/grace
$subscription->onTrial();        // currently in trial period
$subscription->onGracePeriod();  // ended but within grace window
$subscription->canceled();       // has been canceled
$subscription->ended();          // period has expired
$subscription->inactive();       // opposite of active
$subscription->pendingCancellation(); // canceled but not yet ended
$subscription->daysUntilEnd();   // int or null
$subscription->daysUntilTrialEnd(); // int or null
```

### Feature usage

[](#feature-usage)

```
$subscription->recordFeatureUsage('api-requests');
$subscription->recordFeatureUsage('api-requests', 5);           // add 5
$subscription->recordFeatureUsage('api-requests', 10, false);   // set to 10
$subscription->reduceFeatureUsage('api-requests', 3);
$subscription->canUseFeature('api-requests');      // bool — checks used < value
$subscription->getFeatureUsage('api-requests');     // int — current usage
$subscription->getFeatureRemainings('api-requests'); // int — remaining quota
$subscription->getFeatureValue('api-requests');     // raw value from pivot
```

If you call `recordFeatureUsage` with a slug that isn't attached to the subscription's plan, it throws `Crumbls\Subscriptions\Exceptions\UnknownFeatureException` — the `featureSlug` and `plan` are exposed as readonly properties on the exception.

#### Enforcing limits (example: user count)

[](#enforcing-limits-example-user-count)

```
// When adding a user to a tenant
$subscription = $tenant->currentSubscription();

if (!$subscription->canUseFeature('users')) {
    throw new \Exception('User limit reached for your plan.');
}

$subscription->recordFeatureUsage('users');

// When removing a user
$subscription->reduceFeatureUsage('users');
```

### Plan lifecycle

[](#plan-lifecycle)

```
$subscription->changePlan($newPlan);    // switch plans (resets usage if billing cycle changes)
$subscription->renew();                 // start a new period
$subscription->cancel();                // cancel at end of current period
$subscription->cancel(immediately: true); // cancel now
$subscription->reactivate();            // undo pending cancellation
```

### Scopes

[](#scopes)

```
use Crumbls\Subscriptions\Models\PlanSubscription;

PlanSubscription::findActive()->get();
PlanSubscription::findEndingPeriod(7)->get();   // ending within 7 days
PlanSubscription::findEndedPeriod()->get();
PlanSubscription::findEndingTrial(3)->get();     // trial ending within 3 days
PlanSubscription::findEndedTrial()->get();
PlanSubscription::ofSubscriber($tenant)->get();
PlanSubscription::byPlanId($plan->id)->get();

Plan::active()->get();
Plan::inactive()->get();
Plan::free()->get();
Plan::paid()->get();
```

### Events

[](#events)

EventFired when`SubscriptionCreated`A new subscription is created`SubscriptionCanceled`A subscription is canceled (includes `$immediate` flag)`SubscriptionRenewed`A subscription is renewed`SubscriptionPlanChanged`A subscription switches plans (includes `$oldPlan` and `$newPlan`)```
use Crumbls\Subscriptions\Events\SubscriptionCreated;

class SendWelcomeEmail
{
    public function handle(SubscriptionCreated $event): void
    {
        $event->subscription->subscriber->notify(/* ... */);
    }
}
```

These events are plain dispatchable events — they do not implement `ShouldBroadcast` out of the box. If you want a broadcast version, extend the event in your application and add `implements ShouldBroadcast` there.

### Middleware

[](#middleware)

Gate routes by feature or subscription:

```
// Must have the "api-requests" feature available
Route::middleware('can-use-feature:api-requests')->group(/* ... */);

// Check a specific subscription by slug
Route::middleware('can-use-feature:api-requests,pro')->group(/* ... */);

// Must have any active subscription
Route::middleware('subscribed')->group(/* ... */);

// Must be subscribed to a specific plan slug
Route::middleware('subscribed:pro')->group(/* ... */);
```

### Pruning expired subscriptions

[](#pruning-expired-subscriptions)

```
php artisan subscriptions:prune              # soft-deletes subs ended more than 30 days ago
php artisan subscriptions:prune --days=90    # custom threshold
php artisan subscriptions:prune --force      # skip confirmation
```

Schedule it:

```
// routes/console.php or bootstrap/app.php
Schedule::command('subscriptions:prune --force')->daily();
```

Configuration
-------------

[](#configuration)

Publish the config to customize table names or swap model classes:

```
// config/subscriptions.php
return [
    'autoload_migrations' => true,

    'tables' => [
        'plans' => 'plans',
        'features' => 'features',
        'plan_features' => 'plan_features',
        'plan_subscriptions' => 'plan_subscriptions',
        'plan_subscription_usage' => 'plan_subscription_usage',
    ],

    'models' => [
        'plan' => \Crumbls\Subscriptions\Models\Plan::class,
        'feature' => \Crumbls\Subscriptions\Models\Feature::class,
        'plan_feature' => \Crumbls\Subscriptions\Models\PlanFeature::class,
        'plan_subscription' => \Crumbls\Subscriptions\Models\PlanSubscription::class,
        'plan_subscription_usage' => \Crumbls\Subscriptions\Models\PlanSubscriptionUsage::class,
    ],
];
```

### Extending models

[](#extending-models)

Every model is resolved through config. Extend the base model and update the config:

```
namespace App\Models;

use Crumbls\Subscriptions\Models\Plan as BasePlan;

class Plan extends BasePlan
{
    public function cancel(bool $immediately = false): static
    {
        // Cancel recurring payment with your provider
        $this->subscriber->paymentProvider()->cancelSubscription($this);

        return parent::cancel($immediately);
    }
}
```

```
// config/subscriptions.php
'models' => [
    'plan' => \App\Models\Plan::class,
],
```

All relationships, scopes, traits, middleware, and the prune command resolve models through config — your custom models are used everywhere automatically.

Models
------

[](#models)

ModelDescription`Plan`A subscription plan with pricing, billing cycle, trial/grace periods`Feature`A standalone feature (e.g. "Users", "API Calls", "SSL")`PlanFeature`Pivot model linking features to plans with a `value` per plan`PlanSubscription`A subscriber's subscription to a plan`PlanSubscriptionUsage`Tracks how much of a feature a subscription has consumedConsiderations
--------------

[](#considerations)

- **Payments are out of scope.** This package handles plan/subscription logic only. Integrate with Stripe, Paddle, etc. separately via events or model overrides.
- **Translatable fields**: `name` and `description` on plans, features, and subscriptions are stored as JSON and support multiple locales via [spatie/laravel-translatable](https://github.com/spatie/laravel-translatable).
- **Soft deletes**: Plans, features, subscriptions, and usage all use soft deletes. The prune command only soft-deletes; use `forceDelete()` if you need permanent removal.
- **Features are standalone**: Create a feature once, attach it to multiple plans with different values. This avoids duplicating feature definitions across plans.

Coming from rinvex/laravel-subscriptions
----------------------------------------

[](#coming-from-rinvexlaravel-subscriptions)

This package started as a modern reboot of `rinvex/laravel-subscriptions`, so the mental model is similar but a few things are worth knowing if you're porting an existing app:

- **Namespace**: `Crumbls\Subscriptions\...` (was `Rinvex\Subscriptions\...`).
- **Config key**: `subscriptions` (was `rinvex.subscriptions`).
- **Subscribing**: prefer `$user->subscribe('main', $plan)` — `newPlanSubscription()` also works. Rinvex's mix of `newSubscription()` / `subscribe()` is unified.
- **Features are standalone.** Rinvex defines features inline on the plan; here a `Feature` is its own row, and the *value* (limit) lives on the `plan_features` pivot. That's how you get "Basic: 5 users, Pro: 50 users" from a single `users` feature.
- **No `rinvex:*` artisan commands.** Use plain `php artisan migrate` and `php artisan vendor:publish`.
- **No model-level validation.** Use Form Requests in your own app.
- **Events.** Four Laravel events (`SubscriptionCreated`, `SubscriptionCanceled`, `SubscriptionRenewed`, `SubscriptionPlanChanged`) in place of rinvex's trait-based hooks.

Upgrading
---------

[](#upgrading)

See [`UPGRADING.md`](./UPGRADING.md) for version-to-version migration steps. Breaking changes are called out in [`CHANGELOG.md`](./CHANGELOG.md).

Contributing
------------

[](#contributing)

See [`CONTRIBUTING.md`](./CONTRIBUTING.md).

License
-------

[](#license)

MIT

###  Health Score

48

—

FairBetter than 93% of packages

Maintenance93

Actively maintained with recent releases

Popularity20

Limited adoption so far

Community19

Small or concentrated contributor base

Maturity54

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 90.9% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~30 days

Total

3

Last Release

71d ago

Major Versions

v1.1.0 → v2.0.02026-04-24

PHP version history (2 changes)1.0.0PHP ^8.2

v1.1.0PHP ^8.3

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/3020753?v=4)[Chase C. Miller](/maintainers/chasecmiller)[@chasecmiller](https://github.com/chasecmiller)

---

Top Contributors

[![Omranic](https://avatars.githubusercontent.com/u/406705?v=4)](https://github.com/Omranic "Omranic (311 commits)")[![chasecmiller](https://avatars.githubusercontent.com/u/3020753?v=4)](https://github.com/chasecmiller "chasecmiller (15 commits)")[![mo7zayed](https://avatars.githubusercontent.com/u/21959069?v=4)](https://github.com/mo7zayed "mo7zayed (8 commits)")[![dependabot-preview[bot]](https://avatars.githubusercontent.com/in/2141?v=4)](https://github.com/dependabot-preview[bot] "dependabot-preview[bot] (1 commits)")[![insign](https://avatars.githubusercontent.com/u/1113045?v=4)](https://github.com/insign "insign (1 commits)")[![jecovier](https://avatars.githubusercontent.com/u/3135068?v=4)](https://github.com/jecovier "jecovier (1 commits)")[![ppalmeida](https://avatars.githubusercontent.com/u/197936?v=4)](https://github.com/ppalmeida "ppalmeida (1 commits)")[![6ichem](https://avatars.githubusercontent.com/u/68327588?v=4)](https://github.com/6ichem "6ichem (1 commits)")[![Rattone](https://avatars.githubusercontent.com/u/7362607?v=4)](https://github.com/Rattone "Rattone (1 commits)")[![amrography](https://avatars.githubusercontent.com/u/9614340?v=4)](https://github.com/amrography "amrography (1 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

laravelfeaturerecurringsubscriptionplansaas

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Type Coverage Yes

### Embed Badge

![Health badge](/badges/crumbls-subscriptions/health.svg)

```
[![Health](https://phpackages.com/badges/crumbls-subscriptions/health.svg)](https://phpackages.com/packages/crumbls-subscriptions)
```

###  Alternatives

[laravelcm/laravel-subscriptions

Laravel Subscriptions is a flexible plans and subscription management system for Laravel, with the required tools to run your SAAS like services efficiently. It's simple architecture, accompanied by powerful underlying to afford solid platform for your business.

24370.0k4](/packages/laravelcm-laravel-subscriptions)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M194](/packages/laravel-ai)[masterix21/laravel-licensing

Laravel licensing package with polymorphic assignment to any model, activation keys, expirations/renewals, and seat control via LicenseUsage. Supports offline verification with public-key–signed tokens, a CLI to generate/rotate/revoke keys, and an extensible architecture via config and contracts.

1563.1k4](/packages/masterix21-laravel-licensing)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
