nafiswatsiq/subbase-payment
Subbase Payment is a Laravel package for adding subscription and payment handling to your application, with tools for managing plans, billing, and recurring payments through a simple, configurable integration that keeps subscription logic clean and reusable.
Know what you're getting into first. This is a 6-star, single-maintainer package whose v1.3.0 release note is an autogenerated compare link. That doesn't disqualify it — the API surface described (plans, traits, webhooks, migrations) is small and conventional — but it means the source is the documentation. Budget the first hour for reading src/ end to end before you write a line of app code. For a package in this size class that is realistically 10–20 files.
Install and orient.
composer require nafiswatsiq/subbase-payment
php artisan vendor:publish --provider="Subbase\\Payment\\SubbaseServiceProvider"
Publishing typically yields three things worth inspecting in this order:
config/subbase.php — this is your real API contract. Look for gateway selection, webhook secret/path, default currency, trial defaults, proration toggles, and the model/table class names it expects. Everything else in the package keys off this file.Run php artisan migrate only after step 2, and only on a disposable database first so you can diff the schema against your existing users table.
First use case — pick the dullest one. Don't start with proration or dunning. Start with: one plan, one price, one monthly interval, subscribe an internal/staff account against the gateway sandbox, receive the webhook locally. That single loop exercises the four moving parts (plan definition → subscription attach → gateway call → webhook) and will surface 80% of the integration friction. Use stripe listen/ngrok/whatever your gateway's CLI forwarder is to get webhooks onto localhost.
First place to look when something doesn't work: the gateway's own dashboard, not the package. In every subscription package of this shape, the gateway is the source of truth and the package is a projection of it. If a subscription is "missing" locally, check whether it exists upstream before debugging Eloquent.
Attach the trait narrowly. The Eloquent trait is meant for the billable model (User, Team, Organization). Resist putting it on multiple models — webhook routing and renewal jobs generally resolve the billable via a single configured class, and multi-billable setups are where these packages tend to leak assumptions. If you genuinely need two billable types, keep a thin adapter per type rather than assuming polymorphic support.
Treat the fluent plan API as seed data, not runtime config. Plans and prices should be defined once — in a seeder or a config array — and then referenced by your stable identifier throughout the app. Pattern:
config/plans.php you own).plan_code / price_code strings in your domain (e.g., on an Account or TeamPlan model).This keeps the package's plan representation at the boundary and means a gateway migration touches one file, not fifty.
Own the checkout entry point. Wrap subscription creation in a single application service (e.g., App\Billing\StartSubscription) that takes your domain objects and returns a redirect/payment-intent handle. Every controller, Livewire component, and API endpoint calls that one method. This is the highest-value twenty lines you'll write: it gives you one place to add idempotency keys, one place to log, and one place to swap implementations later.
Webhooks: register, verify, queue, then handle. The package ships signature verification — use it, don't reimplement it, but do check where it sits in the pipeline. Correct shape:
If the package's default handler is synchronous, wrap it: accept, verify, enqueue a job that calls the package's event handler. That's a five-line change that buys you a lot of sleep.
Reconciliation as a standing job. Independent of the package, run a scheduled command that lists active subscriptions from the gateway API and diffs them against your local state (status, period end, cancellation flag). This is your safety net for dropped webhooks, and it is also the fastest way to detect a package upgrade that silently changed behavior. Log and alert on drift rather than auto-correcting at first — you want to know, not to paper over.
Testing against the seam, not the package. Because gateway code is behind an interface, your feature tests should bind a fake implementation of that interface and assert on your subscription state transitions. Keep a separate, small set of sandbox integration tests (tagged, excluded from CI-by-default or run nightly) that exercise the real gateway for: create, upgrade-with-proration, cancel-at-period-end, failed payment → dunning → recovery. Those are the scenarios the package's own tests, if any exist, will not cover for your configuration.
Upgrade policy as a code artifact. Pin the version in composer.json exactly (no ^), and put the upgrade runbook in the repo: diff the commits, classify changes, run the sandbox suite, ship. For a package whose release notes are empty, the diff is the changelog — git log v1.2.1..v1.3.0 --oneline on a clone, or GitHub's compare view. Make that a required step, not a best intention.
Empty release notes are a signal, not a nuisance. For a payments library, an unreviewable minor bump is the highest-severity risk in the whole assessment. Adopt a hard rule: no composer update on this dependency outside a scheduled, diff-reviewed PR. Also lock transitive resolution — a composer.lock you actually commit and respect in deploys.
Proration math is where the bugs live. Every "how much do I owe when I upgrade on the 14th" bug traces back to rounding, currency, or timezone. Two concrete defences: (a) store amounts in integer minor units everywhere in your domain, and (b) trust the gateway's proration preview rather than computing it yourself — fetch the upcoming invoice / preview endpoint and show that to the user. Never show a computed number you can't reconcile against the gateway's own preview.
Webhook idempotency must be verified, not assumed. This is the single most common production defect in packages of this class. Test it explicitly: replay the same event payload twice against your handler and assert no state change and no duplicate charge on the second pass. If the package doesn't dedupe on event ID, add a webhook_events table with a unique index and check it yourself.
Concurrency: the double-subscribe race. Two clicks on "Subscribe," or a retried browser request, can create two gateway subscriptions. Guard at the application layer with a database unique constraint on (billable, plan, active-status) plus an idempotency key passed to the gateway. Do not rely on the package to enforce this — even if it does, the constraint costs nothing and documents the invariant.
Signature verification fails for boring reasons. Classic causes: a proxy or middleware mutating the raw body (verification needs bytes, not the parsed array), wrong secret in the environment between sandbox and production, and clock skew on timestamp-tolerance checks. When it fails, log the raw headers and the unparsed body — but never the secret — and compare against what the gateway dashboard shows it sent.
Check the Laravel/PHP floor before anything else. Minor bumps are a common moment for a framework requirement to move. Verify the constraint for the exact version you're pinning against your app's laravel/framework and PHP version. If you're on an older LTS, this is a gating check that happens before the sandbox spike, not after.
Schema ownership is a decision to write down. If the package's migrations create subscriptions/plans tables, decide now whether that is your canonical billing state or a cache of gateway state. The latter is far easier to exit from. Practically: keep your own billing_accounts table with the fields your product cares about (plan code, seat count, renewal date as you want to display it) and treat the package tables as implementation detail. Then a fork or replacement is a data migration, not an archaeology project.
Extension points to probe, in order of leverage: (1) the gateway interface binding — your test doubles and any future provider swap live here; (2) the plan/price resolver — override it if plan definitions need to come from the database for a product/ops team; (3) the webhook event dispatch — if handlers are dispatched as events, you can listen without touching package code. If none of these are overridable, that's a strong signal to keep the internal adapter thin and consider vendoring.
Trial abuse and cancellation semantics. Trial periods are cheap to enable and easy to get wrong: what happens to a trial when the user cancels mid-trial, when they change plan, or when the payment method is declined at trial end? Define the state machine on paper before coding — trialing → active → past_due → canceled → expired — and assert those transitions in tests. The package gives you toggles; the semantics are yours.
Bus factor mitigation, concretely. One maintainer means: define an abandonment trigger (e.g., six months with no release or an unanswered security/critical bug) that starts a fork-or-exit evaluation, and keep the adapter layer thin enough that the answer is a week, not a quarter. Cheap insurance: vendor the package into your repo (repositories with a path/VCS entry) once it's on the billing critical path, so you can patch under pressure.
Ramp-up is domain ramp, not API ramp. The API is small; the gateway's subscription lifecycle, proration, and dunning are not. Have the engineer doing the integration write a short internal doc — schema ownership, webhook model, idempotency guarantees, upgrade path, exit path — before the production pilot. That document is what actually de-risks the decision, and it also tells the next developer where the bodies are buried.
How can I help you explore Laravel packages today?