Tenancy, billing and limits decided properly
Pick the tenancy model on purpose
There are two workable models. In the first, all tenants share one database and every tenant-owned table carries a tenant ID. A global scope on the models adds the filter to every query, and a trait fills in the ID on create. It is cheap to run, migrations happen once and cross-tenant reporting is a plain query. The risk is any code path that bypasses the scope, such as raw queries or a job that runs without tenant context. In the second model each tenant has a database of its own. Isolation is stronger, a single customer can be backed up, restored or moved to another region, and large enterprises often ask for it. The price is operational: migrations run once per tenant, connection handling needs care, and reports across customers become a pipeline. We default to the shared database for most products and move to separate databases when contracts or data volumes call for it. Whichever you choose, the suite includes tests that create two tenants and prove neither can read the other.
Billing is a state machine fed by webhooks
Cashier handles the conversation with Stripe or Paddle, but your application still has to decide what each state means. Trialing, active, past due, paused, cancelled with time remaining and fully ended all need a defined level of access. We write that table down with you. Webhooks are the source of truth: handlers verify the signature, are safe to receive twice and update local state before anything else reacts. Failed renewals trigger a sequence of emails and in-app notices before access is reduced.
Entitlements as data
Plans change more often than founders expect. We keep limits and features in configuration or a plans table, and the rest of the code asks one question: can this tenant do this? Seat limits are checked on invitation, usage quotas are counted in a way that survives concurrent requests, and feature flags allow a capability to be opened for one customer during a pilot. Adding a tier later is a data change plus a pricing page update.
Queues that stay fair
Every queued job carries its tenant and restores that context before running. Heavy work such as imports and exports goes to its own queue with its own workers, so one large account cannot starve password reset emails for everybody else. Horizon shows throughput, wait times and failures, and alerts fire when wait time crosses a threshold.
Seeing what is happening
Logs and error reports are tagged with tenant and user. A super-admin console gives support a safe way to look into an account, with impersonation that is logged and visibly marked. From the first paying customer you can answer who is affected by a bug without opening a database client.