veeqtoh/cashier-paystack
Laravel Cashier for Paystack: a fluent billing library that brings subscriptions, customers, payment methods, single charges, invoices, refunds, and webhook handling to your Laravel app with minimal configuration.
Architecture fit
The package follows the canonical Laravel Cashier pattern established by laravel/cashier (Stripe): a service provider registers the package, publishes a config file (config/paystack.php) and migrations, and exposes a Billable trait that is composed into an existing Eloquent model (typically User). This is a well-understood, low-ceremony integration shape for our stack:
users table and to one subscription per "name" per user.users table (adding paystack_id, card_brand, card_last_four, trial_ends_at, etc.) and introduces a subscriptions table. This is a schema-level commitment on our most trafficked table.auth_token-driven charging) leak into application code and must be understood by whoever owns checkout.Integration feasibility
High feasibility on paper. The install path is a four-command sequence (composer require, vendor:publish, migrate, env keys) plus a one-line trait addition, and the README documents the full happy path including subscriptions, trials, single charges, refunds, payment methods, webhooks, and invoice PDFs. For a Laravel team already familiar with Cashier concepts, a spike should be measurable in days, not weeks.
The gating question is not feasibility but supportability: we would be adopting a package with 6 GitHub stars, 0 dependent packages, a single apparent maintainer, and a release history that only began after the predecessor (webong/cashier-paystack) was abandoned. The README is comprehensive; the signals around it are thin.
Technical risk
| Risk | Severity | Notes |
|---|---|---|
| Maintainer/bus factor | High | Solo maintainer, 6 stars, 0 dependents. If it stalls, we inherit a fork. Precedent: this package exists because its predecessor was abandoned. |
| API coverage vs. Cashier baseline | Medium | README covers the Stripe-Cashier surface, but parity is unverified — no evidence of upstream contract tests, Paystack sandbox CI, or coverage metrics. |
| Webhook correctness | High | Billing state is reconciled via Paystack webhooks. Signature verification, replay/idempotency, and CSRF exclusion are the classic failure points, and the README's "Webhooks & CSRF Protection" section is the least concrete part of the doc. |
| Schema coupling | Medium | Adding columns to users and a new subscriptions table is cheap now, expensive later. Migrations on a large users table require planning. |
| Currency/market scope | Low–Medium | Defaults to NGN, supports GHS via Cashier::useCurrency. Fine if our go-to-market is Nigeria/Ghana; a blocker if we need multi-currency, multi-gateway, or non-African acquirers. |
| Money-unit bugs | Medium | Charges are in Kobo (minor units). Mixed major/minor unit handling in app code is a recurring source of 100× billing defects. |
| Version/versioning discipline | Medium | Version badges and a CHANGELOG section exist, but no documented semver policy or upgrade guide content visible. A breaking change in a 0.x-style package is a live possibility. |
| Licensing | Low | MIT — no legal friction, forkable. |
Key questions
verifyTransaction sweep)?users-table mutation, or do we need billing state in a separate schema for compliance/data-residency reasons?Stack fit
Good fit if we are on Laravel 10.x with PHP 8.x and MySQL/Postgres, which matches the declared minimums. The trait-based design means billing status becomes available anywhere the User model is available — convenient for middleware (subscribed('main')), Blade conditionals, and API resources. The package is a poor fit if we run multi-tenant with per-tenant Paystack credentials (config is global/env-driven) or if the billing surface is large enough to warrant a dedicated service.
Recommendation: adopt inside a thin internal billing module (App\Billing) that wraps Cashier calls behind an interface (createSubscription, cancel, onGracePeriod, chargeOnce). This costs ~1–2 days and converts an unhedged vendor bet into a swappable dependency.
Migration path
Three scenarios, in increasing cost:
webong/cashier-paystack. This is the package's stated purpose, but "complete re-write" implies namespace, trait, and possibly table-shape changes. We must produce an explicit column-by-column and method-by-method diff before committing; existing subscriptions rows need a mapping exercise and a rollback plan.laravel/cashier (Stripe). No automated path. Gateway IDs, plan identifiers, and the subscriptions table shape differ. Treat as a re-implementation with historical-data import, not a migration.Compatibility
User model doesn't already define conflicting accessors/mutators for trial_ends_at or card fields; the README explicitly requires a date mutator for trial_ends_at, which suggests Laravel 10 $casts may need hand-holding.bigint vs uuid) compatibility with the published migrations — Cashier-style packages often assume bigint.paystack.php) doesn't collide with an existing Paystack SDK integration (e.g., unicodeveloper/laravel-paystack, which many Nigerian Laravel teams already have installed). Running two Paystack clients is a real and common footgun.~x.y) and commit composer.lock.Sequencing
users table index/lock analysis, backfill strategy if applicable, and a billing-state read model for support tooling.Maintenance
This is the dominant cost. We inherit the maintenance of a package with one maintainer, no dependents, and a lineage of abandonment. Concretely:
Support
No vendor support, no SLA, no commercial backing. Internal first-line support must be our own engineers. Documentation is a strong README but not a support artifact — it is silent on failure recovery, webhook retry handling, error taxonomy, and what Paystack error codes surface as exceptions. We will need to build and own:
Scaling
subscribed('main')) sit on hot paths — index subscriptions(user_id, name) and cache status per request. Adding columns to a large users table requires online-DDL or a shadow-table approach.Failure modes
subscription.create or charge.success leaves a paying customer marked unpaid (or vice versa). Mitigation: idempotent handlers keyed on Paystack event reference, nightly reconciliation sweep, alert on divergence count > 0.x-paystack-signature (HMAC-SHA512 with secret key) in middleware; test it explicitly; scope CSRF exemption to the single route.past_due-style state in our own model.auth_token fails when the card expires or the issuer forces re-authentication. Mitigation: surface a card-update flow (README documents retrieval/deletion of payment methods but not update-and-retry); treat as a product requirement, not a package gap to ignore.App\Billing wrapper so the blast radius is one adapter, not the codebase.Ramp-up
Low for Laravel engineers already fluent in Cashier idioms — the API is deliberately Stripe-Cashier-shaped, so newSubscription(...)->trialDays(10)->create($token) reads as familiar. Expect 2–4 days to productive contribution including sandbox setup and webhook testing.
Higher for the team's collective operational ramp-up: understanding Paystack's object model (plans, plan codes, authorization codes, transaction references), webhook lifecycle, and reconciliation. That is a 1–2 week organizational learning curve and is independent of the package — it would exist with any Paystack integration.
Documentation gaps to close internally on day one: webhook signature verification specifics, error-code mapping, the trial-without-payment-method path (generic trials vs. trialDays), and invoice PDF storage/retention. Notably, several README passages use "Paystack Paystack code" — a copy-paste artifact from the Stripe docs — which signals the docs were adapted rather than written against verified behavior. Treat every code sample as unverified until the spike proves it.
Verdict: Technically sound and well-shaped for a Laravel + Paystack monolith, and cheap to trial. Adoption is justified only behind an internal abstraction layer, with a private fork prepared in advance and reconciliation/observability built before general availability. The 6-star/0-dependent profile is not a reason to reject the package; it is a reason to refuse to depend on it unmargined.
How can I help you explore Laravel packages today?