Building a Multi-Tenant SaaS with Laravel and Vue.js: Architecture Choices That Matter

Single database, database-per-tenant or hybrid? Lessons from building multi-tenant, multi-domain Laravel SaaS platforms like TrackReward, with Vue.js front ends and Stripe billing.

Short answerPick your tenancy model on day one: a shared database with a tenant_id is simplest, database-per-tenant gives the strongest isolation. Resolve the tenant early in every request, scope every query, and keep billing, queues and caches tenant-aware.

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 a domains table
  • 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.