I have built several SaaS products on Laravel, including TrackReward, an affiliate tracking platform that runs multi-tenant, multi-domain and multi-database. These are the decisions that shaped those builds.
Decision 1: how tenants share data
There are three common models:
| Model | Pros | Cons |
|---|---|---|
Shared DB, tenant_id column |
Simple, cheap, easy migrations | One missed scope leaks data |
| Database per tenant | Strong isolation, per-tenant backups | More ops work, migrations run N times |
| Hybrid | Central DB for accounts and billing, tenant DBs for business data | Most moving parts |
For B2B products where clients ask about data isolation, I lean toward database per tenant with a central landlord database for users, plans and billing.
Decision 2: identifying the tenant
Every request must know its tenant before any query runs. Common signals:
- Subdomain:
acme.yourapp.com - Custom domain:
rewards.acme.com, mapped in adomainstable - Header or token for API calls
Resolve it in middleware, switch the database connection or set a global scope, and fail closed: no tenant, no data.
Decision 3: scoping everything, not just queries
Tenancy bugs rarely come from the obvious places. Check:
- Queues: jobs must carry the tenant ID and re-initialize tenancy when they run.
- Cache keys: prefix with the tenant, or one client sees another's cached dashboard.
- File storage: separate folders or buckets per tenant.
- Scheduled tasks: loop over tenants explicitly.
- Search indexes: filter by tenant in every search call.
Decision 4: billing
Stripe Billing fits most SaaS products. Keep subscription state in the central database and treat webhooks as the source of truth: invoice.paid, customer.subscription.updated, invoice.payment_failed. Make webhook handlers idempotent; Stripe can deliver the same event more than once.
For marketplaces and some European clients, Adyen is a strong alternative; the same webhook-first approach applies.
Decision 5: the Vue.js front end
I usually build the dashboard as a Vue 3 single-page app with Pinia for state, talking to a Laravel API secured with Sanctum or Passport. The front end stays tenant-agnostic; the API decides what the user may see. Role-based access control lives on the server, and the UI only hides what the API would refuse anyway.
Decision 6: deployment
DigitalOcean droplets or managed Kubernetes work well at early stages. Run queue workers under Supervisor or Horizon, schedule migrate for every tenant database in your deploy script, and back up each tenant database separately.
A checklist before launch
- Automated test that tries to read another tenant's data and must fail.
- Webhook replay test for billing events.
- Per-tenant rate limits on public APIs.
- An admin "impersonate tenant" tool with audit logging.
Planning a SaaS on Laravel? Talk to me before the first migration. The tenancy decision is cheap to make early and expensive to change later.