saascode

Multi-tenant organizations with isolation built into the database

A multi-tenant SaaS boilerplate with organization-scoped users, plans, feature controls, lifecycle tools, and database-enforced row isolation.

fabric · aug 21, 2026 · 4 min read

A multi-tenant SaaS boilerplate earns the name only when tenant separation lives below the interface. Hiding another customer's rows in React is not isolation. The organization layer here starts in Postgres: users, subscriptions, feature flags, audit records, and the rest of the tenant-owned model carry an organization boundary that policies can enforce.

The base schema calls organizations what they are: tenants. Each organization has its own identifier, name, slug, logo, and plan relationship. Users point to an organization. Subscriptions point to an organization. Feature flags and audit records do too. That shared key gives the application one consistent answer to “whose data is this?”

The tenant boundary in the database

The central helper resolves the signed-in user's organization from the user profile. Row-level policies then compare a row's organization_id with that result. The baseline policies use that pattern for users, roles, subscriptions, feature flags, and audit logs. A user can read the organization they belong to. Organization-scoped records stay behind the same boundary.

This is not limited to the first migration. A static scan of the current migration set found 54 created application tables and 54 corresponding ENABLE ROW LEVEL SECURITY statements. That number proves coverage, not perfection: it shows that every table created by the reviewed migration set has RLS switched on. The policies still matter, and public or platform-global data can intentionally use a different policy from tenant-owned data.

The base also includes more specific helpers for products that need a deeper tenant model. One helper checks an operator role and tenant together. Another checks a player or client role and tenant together. Those exist for products where the same role name may appear in many organizations and a role check alone would be ambiguous.

What the admin panel can operate

The organization API is not a placeholder list. It can list tenants with pagination, search and plan data, create organizations, read a tenant detail, update it, and delete it. Separate routes expose usage, per-organization limit overrides, and feature overrides.

Lifecycle states are first-class too. The database adds active, suspended, and banned states with a reason, timestamp, and actor. Admin routes can suspend, ban, and reactivate an organization. The more destructive lifecycle operations require the top platform role. Those operations also write audit entries, so a tenant state change is not only a boolean flipped somewhere in the database.

Usage is gathered from the same organization relationships. The detail endpoint joins the assigned plan. The usage route counts members and feature flags for the requested tenant. The feature route reads and toggles that organization's feature rows. The limits route stores overrides against a unique organization ID. This is the practical side of multi-tenancy: the owner can inspect and operate one tenant without pretending every tenant has identical needs.

New organizations also receive their feature-flag catalog through a database trigger. The provisioning function copies global flag definitions into organization-scoped rows and uses conflict-safe inserts, so retrying the operation does not duplicate a tenant's flags.

There is also a distinction between the platform organization and customer organizations. In multi-tenant mode, every tenant is an organization row, while platform-wide settings still need a stable owner. The resolver chooses the organization of the oldest super admin, with the oldest organization as a fresh-install fallback. That keeps platform configuration from accidentally depending on whichever tenant happened to be queried first.

The honest limit

“Organization-scoped” does not mean every table is private to one tenant. Some data is intentionally global. Plans are publicly readable in the baseline schema. Languages and translations were explicitly migrated from organization-scoped rows to one platform-global set. Those tables still have RLS enabled, but their policies express shared platform data rather than tenant isolation.

The distinction is worth keeping straight, because the two are easy to say in one breath and only one of them is true of every table. Row-level security is enabled on every table the migrations create. Row-level tenant isolation is what tenant-owned tables use. Shared platform data has RLS too — its policies just express “everyone reads this” rather than “only this tenant reads this.”

RLS is also not a substitute for server-route authorization. Server code that uses a privileged database client can bypass user-session policies by design. The admin organization routes therefore apply role guards before using that client. Any new privileged route has to keep doing the same. The database boundary protects ordinary user-scoped access; privileged operations need both a route guard and a deliberately scoped query.

Finally, the generic base does not promise a finished tenant-owner role model for every business. It ships the platform organization model and the isolation helpers. Product-specific roles still have to be added consistently to the role configuration and database constraint when a product needs them.

Why every product starts here

Retrofitting multi-tenancy is expensive because the tenant key reaches everything: data ownership, roles, billing, feature access, audit history, and support operations. Adding it later means changing tables and application assumptions at the same time.

Starting with organizations does not force every product to become a complex enterprise suite. A single-tenant deployment can live with one organization row. A team product can use organizations as workspaces. A larger SaaS can expose tenant lifecycle, feature overrides, and limits from the beginning. The model scales in both directions because the boundary is already present.

The useful promise is precise: tenant-owned data has a database-level organization key and a policy pattern to enforce it, while the admin gets real organization operations. Shared platform data stays shared on purpose. That is a stronger foundation than a blanket claim that every row belongs to a tenant.

end