In practice that's a short, recognizable list: currency codes and exchange rates, country and locale tables, tax rates and jurisdictions, subscription plan catalogs, and feature flag definitions. Every one of them is authored once, centrally, and read by every tenant in exactly the same shape. None of it needs a tenant_id column, a separate schema, or its own row in a per-tenant seed script. It needs one copy, in one table, that every tenant reads from.
That list looks obvious in isolation. It stops looking obvious the moment someone on your team asks whether the subscription plan table should be tenant-scoped, because one enterprise customer negotiated a price nobody else gets. Or whether the feature flag table should be tenant-scoped, because enablement is obviously per-tenant even if the flag definition isn't. Get the ownership question wrong at the schema level and you're either shipping N copies of identical data that drift the moment one copy gets patched, or leaking one tenant's private override into a table every other tenant reads from too.
The same distinction matters before a pull request merges. Parallel development only gives useful feedback when each branch can validate tenant-owned state in isolation while continuing to rely on one authoritative set of global reference data. Otherwise, a preview can pass because it inherited a stale catalog or fail because another branch changed a record it should never have shared. That is why we built Autonoma with managed preview environments and end-to-end testing in one product: PreviewKit provisions, routes, and tears down a full-stack preview per pull request with database isolation, environment routing, and secrets propagation.
The short answer: scope by ownership and mutability
The heuristic that resolves almost every one of these arguments is ownership and mutability, not subject matter. Ask two questions about a table: who writes to it, and who reads it. If a single tenant is the only party that ever creates or mutates a row, scope it to that tenant. If the data is authored centrally, by your team, an admin console, or an upstream feed, and every tenant reads the identical rows, it stays global, regardless of how "business specific" the subject matter sounds.
A currencies table reads like plain infrastructure and a subscription plan catalog reads like a billing object with real commercial weight, but both pass the same test: one writer, many identical readers. Ownership, not vibes, is what should decide the schema.
Global reference tables keep one authoritative copy that every tenant reads.
What stays global
Currencies, exchange rates, and tax rates belong in a single global table because a US dollar is worth the same amount regardless of which tenant is looking at it, and nobody wants sixty-two copies of a VAT rate table drifting out of sync after the next rate change. Country and locale tables are the same story: ISO codes and language lists don't change per customer, so scoping them per tenant just multiplies the rows you have to keep synchronized on every update.
Subscription plan catalogs and feature flag definitions are the pair that trips people up, because they sound tied to individual customer relationships. They aren't, not at the definition level. A plan's name, its included features, and its list price are authored once by your product or billing team and read identically by every tenant evaluating that plan. A flag's key, description, and default state work the same way: one row, defined centrally, read everywhere. What varies per tenant is a separate concern, covered below.
What must be tenant-scoped
The contrast is anything a tenant actually creates, owns, or mutates through normal use of your product. Customer accounts, orders, invoices, uploaded files, audit logs, API keys, and webhook configurations are all written by a single tenant's activity and should never be visible to, or writable by, any other tenant. This is the category most teams get right instinctively, because a customer's invoice leaking into another customer's dashboard is an obvious, visible failure. It's also the category that tenant_id columns, separate schemas, or database-per-tenant models exist to protect (we've covered the mechanics of that boundary in more depth in our piece on database branching vs tenant isolation).
The rule of thumb holds even for data that looks like reference data on the surface. A tenant's custom email templates, saved report filters, or notification preferences all get created and edited by that tenant alone, so they belong in tenant-scoped tables even though a naive glance might file them next to "configuration."
The same ownership boundary makes preview data easier to reason about. In PreviewKit, the Planner handles the database state each test scenario needs, while your schema still decides which records are tenant-owned and which reference tables remain global.
How Autonoma validates tenant boundaries before merge
The design rule in this article is necessary, but it is easy to verify only against a convenient shared environment. That is precisely where a branch can inherit another branch's state and hide the missing filter, incorrect override, or shared write that the schema was meant to prevent.
Autonoma's managed preview environments put that check in the pull request workflow. PreviewKit supplies the isolated runtime, and the Diffs Agent maintains the end-to-end test cases as each pull request changes, so the tests exercise the tenant boundary against the code that is about to merge. This does not replace tenant-scoped queries or a global catalog; it makes those choices testable in an environment that is isolated at the same boundary as the change.
The gray areas
The genuinely hard cases are the ones where a table is mostly global but not entirely. Subscription plans are the clearest example: the catalog itself (name, features, default price) stays global, but the moment one enterprise customer negotiates a custom rate, you need somewhere to put that exception. The clean pattern is a global plan catalog plus a thin, tenant-scoped override table keyed by tenant and plan, holding only the rows where a tenant actually deviates from the default. Most tenants have zero rows in that override table. A handful have one or two.
Feature flags follow the same shape from the other direction. The flag registry (its key, description, and rollout defaults) is global and authored centrally. Whether a specific tenant has that flag turned on is a separate, tenant-scoped enablement table joining tenant and flag. Keeping those as two tables instead of one is what lets you add a new flag definition once and roll out enablement gradually, tenant by tenant, without duplicating the definition itself.
Shared-but-editable templates, onboarding checklists, default email copy, and similar content, land in the same pattern once you notice it: a global default that every tenant reads until it edits its own copy, at which point that tenant gets a row in a tenant-scoped table and the global default becomes its fallback rather than its only source.
| Data | Stays global | Tenant-scoped |
|---|---|---|
| Reference data | Currencies, tax rates, locales | n/a, no tenant version needed |
| Plans | Plan catalog, features, price | Custom pricing overrides |
| Feature flags | Flag key, description, defaults | Per-tenant enablement rows |
| Templates | Default template content | Tenant's edited copy |
| Customer activity | n/a, always owned by one tenant | Accounts, orders, invoices, API keys |
A global default plus a thin tenant-scoped override table prevents duplicated catalogs and preserves private exceptions.
Failure modes: what breaks when you get it wrong
Over-scoping, making genuinely global data tenant-scoped, fails quietly at first and expensively later. A currencies table duplicated per tenant means a single decimal-precision fix requires a migration across every copy instead of one row. Run that migration under time pressure and some copies update while others don't, so tenant A shows the corrected value and tenant B still shows the stale one, a bug that only surfaces because a customer happened to compare two accounts. Seed scripts and fixtures balloon the same way: instead of one small reference table, your test data setup carries N identical copies that all have to be generated, loaded, and kept current, for no isolation benefit at all.
Under-scoping runs the opposite direction and fails loudly. Leave genuinely tenant-specific data in a shared table and one tenant's write corrupts another tenant's read. A tenant that flips what should have been its own enablement row, but the table wasn't actually scoped, changes behavior for every other tenant reading that row simultaneously. In a live production system that's a cross-tenant data leak. In a test or preview environment it's the more common version of the same bug: one tenant's test run mutates a table that other tenants' previews were also depending on being stable, and every other environment reading it breaks in ways nobody can reproduce because the corrupting write already rolled back or got overwritten by the time someone looks.
Make the boundary part of pre-merge validation
Modern engineering teams do not make this choice only when they design a table. They make it every time a pull request changes a query, a seed path, or a tenant-specific override. A useful pre-merge check proves that tenant-owned data stays private while shared catalogs stay authoritative, even when other work is progressing in parallel.
PreviewKit makes that implementation practical by giving each pull request an isolated preview environment rather than asking a shared staging database to represent every branch at once. The durable outcome is not more copies of every table. It is a clear ownership boundary, validated against the exact change before it becomes everyone else's production problem.
FAQ
Shared reference data should not be tenant-scoped: currencies and exchange rates, country and locale tables, tax rates and jurisdictions, subscription plan catalogs, and feature flag definitions. These tables are authored centrally, are identical across every tenant, and are read far more often than they're written. Tenant-scoping them multiplies maintenance cost and creates drift with no isolation benefit.
The flag definition itself (its key, description, and default state) should stay global, since it's authored once and identical for every tenant. Whether a specific tenant has that flag enabled is a separate, tenant-scoped enablement table joining tenant and flag. Splitting definition from enablement lets you add a flag once and roll it out gradually without duplicating the definition per tenant.
The plan catalog, name, included features, and default price, should stay global, since every tenant evaluating a plan reads the same row. Custom pricing that a specific tenant negotiated belongs in a separate, thin tenant-scoped override table keyed by tenant and plan, holding only the rows where a tenant deviates from the default. Most tenants have zero rows in that override table.
Scope by ownership and mutability rather than subject matter: if a table is authored centrally and every tenant reads identical rows, it stays global. If a single tenant is the only party that creates or mutates a row, it gets tenant-scoped. Currencies, locale tables, tax rates, plan catalogs, and feature flag definitions pass the global test. Customer accounts, orders, invoices, uploaded files, and API keys do not.




