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

Tomato Saas Laravel Package

tomatophp/tomato-saas

Tomato SaaS is a Laravel/TomatoPHP package for building SaaS applications faster, with reusable modules for plans, subscriptions, tenants, and billing. It helps teams add monetization and multi-tenant features without starting from scratch.

View on GitHub
Deep Wiki
Context7

Getting Started

tomato-saas is the subscription/plan layer of the TomatoPHP stack — it gives you plans, per-account subscriptions and feature gating on top of tomato-admin. It is billing + entitlement management, not tenant isolation.

  1. composer require tomatophp/tomato-saas (pulls in the Tomato admin/framework stack — install that first if you don't have it).
  2. Run the installer: php artisan tomato-saas:install. It publishes config + migrations, migrates, and seeds the admin permissions/routes. Then build assets for the admin UI (npm install && npm run build, or publish the Tomato assets).
  3. Read these four things before writing any code:
    • config/tomato-saas.php — account model binding, feature definitions, route/prefix options.
    • src/Services/SaaS.php — the helper facade (SaaS::getSubscription(), SaaS::getPlan(), SaaS::getFeatures(), SaaS::can('feature')).
    • src/Traits/HasSaaS.php — the trait you put on your billable model.
    • database/migrations/* — confirm the actual plans / subscriptions schema (columns and morph keys) rather than assuming.
  4. Add the trait to your billable model (typically Account, Company, or User) and point the config at that class.
  5. Login to /admin → create two or three plans (e.g. free, pro) with feature keys, subscribe your own account, then gate one route with SaaS::can('reports') and confirm both the allowed and denied branches.

First real use case: pick the single most valuable paid feature in your app, put it behind SaaS::can(), and leave everything else open. Ship that before building limit counters.

Implementation Patterns

Feature keys are a public contract. Put every key in one PHP enum or constants class (Feature::REPORTS = 'reports'), seed plans from it, and reference only that class in policies, middleware, Blade, and tests. String keys scattered across the codebase are the #1 maintenance cost of this package.

Route-level gating for booleans, action-level checks for limits. Boolean features map cleanly to a route middleware group; numeric limits (seats, projects, API calls) belong at the write path — check used >= limit inside the create action, not in middleware, or you'll gate reads you didn't mean to gate.

Route::middleware(['auth', 'subscription:reports'])->group(function () {
    Route::get('/reports', ReportController::class);
});

Register the alias in bootstrap/app.php/Kernel and apply it to groups, not individual routes — otherwise new routes silently ship ungated. Make the middleware content-negotiate: redirect to the plans page for HTML, 403 JSON for expectsJson().

Never trust the UI gate. @if(SaaS::can('reports')) in Blade is nav cosmetics; the controller/policy check is the actual enforcement. Write a test that hits the endpoint directly and asserts the denial.

Webhooks are the source of truth for billing. The package stores plan/subscription records; payment-provider wiring (Stripe/Paddle) is yours. Handle the webhook idempotently (event ID table), then update the subscription. Do not activate a plan from the checkout success redirect — users hit it twice, hit it after cancelling, and hit it with a stale session.

Testing. Build a small factory/helper that creates a plan + subscription for an account, and bind a fake into the container when you need deterministic entitlements. Assert both branches of every gate. Avoid $this->withoutMiddleware() on gated routes — you'll lose the only coverage that matters.

Admin screens as ops tooling. Plans/subscriptions CRUD ships via tomato-admin. When you need extra plan fields, publish and extend the migration + add the column to the form, rather than forking the package.

If you have real tenants, run a tenancy package alongside this and set the current account explicitly per request/job. tomato-saas will happily return the authenticated user's account — which is wrong inside a queued job.

Gotchas and Tips

  • The helper is request-scoped. SaaS:: resolves from the authenticated user. In jobs, commands, and scheduled tasks there is no auth: either resolve and pass the account explicitly, or you get null and your gate silently opens (or silently denies) everywhere. This is the single most common production bug with this package.
  • Decide what null subscription means and test it. Free-tier users with no subscription row are a classic hole — either create a real free subscription on registration or handle the fallback explicitly in config. Don't let can()'s default decide for you.
  • Morph columns must match the class you trait-ed. Renaming/moving App\Models\Account leaves account_type pointing at a dead class and getSubscription() quietly returns null. When gating "stops working," check this first.
  • Cache features, invalidate on change. SaaS::can() walks relations; memoize per request at minimum, and Cache::forget() the account's entitlement key when a plan is edited in admin or a webhook lands.
  • Patch releases can still ship migrations. v1.1.4 has no changelog beyond the compare link, so treat it as opaque: after every composer update, diff vendor/tomatophp/tomato-saas/database/migrations against database/migrations and read the GitHub compare diff before deploying. Pin "tomatophp/tomato-saas": "1.1.*" and bump deliberately.
  • Installer is not an upgrader. tomato-saas:install is a first-run command; re-running it on an existing app can overwrite published config/routes. Use vendor:publish --tag=... --force consciously, and keep your customizations in the published copies under version control.
  • Permissions seeding is required in CI. Admin routes are permission-gated; skip the seeder in your test bootstrap and you'll get 403s that look like bugs in your own code.
  • Unstyled admin? It's assets, not the package — check the Tomato asset publishing/npm run build step before debugging PHP.
  • Override, don't edit vendor. Views live under resources/views/vendor/tomato-saas, middleware is swappable via config. That's your extension surface: a decorator bound over the SaaS service for custom resolution logic, a subclassed middleware for custom denial responses, and model events on subscriptions for side effects (provisioning, notifications, seat sync).
  • Upgrade note for v1.1.4: patch-level bump with no stated changes — no migration or API action implied, but nothing is promised either. Verify against the compare diff; if you're on 1.1.3, this is a low-risk bump, not a "review your gating" bump.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource