ridho / justsubs
Subscription Management for Laravel
Requires
- php: ^8.2
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- illuminate/contracts: 11.*
- larastan/larastan: ^3.10
- laravel/pint: ^1.30
- orchestra/testbench: ^9.0
- phpunit/phpunit: ^10.5|^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
JustSubs
JustSubs is a solid, self-hosted subscription and billing infrastructure package for Laravel applications. It abstracts away the complexity of managing plans, polymorphic subscriptions, invoices, and payment processing while providing a built-in, standalone Tailwind CSS dashboard.
Features
- 🏗️ Domain Modeling: Complete abstraction for Plans, Subscriptions, Invoices, and Payments.
- 👥 Polymorphic Subscribers: Bind subscriptions to
User,Team,Company, or any Eloquent model. - 🎨 Built-in Dashboard: A ready-to-use Tailwind CSS control panel. No extra frontend compilation required for your host app.
- 🔌 Agnostic Payments: Extensible Payment Driver system. Start with manual payments and extend it later (e.g. Midtrans, Stripe).
- 📊 Analytics Overview: Automatically calculates active subscriptions, revenue, and normalized Estimated MRR (Monthly Recurring Revenue).
- 🛡️ Secure & Scalable: Race-condition safe with pessimistic locking, highly optimized queries avoiding N+1 memory exhaustion, and CSRF-protected dashboard endpoints.
Requirements
- PHP 8.2+
- Laravel 11.x
- Relational Database (MySQL, PostgreSQL, SQLite)
Installation
You can install the package via composer:
composer require ridho/justsubs
Publish the package configuration, migrations, and dashboard assets:
php artisan justsubs:install
Run the database migrations:
php artisan migrate
Configuration
After running the install command, you can modify the configuration file located at config/justsubs.php.
Here you can change the table names (to prevent collisions with packages like Laravel Cashier) and customize the dashboard route prefix (defaults to /justsubs).
Dashboard Authorization
By default, the dashboard is only accessible in the local environment. To authorize users in production, register an authorization callback within the boot method of your App\Providers\AppServiceProvider.
Because it uses a closure, you have total freedom to define who gets access. Here are a few common examples:
1. Using an array of emails (Horizon style)
use Ridho\JustSubs\JustSubs; public function boot() { JustSubs::auth(function ($request) { return in_array($request->user()?->email, [ 'admin@yourdomain.com', 'ceo@yourdomain.com', ]); }); }
2. Using a Roles package (e.g. Spatie)
JustSubs::auth(function ($request) { return $request->user()?->hasRole('super-admin'); });
3. Using Laravel Gates
JustSubs::auth(function ($request) { return $request->user()?->can('view-justsubs-dashboard'); });
Since JustSubs uses Polymorphic subscribers, you can also define how to display the subscriber's name in the dashboard:
JustSubs::resolveSubscriberNameUsing(function ($subscriber) { return $subscriber->company_name ?? $subscriber->name; });
Adding HasSubscriptions
Add the HasSubscriptions trait to the Eloquent model that will be acting as the subscriber (e.g., your User or Team model).
namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use Ridho\JustSubs\Concerns\HasSubscriptions; class User extends Authenticatable { use HasSubscriptions; }
Creating Plans
Plans can be created easily via the built-in Dashboard or programmatically using Eloquent.
use Ridho\JustSubs\Models\Plan; use Ridho\JustSubs\Enums\IntervalUnit; $plan = Plan::create([ 'name' => 'Pro Monthly', 'slug' => 'pro-monthly', 'description' => 'Full access on a monthly basis.', 'price' => 150000, // Stored in smallest unit (e.g., IDR or Cents) 'currency' => 'IDR', 'interval_count' => 1, 'interval_unit' => IntervalUnit::Month, 'is_active' => true, ]);
Creating Subscriptions
To subscribe a user to a plan, use the SubscriptionManager service. Note that overlapping active subscriptions are blocked by default.
use Ridho\JustSubs\Services\SubscriptionManager; $manager = app(SubscriptionManager::class); // Activates immediately and sets the ends_at date $subscription = $manager->subscribe($user, $plan);
To check subscription status on your subscriber model:
if ($user->subscribed()) { // Has an active subscription } if ($user->subscribedTo('pro-monthly')) { // Specifically subscribed to the 'pro-monthly' plan slug }
Renewing
Subscriptions can be renewed via the dashboard or code. If the subscription is currently active, the renewal preserves the remaining time by adding the interval to the current ends_at. If it has expired, it restarts from now().
$manager->renew($subscription);
You can also manually extend a subscription (e.g. compensating a user for downtime):
use Ridho\JustSubs\Enums\IntervalUnit; // Extends the subscription by 5 days $manager->extend($subscription, 5, IntervalUnit::Day, 'Apology for server downtime');
Cancelling
You can cancel a subscription gracefully (access remains until the period ends) or immediately.
// Graceful cancellation (status stays Active until ends_at) $manager->cancel($subscription, immediately: false); // Immediate termination $manager->cancel($subscription, immediately: true);
Invoices
To issue a bill for a new or existing subscription, use the BillingManager. This creates a collision-safe invoice number (e.g., INV-202608-00001).
use Ridho\JustSubs\Services\BillingManager; $billing = app(BillingManager::class); $invoice = $billing->createInvoice( subscriber: $user, amount: 150000, currency: 'IDR', subscription: $subscription // Link it to activate/renew upon payment );
Manual Payments
If a user pays via bank transfer or outside a gateway, you can mark the invoice as paid. This process automatically activates or renews the linked subscription safely inside a database transaction.
use Ridho\JustSubs\Services\PaymentDrivers\ManualPaymentDriver; $driver = new ManualPaymentDriver(); $billing->processPayment($invoice, $driver, $amount = 150000, 'IDR', [ 'reference' => 'Bank Transfer TXN-12345' ]);
This can also be done visually by clicking "Mark as Paid (Manual)" on the Invoice Dashboard.
Dashboard
Navigate to your application's /justsubs URI to access the dashboard.
It includes:
- Analytics Overview: Active count, Outstanding revenue, MRR, Plan distribution.
- Plans: Full CRUD for plan management.
- Subscriptions: Table viewer, filtering, and manual operation interfaces (Renew, Extend, Cancel, Change Plan).
- Invoices: Billing records and manual payment processing.
- Payments: Immutable ledger of successful transactions.
Events
JustSubs dispatches eloquent events you can listen to in order to send emails or run webhook synchronizations:
Ridho\JustSubs\Events\PaymentReceivedRidho\JustSubs\Events\InvoicePaidRidho\JustSubs\Events\SubscriptionCreatedRidho\JustSubs\Events\SubscriptionActivatedRidho\JustSubs\Events\SubscriptionRenewedRidho\JustSubs\Events\SubscriptionExtendedRidho\JustSubs\Events\SubscriptionPlanChangedRidho\JustSubs\Events\SubscriptionCancelledRidho\JustSubs\Events\SubscriptionExpired
Payment Driver Extension
JustSubs operates on a flexible PaymentDriver contract. You can integrate Midtrans, Stripe, or Xendit by extending the registry without modifying core code.
- Implement
\Ridho\JustSubs\Contracts\PaymentDriver. - Register it in your
AppServiceProvider:
use Ridho\JustSubs\JustSubs; JustSubs::extendPaymentDriver('midtrans', function () { return new \App\Services\MidtransDriver(); });
Testing
composer test
The test suite spans across unit tests and HTTP feature tests, enforcing behavior with SQLite memory databases. All operations check for Race Conditions, Type Casting, and Duplicate Subscriptions.
Security
If you discover any security related issues, please create an issue or contact the author directly. Do not expose vulnerability details openly on public trackers. Built-in securities:
- CSRF Protection: Verified on all state-changing dashboard routes.
- Idempotency Check: Duplicate payments to an already-paid invoice are instantly rejected with
PaymentFailedException. - Pessimistic Locking: Parallel payment callbacks lock the DB row (
lockForUpdate()) to prevent race conditions.
Upgrade Notes
No prior versions exist. The first release candidate is v0.1.0. Wait for the 1.0.0 battle-tested release before massive production adoption.
License
The MIT License (MIT).
Contributing
If you wish to contribute to JustSubs, you can run the following commands to ensure your code meets the quality gate requirements:
composer install # Run tests composer test # Fix code formatting composer format # Run static analysis composer analyse # Run all quality checks (Validate, Format Check, Static Analysis, Tests) composer quality