Happy Pet Tech serves grooming salons, boarding facilities, vet clinics, and pet retailers across five countries from a single Next.js and MongoDB codebase. Most of what makes that workable comes down to a few decisions about tenant isolation, and one rule we treat as non-negotiable.
Multi-tenant systems sit on a spectrum. A database per tenant gives the strongest isolation and becomes painful to operate as tenants multiply. A shared database scoped by tenant ID is cheap to run, and one forgotten filter away from showing one business another business’s data.
We run the shared model. The honest cost of that choice is that isolation becomes a code discipline instead of an infrastructure guarantee, and discipline has to be built into the system, because it won’t survive on good intentions as the team grows.
Every request resolves its tenant up front, and all data access goes through a tenant-scoped layer rather than the raw connection. A developer writing a feature shouldn’t be able to query across tenants by accident, and with the scoped layer in place, they can’t. That removes the whole class of bug instead of catching instances of it in review.
Application code has no “all tenants” query. When we need one, it’s an admin tool, and it lives in a separate surface entirely.
Four business types means four sets of workflows and terminology. A “booking” for a groomer is a slot on a calendar; for a clinic it connects to medical records. Those differences live in per-tenant configuration: which modules are on, what things are called, regional settings for each country. The application branches on config, not on business-type conditionals scattered through the code.
Shared collections grow with every tenant, so query shape matters more over time, not less. Compound indexes lead with the tenant ID, everything is paginated, and no collection is ever loaded without a bound.
Five countries in, the shared model is still the right call for a team our size. Ask me again at ten times the tenant count.