Rename + Scope Cleanup: legal_entitytenant, tenantorganization

Date: 2026-04-24 Sub-project: 1 of 2 in the organization/tenant refactor. Status: Design — awaiting review.

Context

The February 2026 refactor renamed the original tenant table to legal_entity and introduced a new parent tenant table as a billing/user grouping. The vocabulary has not stuck: in practice the team still says "tenant" when referring to the operational unit. At the same time, investigation confirmed that the Feb squash already moved most per-operational-unit data (business partners, prompt customizations, DATEV integration) down to legal_entity scope — so the remaining work is primarily vocabulary alignment plus a small residue of redundant org-level FKs that can be dropped.

This sub-project covers: (a) the rename itself, and (b) a tail-end scope cleanup dropping redundant org-level columns on audit/stats tables. Sub-project 2 is the admin UI restructure that will follow.

After this sub-project:

Scope

In scope:

Out of scope:

Approach

Single atomic migration + coordinated code flip. One migration file (.up.sql + .down.sql), one deploy, no aliasing or dual-naming period.

PostgreSQL's ALTER TABLE ... RENAME is metadata-only, so the SQL side is cheap. The scale of the work is in the code diff (every namespace touching these entities) and careful migration ordering to avoid name collisions mid-transaction.

Alternatives rejected:

Naming Mapping

Tables

Old name New name
tenant organization
tenant_membership organization_membership
legal_entity tenant
legal_entity_datev_integration tenant_datev_integration
legal_entity_oauth_integration tenant_oauth_integration
legal_entity_prompt_customization tenant_prompt_customization

business_partner_dataset keeps its name; only its FK column is unchanged (already legal_entity_id, renamed below).

Name-collision note: The column swap (tenant_idorganization_id, then legal_entity_idtenant_id) must run in that order across the whole schema, or the second rename would collide with the first set's old name. Table renames have no collision since no tenant_integration / tenant_prompt_customization exists today (they were renamed away in the Feb squash).

Column renames (applied table-wide)

Old column New column Meaning today Meaning after
tenant_id (FK to old tenant) organization_id FK to old tenant FK to organization
legal_entity_id tenant_id FK to legal_entity FK to tenant

Tables currently with the old-sense tenant_id (become organization_id): legal_entity and tenant_membership. That's it — confirmed empirically 2026-04-24. Earlier assumption that audit/stats tables carry a redundant organization_id/old-sense tenant_id was wrong; see Scope.

Tables currently with legal_entity_id (become tenant_id) — confirmed from the live schema 2026-04-24, 20 tables:

Not mentioned in initial drafts: ap_supplier_verification, booking_history_item, fpna_data_map, notification_routing. These were discovered during plan authoring. The child table notification_routing_channel does NOT carry a legal_entity_id (only routing_id FK to its parent) — no rename there.

ap_ingestion does NOT have a legal_entity_id column today (it inherits tenant via its FK to document), so it is not in the rename list despite being mentioned in earlier drafts.

notification_channel_teams.tenant_id is a TEXT field holding the external Microsoft Teams tenant identifier — it is NOT a reference to our tenant/legal_entity tables and MUST NOT be renamed.

Re-verify the list before writing the migration with:

SELECT table_name, column_name FROM information_schema.columns
WHERE column_name IN ('tenant_id','legal_entity_id')
ORDER BY table_name;

Indexes, FK constraints, trigger functions

Malli schema registry keys

Clojure namespaces + file paths

Old New
src/com/getorcha/schema/tenant.clj (old sense) src/com/getorcha/schema/organization.clj
src/com/getorcha/schema/legal_entity.clj src/com/getorcha/schema/tenant.clj
src/com/getorcha/admin/http/tenants.clj src/com/getorcha/admin/http/organizations.clj
src/com/getorcha/admin/http/tenants/file_store.clj (unchanged location) src/com/getorcha/admin/http/tenants/file_store.clj
src/com/getorcha/admin/http/tenants/prompt_customizations.clj (unchanged location) src/com/getorcha/admin/http/tenants/prompt_customizations.clj

The admin/http/tenants/ directory stays put: its contents are already per-legal-entity pages, which after the rename are per-new-tenant pages, so the directory name is semantically correct post-rename. Only the top-level tenants.clj (old-sense organizations listing) moves to organizations.clj. SP2 will later add a new admin/http/tenants.clj (tenant detail page) alongside the existing directory.

Test namespaces mirror these renames. Any other file whose name embeds legal_entity or tenant (old sense) is renamed during implementation.

Routes

Old New
GET/POST /tenants GET/POST /organizations
GET /tenants/-/generate-slug GET /organizations/-/generate-slug
GET/PUT /tenants/:id GET/PUT /organizations/:id
POST /tenants/-/legal-entities POST /organizations/-/tenants
GET/PUT /tenants/-/legal-entities/:id GET/PUT /organizations/-/tenants/:id
/tenants/-/legal-entities/:id/file-store* /organizations/-/tenants/:id/file-store*
/tenants/-/legal-entities/:id/prompts* /organizations/-/tenants/:id/prompts*

End-user /settings/* paths don't contain these words; their labels change, not their paths.

Code-level rename patterns

Applied as coordinated search/replace across src/, test/, dev/, old-sense renames executed before legal-entity renames in the same branch:

Old pattern New pattern
legal-entity (kebab) tenant
legal_entity (snake) tenant
legalEntity (camel, if present in JSON/JS) tenant
:legal-entity-id :tenant-id
le- abbreviation prefix at symbol start or after - (e.g. le-id, le-ids, primary-le-id, all-le-ids, le-filter-ids, le-name, le-param, le-id-set, le-id-str) tenant- (e.g. tenant-id, primary-tenant-id, all-tenant-ids)
tenant-id (old sense) organization-id
tenant_id (old sense) organization_id
:tenant/* namespaced keyword (old sense) :organization/*
"Legal Entity" UI string "Tenant"
"Legal Entities" UI string "Tenants"
"Tenants" (UI string, old sense) "Organizations"
*tenant*, *tenant-id* dynamic vars (old sense) *organization*, *organization-id*

Migration File Structure

One pair: resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.up.sql + .down.sql, each inside a single transaction.

.up.sql ordering:

  1. Rename all tenant_id columns → organization_id.
  2. Rename tenant table → organization.
  3. Rename all legal_entity_id columns → tenant_id.
  4. Rename legal_entity table → tenant.
  5. Rename child tables (tenant_membershiporganization_membership, legal_entity_datev_integrationtenant_datev_integration, legal_entity_oauth_integrationtenant_oauth_integration, legal_entity_prompt_customizationtenant_prompt_customization).
  6. Rename FK constraints and indexes.
  7. CREATE OR REPLACE FUNCTION for every trigger function with JSON key changes.
  8. UPDATE ... SET col = jsonb_set(...) for JSONB columns that embed entity-ID references (audit below).
  9. Scope cleanup (reduced after 2026-04-24 investigation):

.down.sql is the literal reverse of the same steps, applied in opposite order. For accounting_system the reverse recreates the enum (with the literal value list recovered from the Feb 2026 migration) and re-adds the column as NULLABLE — the original per-row values are lost on drop and cannot be recovered. Accept this asymmetry and document it in the migration header.

JSONB audit

Before finalizing the migration, implementation enumerates all jsonb/json columns and samples their contents for references to legal_entity_id, tenant_id (old sense), or internal UUIDs that would still be valid but carry the old terminology as a key.

Expected candidates:

Exact UPDATE statements are derived from the audit, not guessed.

Code-side Changes

Order of operations on the branch:

  1. Rename old-sense tenantorganization everywhere (code + UI + routes + Malli registry keys + integrant keys). Codebase is coherent: legal-entity still means tenant.
  2. Rename legal_entitytenant everywhere.
  3. git mv for file/namespace moves (preserves blame).
  4. Update docs/architecture/*.md to new vocabulary in content.
  5. Add rename notes to affected plans/specs under docs/plans/, docs/superpowers/plans/, docs/superpowers/specs/. Filter: only plans whose meaning would be confusing to read today without the note.

Rename note template (top of affected docs):

Note (2026-04-24): After this document was written, legal_entity was renamed to tenant and the old tenant was renamed to organization. Read references to these terms with the pre-rename meaning.

Surfaces to audit and rename during implementation:

Trigger-event consumers: workers reading JSON events emitted by DB triggers update key parsing in the same commit.

Scope-cleanup query rewrites:

Testing

Rollout

Risks

Open Questions

None at spec time. Any discovered during implementation get flagged and resolved in the implementation plan (sub-project 1's next phase).

Follow-ups (handed to sub-project 2)

Historical note

Initial planning assumed organization_integration, organization_prompt_customization, and business_partner_dataset were still org-scoped and needed to be moved down to tenant-scope. Investigation showed the Feb 2026 squash migration had already moved all three to legal_entity scope (renaming tenant_integrationlegal_entity_integration, tenant_prompt_customizationlegal_entity_prompt_customization, and business_partner_dataset.tenant_idlegal_entity_id). Only the rename + column-drops above remain.

Second round of corrections (2026-04-24 during implementation-plan authoring): Live schema inspection identified further drift between earlier drafts and actual state. The spec was updated accordingly. Summary of changes: