legal_entity → tenant, tenant → organizationDate: 2026-04-24 Sub-project: 1 of 2 in the organization/tenant refactor. Status: Design — awaiting review.
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:
org shorthand).tenant.accounting_system enum column is gone (derived from tenant_datev_integration existence). Note: in pre-rename naming, this is legal_entity.accounting_system — post-rename target is tenant, not organization.organization_id columns to drop from audit/stats tables — schema investigation (2026-04-24) confirmed ap_acquisition_llm_stat, ap_datev_export_audit, and ap_qa_dataset_item already carry only legal_entity_id (no old-sense tenant_id). The "scope cleanup" originally planned for these tables reduces to the standard legal_entity_id → tenant_id rename.In scope:
/tenants/... → /organizations/..., /tenants/-/legal-entities/:id/... → /organizations/-/tenants/:id/...) and end-user app routes where applicable.docs/architecture/*.md (renamed in content).tenant.accounting_system enum column (post-rename name; today this is legal_entity.accounting_system). Also drop the accounting_system enum type — inspection confirmed it is referenced only by this one column.datev_export_audit.organization_id, qa_dataset_item.organization_id, acquisition_llm_stat.organization_id) are no-ops: schema inspection on 2026-04-24 confirmed none of those tables carry an organization_id / old-sense tenant_id column today. They each already hold only legal_entity_id, which is renamed to tenant_id in the standard column pass. The acquisition_llm_stat item originally proposed switching from an org-scoped FK with ON DELETE SET NULL to a tenant-scoped FK; since the current column is legal_entity_id with ON DELETE CASCADE, nothing to switch — the standard rename suffices.Out of scope:
resources/migrations/ (historical; append-only).resources/migrations/init.sql (per CLAUDE.md).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:
| 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_id → organization_id, then legal_entity_id → tenant_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).
| 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:
ap_acquisition_llm_statap_datev_export_auditap_doc_sourceap_qa_dataset_itemap_supplier_verificationapi_keyapi_request_logbooking_history_itembooking_history_uploadbusiness_partner_datasetcost_center_datasetdatev_rewe_linkdocumentfpna_data_mapgl_accounts_datasetlegal_entity_datev_integrationlegal_entity_oauth_integrationlegal_entity_prompt_customizationnotification_channelnotification_routingNot 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;
tenant or legal_entity rename to match the new table/column names (e.g. idx_doc_source_legal_entity_id → idx_doc_source_tenant_id).tenant.tenant_pkey1 → organization_pkey (the 1 suffix dates back to the Feb swap; tenant_pkey was held by the pre-existing legal_entity table which had kept its original pkey name).tenant.idx_tenant_slug → idx_organization_slug.tenant.tenant_slug_key1 → organization_slug_key.tenant.tenant_slug_format (check constraint) → organization_slug_format.trigger_tenant_updated_at → trigger_organization_updated_at.legal_entity.tenant_pkey remains tenant_pkey on the new tenant table (no rename needed because the old tenant's pkey has already been renamed to organization_pkey by that point).legal_entity_id / tenant_id keys are redefined via CREATE OR REPLACE FUNCTION with updated bodies. Implementation enumerates every matching function via pg_proc (authoritative); known-present ones from migration history include notify_datev_export_event, notify_document_classified, notify_ingestion_event, plus the matching-event notifier added by 20260302194439-add-matching-event-trigger. There may be more. Any worker consuming those events updates its key parsing in the same deploy.:com.getorcha.schema.tenant/id (old sense) → :com.getorcha.schema.organization/id:com.getorcha.schema.legal-entity/id → :com.getorcha.schema.tenant/id| 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.
| 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.
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* |
One pair: resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.up.sql + .down.sql, each inside a single transaction.
.up.sql ordering:
tenant_id columns → organization_id.tenant table → organization.legal_entity_id columns → tenant_id.legal_entity table → tenant.tenant_membership → organization_membership, legal_entity_datev_integration → tenant_datev_integration, legal_entity_oauth_integration → tenant_oauth_integration, legal_entity_prompt_customization → tenant_prompt_customization).CREATE OR REPLACE FUNCTION for every trigger function with JSON key changes.UPDATE ... SET col = jsonb_set(...) for JSONB columns that embed entity-ID references (audit below).ALTER TABLE tenant DROP COLUMN accounting_system; (post-rename name; this is legal_entity.accounting_system before the rename).DROP TYPE accounting_system; — confirmed unused elsewhere..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.
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:
api_key.permissions — likely has "legal_entity_id": "..." entries.ap_qa_dataset_item.structured_data_snapshot — mirrors document.structured_data; audited for internal key references.document.structured_data — invoice body; unlikely but audited.document.diagnostics, document_processor_run.result — diagnostic payloads; audited.legal_entity_datev_integration.config / .metadata, legal_entity_oauth_integration.config / .metadata — ERP / OAuth connection blobs.ap_datev_export_audit.request_payload — posted DATEV payload.business_partner_dataset.data, gl_accounts_dataset.data, cost_center_dataset.data — master data arrays; unlikely to reference internal IDs but audited.legal_entity.fpna_data_source — FP&A data-source config blob; audit for embedded entity-ID keys.Exact UPDATE statements are derived from the audit, not guessed.
Order of operations on the branch:
tenant → organization everywhere (code + UI + routes + Malli registry keys + integrant keys). Codebase is coherent: legal-entity still means tenant.legal_entity → tenant everywhere.git mv for file/namespace moves (preserves blame).docs/architecture/*.md to new vocabulary in content.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_entitywas renamed totenantand the oldtenantwas renamed toorganization. Read references to these terms with the pre-rename meaning.
Surfaces to audit and rename during implementation:
src/com/getorcha/{schema,db,admin,app,workers,integrations,notifications,link,ai}/**.admin/http/*, admin/ui/*).app/http/settings/*, erp/ui/*).test/com/getorcha/**)..clj-kondo/).resources/com/getorcha/config.edn if any.dev/.Trigger-event consumers: workers reading JSON events emitted by DB triggers update key parsing in the same commit.
Scope-cleanup query rewrites:
legal_entity.accounting_system (post-rename: tenant.accounting_system) readers → check tenant_datev_integration row existence instead (or .is_active field if "active" is the real question). Audit src/com/getorcha/admin/db/queries.clj and any ingestion/export pathway that branches on accounting_system for the full list of callsites.datev_export_audit.organization_id, qa_dataset_item.organization_id, acquisition_llm_stat.organization_id) don't exist and no query rewrites are needed for them.clj -X:test:silent passes.clj-kondo --lint src test dev reports zero problems.(reset) boots cleanly against migrated DB./organizations, create an organization, create a tenant under it, edit both, verify stats display..down.sql dry-run from migrated state back to pre-migration state succeeds..down.sql, revert the code commit, redeploy.legal_entity_id from emitted JSON events and is missed in the code flip, it silently fails after migration. Mitigation: grep for "legal_entity_id" string literals in code, not just identifier references.jsonb/json column, sample values for legal_entity_id substring matches before writing the UPDATE statements.api_request_log before merge.None at spec time. Any discovered during implementation get flagged and resolved in the implementation plan (sub-project 1's next phase).
admin/http/organizations.clj into a grouped/expandable table and adds a per-tenant detail page.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_integration → legal_entity_integration, tenant_prompt_customization → legal_entity_prompt_customization, and business_partner_dataset.tenant_id → legal_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:
ap_acquisition_llm_stat, ap_datev_export_audit, and ap_qa_dataset_item already only carry legal_entity_id (no organization_id or old-sense tenant_id). The standard column rename legal_entity_id → tenant_id covers them.accounting_system column is on legal_entity, not on the old tenant — post-rename target is tenant.accounting_system, not organization.accounting_system.legal_entity_id were missing from the rename list: ap_supplier_verification, booking_history_item, fpna_data_map, notification_routing.notification_workflow / notification_workflow_channel. The actual tables are notification_routing / notification_routing_channel (child table has no FK to rename).notification_channel_teams.tenant_id is a TEXT external Microsoft Teams identifier; it is NOT a reference to our tenant table and is explicitly excluded from the rename.tenant_pkey1, tenant_slug_key1, tenant_slug_format, idx_tenant_slug on the grouping table; tenant_pkey on legal_entity) are normalized as part of this migration.legal_entity.fpna_data_source and corrected table-prefix names (ap_qa_dataset_item, ap_datev_export_audit).