Tenancy, billing and growing in stages
Three ways to separate tenants
The first decision is where the data of one customer ends and the next begins.
- Shared database, tenant column. Every table carries a tenant ID and every query is scoped by it. Cheapest to run and simplest to deploy. The risk is a forgotten filter, so scoping must be automatic and tested.
- Database per tenant. Strong isolation, easy backup and export for a single customer, and a clean answer for clients with strict data requirements. The cost is running schema changes across many databases.
- Hybrid. Shared by default, with dedicated databases for the few large customers who need them.
Most products should start shared and keep the hybrid option open. We make tenant resolution, by subdomain, custom domain or account switcher, a single component so the storage choice can change later.
Keeping payments and access in step
The payment provider is the source of truth for money, and your application is the source of truth for access. They are kept in step by webhooks, which arrive late, out of order and sometimes twice. We verify signatures, store each event, process it idempotently and reconcile nightly. The customer returning from the checkout page is treated as a hint and nothing more. Failed renewals trigger a grace period and reminder emails before access changes, because locking out a customer over an expired card is an easy way to lose one. Where Stripe is unavailable or a poor fit, the same structure works with Paddle, Razorpay or PayPal.
One service that says what an account may do
Scattering "if plan is pro" checks across the codebase is the mistake we see most. Instead, each plan is a set of entitlements: features switched on, and quotas such as seats, projects or storage. The application asks one service whether an account may do something, and usage is counted as it happens. New plans, grandfathered plans and one-off deals for a large customer then need no deployment.
Which PHP foundation to build on
A SaaS product leans on queues, scheduled tasks, mail and authentication from its first week, which is why most of ours start on Laravel. Other bases are sound too. An API-first product with a JavaScript front end can run lean on Slim or Symfony components. An existing plain PHP application can become a product without a rewrite, by adding tenant scoping at its data layer and a billing module beside it. We choose from what you already have and who will maintain it.
What each growth stage needs
A new product does not need a cluster. Stage one is a single well-configured server running PHP-FPM, the database, a queue worker and backups you have restored at least once. Stage two moves the database and workers onto their own machines and adds Redis for cache and sessions. Stage three puts several application servers behind a load balancer with object storage for files. PHP makes this path straightforward because requests share nothing by default. What must be right from the first commit is the set of assumptions in the code: no files on local disk, no sessions tied to one server, no slow work inside a request.