ADR-0021: Table naming convention — module-owned vs cross-cutting registries
Status
AcceptedTags
database, naming, conventions, ddd, modulesDecision
Table names follow one of four patterns, chosen by ownership and scope — not by tenancy:| Pattern | When | Examples |
|---|---|---|
<module>_<entity> | A specific module owns the table | ai_configs, ai_usage, ff_tasks, leave_balances, audit_log |
org_<entity> | Cross-cutting per-org registry — any module can write rows, no single owner | org_feature_flags, org_module_configs |
<entity> (no prefix) | Globally shared, not per-org | users, ai_models, ai_plans, ai_config_defaults |
admin_<entity> | Platform-admin scope | admin_feature_flags, admin_audit_logs, admin_email_templates |
org_ prefix is not a tenancy marker. Most tables are per-org and that fact is implicit in the organization_id column on every row, not encoded in the table name. The prefix is reserved for registries with no single owning module.
Why
The codebase already has ~25 per-org tables without the prefix (ai_configs, ff_tasks, leave_*, customers, orders, …) and only two with it (org_feature_flags, org_module_configs). Without writing the rule down, the next author working on a fieldforce-adjacent feature looked at org_feature_flags and named their AI scaffolding org_ai_configs — a mis-prefix that ended up never being adopted by the live AI module (which uses ai_configs). The dead scaffold then conflicted with the phase-3 design and cost a grilling session to untangle.
Documenting the rule prevents recurrence.
Rejected alternatives:
- Always prefix per-org tables with
org_. Would require renaming ~25 tables, breaking every existing query. Rejected — the convention horse already left the barn. - No prefixes at all (rely on docs). Loses the useful signal that
org_feature_flagsis a generic registry, not module-owned. Rejected. - Tenancy prefix (
t_for tenant tables). Doesn’t match any existing convention; would be a third pattern to learn. Rejected.
How it works
When adding a new table, ask the questions in this order:- Is it per-org? If no →
<entity>(e.g.ai_models) oradmin_<entity>for admin-scope. - If per-org, is there a single owning module? If yes →
<module>_<entity>(e.g.ai_configs). If no — the table is a cross-cutting registry consumed by multiple modules — →org_<entity>(e.g.org_feature_flags).
org_id + key + a value column) and rows for different consumers coexist in the same table. Today: org_feature_flags, org_module_configs. New entries are rare — most module work creates module-owned tables.
Known limitations
- The convention does not encode whether a table is per-org versus shared. Readers infer this from columns (
organization_idpresent or not). audit_logandaudit_trailare per-org but use neither prefix — they predate this convention and are not worth renaming.- “Owning module” is sometimes ambiguous for cross-module entities (e.g.
usersis consumed by every module). When in doubt, prefer no prefix and treat the table as global.
Rules for agents
- New per-org tables owned by one module MUST use the
<module>_<entity>pattern. - New tables MUST NOT use the
org_prefix unless they are generic registries (org_id+key+ JSONB or similar shape). - When extending an existing table, do not rename it to fit this convention.
- When renaming is genuinely necessary (e.g. removing a mis-prefixed scaffold), do it as part of a migration that also drops the old table, never in parallel.