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

Subbase Laravel Package

nafiswatsiq/subbase

Advanced subscription management for Laravel with a Filament admin panel. Build plans with multi-currency pricing and features, manage full subscription lifecycles, add promo codes and discounts, plus optional Spatie roles and custom models. Built on laravelcm/laravel-subscriptions.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and wire it up (5 commands):

composer require nafiswatsiq/subbase
php artisan subbase:install     # publishes config, migrations, views, translations
php artisan migrate

2. Give your billable model the trait — Subbase delegates all subscription logic to laravelcm/laravel-subscriptions, so the trait comes from there:

use Laravelcm\Subscriptions\Traits\HasPlanSubscriptions;

class User extends Authenticatable
{
    use HasPlanSubscriptions;
}

3. Register the Filament plugin in app/Providers/Filament/AdminPanelProvider.php:

->plugin(SubbasePlugin::make())

The service provider itself is auto-discovered; you only touch bootstrap/providers.php if you've disabled discovery.

Where to look first, in this order:

File Why
config/subbase.php Currency defaults, locale→currency map, table names, model bindings, optional permission strings. This is the spine of the package.
vendor/nafiswatsiq/subbase/src/Models/Plan.php getPriceForCurrency, getPriceForLocale, hasCurrencyPrice, setPriceForCurrency — your pricing API.
vendor/nafiswatsiq/subbase/src/Helpers/PlanPriceHelper.php The single place where price + discount resolution is composed.
docs/discount-feature.md + upstream laravelcm/laravel-subscriptions README Subbase only documents its additions; the actual subscribe/cancel/swap/feature-usage API lives upstream. Budget an hour reading the upstream docs — it's not optional.

First use case to prove the install: seed two plans with a USD and an IDR price, create one percentage discount code, then drop <x-subbase::plan-list /> into a Blade view. If you see currency-correct prices, an interval tab switcher, and a promo badge, everything downstream works.


Implementation Patterns

Pricing: always resolve, never hardcode

$plan = Plan::active()->where('featured', true)->first();

$price = $plan->getPriceForLocale(app()->getLocale()); // en-US → USD via config map
$price = $plan->getPriceForCurrency('EUR');            // explicit override
$plan->setPriceForCurrency('EUR', 15.99)->save();

Treat getPriceForLocale() as the display default and getPriceForCurrency() as the checkout truth — the user's chosen currency at checkout should always be explicit and validated with hasCurrencyPrice() before you charge.

The discount flow at checkout

The discount engine is deliberately inert — it validates and calculates, it does not apply anything for you. The pattern that works:

$discount = Discount::findByCode($request->code);

abort_unless($discount && $discount->isValid(), 422, 'Invalid promo code');
abort_unless($discount->appliesToPlan($plan->getKey()), 422, 'Code not valid for this plan');

$result = PlanPriceHelper::resolveWithDiscounts($plan, $currency);
// ['original' => 99.00, 'discounted' => 49.50, 'hasDiscount' => true, ...]

// ...charge $result['discounted'] via your gateway...

DB::transaction(function () use ($discount, $subscription) {
    $discount->markUsed();                       // you call this manually
    $subscription->discounts()->attach($discount->id); // discount_subscription pivot
});

Only call markUsed() after payment confirmation, inside a transaction with the pivot attach. Never at code-entry time.

Subscription lifecycle

Because Subbase wraps upstream, use the upstream fluent API rather than the Filament resources:

$user->newSubscription('main', $plan)->create($paymentMethodId);
$user->subscription('main')->cancel();       // or ->cancelNow()
$user->subscription('main')->swap($newPlan);
$user->subscription('main')->renew();

$user->subscription('main')->canUseFeature('api-calls');
$user->subscription('main')->recordFeatureUsage('api-calls');

Reserve the Filament panel for administration (creating plans, features, discounts, viewing subscribers) and do all customer-facing state transitions in your own controllers/services.

Custom models

Bind them in config and extend the base class:

'models' => [
    'plan'               => App\Models\Plan::class,
    'subscription'       => App\Models\Subscription::class,
    'subscription_usage' => App\Models\SubscriptionUsage::class,
],
class Plan extends \Nafiswatsiq\Subbase\Models\Plan {}

Do this before you write business logic that type-hints the package models — retrofitting is painful because the Filament resources, PlanPriceHelper, and discount relations all resolve through config.

Frontend component

<x-subbase::plan-list subscribe-route="checkout.show" />
Route::get('subscribe/{plan}', CheckoutController::class)->name('checkout.show');

Publish the view (--tag="subbase-views") as soon as a designer touches it; the default component is opinionated Tailwind and its interval tabs, featured-plan emphasis, and discount badge are all in one Blade file.


Gotchas and Tips

This package is early-stage — pin aggressively. Six stars, zero dependents, and a requirements line listing Laravel 13.7+ / Filament 5.0+. Verify those constraints against your actual composer.json before committing; a constraint mismatch here will block install or, worse, resolve to an untested combination. Use "nafiswatsiq/subbase": "1.5.*" rather than ^1.0.

subbase:install is not idempotent-upgrading. It (and vendor:publish) silently skip existing files. On version bumps you must use:

php artisan subbase:upgrade --migrations   # additive only, safe
php artisan subbase:upgrade --config       # selective overwrite
php artisan subbase:upgrade --force        # clobbers config AND views

Diff your config and your published views against the vendor copies before running --force. There is no merge; your customizations are gone.

Config caching bites on model bindings. After editing config/subbase.php (especially the models array) you must php artisan config:clear. A stale cached config with a half-migrated model binding produces confusing "class not found" or, worse, silently writes to the base table instead of your custom one.

Migration collisions with upstream. Subbase publishes migrations atop laravelcm/laravel-subscriptions. Check the published table names in config/subbase.php against any existing subscription tables in your database before migrate on a production-adjacent environment.

Currency fallbacks are silent. getPriceForLocale() returns the base price when no currency row exists. That means a missing EUR price renders as a plausible USD number rather than an error. Guard checkout paths with hasCurrencyPrice() explicitly, and add a test that asserts every active plan has a price for every currency in your getAvailableCurrencies() union.

Discount validation is passive. isValid() checks active flag, date window, and max_uses — but max_uses is only correct if markUsed() is called exactly once. There's no atomic counter or uniqueness constraint visible in the docs; wrap redemption in a transaction and re-fetch the discount row (lockForUpdate()) if you expect concurrent promo usage.

Timezone drift on starts_at / expires_at. These are stored as timestamps; schedule them in the app timezone and verify against config('app.timezone'), or a "starts midnight" campaign will go live at the wrong hour.

Permissions: three-state behavior. With spatie/laravel-permission installed, Filament resource access follows your configured strings. Without it, everything is open. With the permission value left empty but Spatie present, it falls back to Shield-style names (ViewAny:Plan, ViewAny:Subscription). Know which state you're in — "empty string" is not the same as "no permission required."

Best extension points, ranked:

  1. Custom models via config — cleanest, covers most needs.
  2. Publishing and editing plan-list.blade.php — full control over frontend display.
  3. PlanPriceHelper — wrap it in your own service to add tax, proration, or tiered pricing rather than patching vendor code.
  4. Translation files (--tag="subbase-translations") — override plan.php, subscription.php, discount.php per locale.

Debugging toolkit:

config('subbase.models.plan');            // confirm the binding is live
Plan::first()->getAvailableCurrencies();  // what's actually seeded
Discount::findByCode('X')?->isValid();    // why a code is rejected

php artisan tinker against Discount and Plan resolves roughly 80% of "why isn't this showing up" reports faster than reading the Blade component.

Write your own tests. The package ships no test suite you can lean on. Cover: price resolution per currency, discount validity windows, markUsed() call counts, and one end-to-end subscribe→use-feature→cancel path. Given the maturity level, those tests are your upgrade safety net.

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.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table