legal_entity → tenant (and old tenant → organization) Implementation PlanFor agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Atomically rename the DB tables, columns, FK/index/constraint names, trigger-function payload keys, Malli registry keys, Clojure namespaces, routes, and UI strings such that the operational unit (VAT-holding company) is called tenant and the billing/membership grouping is called organization, with no behavior change. Then drop legal_entity.accounting_system and the accounting_system enum (the only real scope cleanup that still applies — see "Spec discrepancies" below).
Architecture: Single atomic migration pair (.up.sql + .down.sql, both wrapped in a transaction) + a coordinated code flip on the same branch. Stage code changes in two phases to keep the branch compilable at each commit: first old-tenant → organization, then legal_entity → tenant. ALTER TABLE ... RENAME is metadata-only in Postgres, so migration cost is in ordering (avoid name collisions) and the code diff.
Tech Stack: PostgreSQL (ALTER TABLE/INDEX/CONSTRAINT RENAME, jsonb_set, CREATE OR REPLACE FUNCTION), Clojure/HoneySQL, next.jdbc, Reitit, Malli, Integrant, Hiccup.
Investigation of the live schema (psql against local dev DB) revealed that several claims in the spec do not reflect the current DB. These are locked-in decisions for this plan — if any turn out to be wrong, resolve inline rather than blocking.
ap_acquisition_llm_stat, ap_datev_export_audit, and ap_qa_dataset_item each carry only legal_entity_id today — NO redundant organization_id / old-sense tenant_id column exists on any of them. They will be renamed (legal_entity_id → tenant_id) with no extra column drops. The spec's "replace acquisition_llm_stat.organization_id with tenant_id FK" item reduces to a plain rename.accounting_system column is on legal_entity, not on old tenant. Post-rename naming: drop is tenant.accounting_system, NOT organization.accounting_system. The enum type accounting_system is referenced only by this one column, so DROP TYPE accounting_system is safe after the column drop.legal_entity_id not listed in the spec: fpna_data_map, booking_history_item, ap_supplier_verification, notification_routing. All get the same treatment: legal_entity_id → tenant_id.notification_workflow / notification_workflow_channel; actual tables are notification_routing / notification_routing_channel. No workflow-named tables exist. The child table notification_routing_channel does NOT carry legal_entity_id (only parent routing_id FK).notification_channel_teams.tenant_id is a TEXT column (Microsoft Teams tenant ID — external identifier). DO NOT rename it. It is unrelated to our tenant table and to this refactor.tenant table's pkey is tenant_pkey1 and its slug index/check are tenant_slug_key1 / tenant_slug_format / idx_tenant_slug; legal_entity's pkey is literally named tenant_pkey. The migration must rename these to the target names (organization_pkey, tenant_pkey on the new tenant) in the correct order to avoid collisions.legal_entity.fpna_data_source (jsonb) is not listed in the spec's JSONB audit candidates. Add it to the audit.resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.up.sqlresources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.down.sqlUse the actual timestamp at commit time (UTC YYYYMMDDHHMMSS). These files hold the complete DB change: table renames, column renames, constraint/index renames, trigger-function bodies, JSONB UPDATE statements (derived from the audit), and the accounting_system drop. Both are wrapped in a single transaction (BEGIN; ... COMMIT; — the migration runner does NOT wrap automatically; see existing migration files in the directory for the convention).
| Old path | New path |
|---|---|
src/com/getorcha/schema/tenant.clj (old sense, grouping) |
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 |
src/com/getorcha/admin/http/organizations/file_store.clj |
src/com/getorcha/admin/http/tenants/prompt_customizations.clj |
src/com/getorcha/admin/http/organizations/prompt_customizations.clj |
| Test namespaces mirroring all of the above | same structure under test/ |
Use git mv so blame is preserved. Any other file whose name embeds legal_entity or old-sense tenant is discovered and renamed during implementation (see enumeration step in Task 6).
docs/architecture/*.md — vocabulary flip in-content.docs/plans/, docs/superpowers/plans/, docs/superpowers/specs/ — add a rename note at the top ONLY if the old terminology would confuse a reader today. Do not mass-rename historical text.Frequent commits. One logical unit per commit. Suggested messages:
chore(db): add legal_entity → tenant rename migrationrefactor(schema): rename old-sense tenant to organizationrefactor(schema): rename legal_entity to tenantrefactor(admin): move tenants.clj → organizations.cljrefactor(routes): rename /tenants to /organizations; /legal-entities to /tenantsrefactor(notifications): update trigger-event consumer keysdocs: add rename note to affected specs/plansFollow existing commit style (type(scope): subject) observed in git log --oneline -20.
Purpose: Freeze the ground-truth list of columns, constraints, and indexes so every later task can be checked against the same reference.
Files:
Create: dump/rename-audit.md (scratch — not committed; dump/ is already gitignored).
Step 1: Enumerate all columns named tenant_id or legal_entity_id.
Run:
psql -h localhost -U postgres -d orcha -c "SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public' AND column_name IN ('tenant_id','legal_entity_id') ORDER BY table_name;"
Expected (as of 2026-04-24 dev DB, use this as reference — re-run to confirm on your DB):
ap_acquisition_llm_stat | legal_entity_id
ap_datev_export_audit | legal_entity_id
ap_doc_source | legal_entity_id
ap_qa_dataset_item | legal_entity_id
ap_supplier_verification | legal_entity_id
api_key | legal_entity_id
api_request_log | legal_entity_id
booking_history_item | legal_entity_id
booking_history_upload | legal_entity_id
business_partner_dataset | legal_entity_id
cost_center_dataset | legal_entity_id
datev_rewe_link | legal_entity_id
document | legal_entity_id
fpna_data_map | legal_entity_id
gl_accounts_dataset | legal_entity_id
legal_entity | tenant_id ← old-sense tenant_id (→ organization_id)
legal_entity_datev_integration | legal_entity_id
legal_entity_oauth_integration | legal_entity_id
legal_entity_prompt_customization | legal_entity_id
notification_channel | legal_entity_id
notification_channel_teams | tenant_id ← EXTERNAL Teams tenant ID (TEXT), DO NOT RENAME
notification_routing | legal_entity_id
tenant_membership | tenant_id ← old-sense (→ organization_id)
Write this output verbatim to dump/rename-audit.md under section "Columns to rename".
tenant or legal_entity.Run:
psql -h localhost -U postgres -d orcha -c "SELECT conname, conrelid::regclass AS table_name, pg_get_constraintdef(oid) AS def FROM pg_constraint WHERE contype='f' AND (conname LIKE '%tenant%' OR conname LIKE '%legal_entity%') ORDER BY table_name, conname;"
Write output to dump/rename-audit.md under "FK constraints".
tenant or legal_entity in its name.Run:
psql -h localhost -U postgres -d orcha -c "SELECT schemaname, tablename, indexname FROM pg_indexes WHERE schemaname='public' AND (indexname LIKE '%tenant%' OR indexname LIKE '%legal_entity%') ORDER BY tablename, indexname;"
Write output to dump/rename-audit.md under "Indexes".
Run:
psql -h localhost -U postgres -d orcha -c "SELECT proname, pg_get_functiondef(p.oid) AS def FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname='public' AND (prosrc LIKE '%legal_entity%' OR prosrc LIKE '%tenant_id%') ORDER BY proname;"
The pg_proc enumeration is authoritative — include every function the query returns, do NOT rely on a hand-maintained list. Known-present functions (migrations confirmed to have added them): notify_datev_export_event (from 20260219091057-squashed-to-v1-963), notify_document_classified (from 20260310200000-add-document-classified-trigger), notify_ingestion_event (from the squash), and a matching-event notifier from 20260302194439-add-matching-event-trigger. There may be more. Copy the full function bodies into dump/rename-audit.md under "Trigger functions" — these are the source material for the CREATE OR REPLACE FUNCTION blocks in the migration.
Run:
psql -h localhost -U postgres -d orcha -c "SELECT table_name, column_name FROM information_schema.columns WHERE data_type IN ('jsonb','json') AND table_schema='public' ORDER BY table_name, column_name;"
Write to dump/rename-audit.md under "JSONB columns (audit targets)".
For each column {table}.{col} from Step 5, run:
psql -h localhost -U postgres -d orcha -c "SELECT COUNT(*) AS rows_with_hit FROM {table} WHERE {col}::text ~ 'legal_entity_id|tenant_id';"
For any column with rows_with_hit > 0, run a follow-up sampling:
psql -h localhost -U postgres -d orcha -c "SELECT {col} FROM {table} WHERE {col}::text ~ 'legal_entity_id|tenant_id' LIMIT 3;"
Record findings in dump/rename-audit.md under "JSONB audit findings". For each hit, note the JSONB path (e.g. $.permissions[*].legal_entity_id) — this is the source material for the jsonb_set / UPDATE statements in the migration.
Expected high-probability hits (confirm or falsify):
api_key.permissions — likely contains "legal_entity_id": "..." keys.
ap_qa_dataset_item.structured_data_snapshot — mirror of document body; audit.
document.structured_data — unlikely to hold internal IDs as keys; audit anyway.
document.diagnostics, document_processor_run.result — diagnostic payloads; audit.
legal_entity_datev_integration.config / .metadata — ERP connection blobs.
legal_entity_oauth_integration.config / .metadata — OAuth connection blobs.
ap_datev_export_audit.request_payload — posted DATEV payload.
legal_entity.fpna_data_source — newly added, not in the spec's audit list.
Step 7: Commit-less checkpoint. dump/ is gitignored; do NOT commit rename-audit.md. It is a scratch reference for the remaining tasks.
.up.sql migrationFiles:
resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.up.sqlUse the current UTC timestamp for YYYYMMDDHHMMSS. Look at the most recent migration file for reference:
ls -1 resources/migrations/ | sort | tail -5
-- Rename: legal_entity → tenant, old tenant → organization.
-- Also drops legal_entity.accounting_system (and the accounting_system enum type).
--
-- See docs/superpowers/specs/2026-04-24-rename-legal-entity-to-tenant-design.md
-- See docs/superpowers/plans/2026-04-24-rename-legal-entity-to-tenant.md
--
-- Ordering matters. The column rename passes (tenant_id → organization_id, then
-- legal_entity_id → tenant_id) MUST run in that order to avoid collisions.
-- Likewise, the old `tenant` table must be renamed to `organization` BEFORE
-- `legal_entity` is renamed to `tenant`.
--
-- The `.down.sql` path is asymmetric on the `accounting_system` column: values
-- are lost when the column is dropped here, and `.down.sql` recreates the
-- column NULLABLE without them. We accept this.
BEGIN;
-- (statements go here)
COMMIT;
tenant_id columns to organization_id.Append:
-- Phase 1: rename old-sense `tenant_id` columns to `organization_id`.
-- Only two tables: legal_entity, tenant_membership.
-- (notification_channel_teams.tenant_id is an EXTERNAL Microsoft Teams tenant
-- id — TEXT — and is NOT renamed.)
ALTER TABLE legal_entity RENAME COLUMN tenant_id TO organization_id;
ALTER TABLE tenant_membership RENAME COLUMN tenant_id TO organization_id;
tenant table to organization and normalize its pkey/index/constraint/trigger names.Append:
-- Phase 2: rename old `tenant` table to `organization`.
-- Its pkey is currently `tenant_pkey1` and slug objects carry `tenant_*_key1`
-- / `idx_tenant_slug` / `tenant_slug_format` from the Feb 2026 rename. Normalize
-- them now so `legal_entity_pkey` can become `tenant_pkey` later.
ALTER TABLE tenant RENAME CONSTRAINT tenant_pkey1 TO organization_pkey;
ALTER INDEX idx_tenant_slug RENAME TO idx_organization_slug;
ALTER TABLE tenant RENAME CONSTRAINT tenant_slug_key1 TO organization_slug_key;
ALTER TABLE tenant RENAME CONSTRAINT tenant_slug_format TO organization_slug_format;
ALTER TRIGGER trigger_tenant_updated_at ON tenant RENAME TO trigger_organization_updated_at;
ALTER TABLE tenant RENAME TO organization;
-- tenant_membership → organization_membership (child of organization).
ALTER TABLE tenant_membership RENAME TO organization_membership;
ALTER TABLE organization_membership RENAME CONSTRAINT tenant_membership_pkey TO organization_membership_pkey;
ALTER INDEX idx_tenant_membership_identity_id RENAME TO idx_organization_membership_identity_id;
ALTER INDEX idx_tenant_membership_tenant_id RENAME TO idx_organization_membership_organization_id;
ALTER TABLE organization_membership RENAME CONSTRAINT tenant_membership_unique TO organization_membership_unique;
ALTER TABLE organization_membership RENAME CONSTRAINT tenant_membership_identity_id_fkey TO organization_membership_identity_id_fkey;
ALTER TABLE organization_membership RENAME CONSTRAINT tenant_membership_tenant_id_fkey TO organization_membership_organization_id_fkey;
-- The FK constraint on legal_entity pointing at old `tenant` gets renamed here
-- while legal_entity still has its old name — it'll be renamed again below
-- when legal_entity itself becomes `tenant`.
ALTER TABLE legal_entity RENAME CONSTRAINT legal_entity_tenant_fk TO legal_entity_organization_fk;
Confirm the current pkey/constraint/index names against your dump/rename-audit.md FK + index enumerations (Task 1 Steps 2 & 3) and ADJUST this block if names differ on your DB. The spec's ground truth is the live schema.
legal_entity_id columns to tenant_id across the schema.Append:
-- Phase 3: rename all `legal_entity_id` columns to `tenant_id`.
-- Includes all 20 tables from dump/rename-audit.md.
ALTER TABLE ap_acquisition_llm_stat RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE ap_datev_export_audit RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE ap_doc_source RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE ap_qa_dataset_item RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE ap_supplier_verification RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE api_key RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE api_request_log RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE booking_history_item RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE booking_history_upload RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE business_partner_dataset RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE cost_center_dataset RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE datev_rewe_link RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE document RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE fpna_data_map RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE gl_accounts_dataset RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE legal_entity_datev_integration RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE legal_entity_oauth_integration RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE legal_entity_prompt_customization RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE notification_channel RENAME COLUMN legal_entity_id TO tenant_id;
ALTER TABLE notification_routing RENAME COLUMN legal_entity_id TO tenant_id;
Re-check this list against dump/rename-audit.md Step 1 output. If the live DB shows additional tables, add them.
legal_entity table and its children to tenant family.Append:
-- Phase 4: rename `legal_entity` → `tenant`. Its pkey is already named
-- `tenant_pkey` (a historical artifact from when it was `tenant` originally),
-- which is fine because the OLD tenant's pkey has already been renamed to
-- `organization_pkey` in Phase 2.
ALTER TABLE legal_entity RENAME TO tenant;
-- pkey `tenant_pkey` retained (no rename needed).
-- Child tables: rename the tables themselves.
ALTER TABLE legal_entity_datev_integration RENAME TO tenant_datev_integration;
ALTER TABLE legal_entity_oauth_integration RENAME TO tenant_oauth_integration;
ALTER TABLE legal_entity_prompt_customization RENAME TO tenant_prompt_customization;
legal_entity_* / *_legal_entity_id_fkey to tenant_* / *_tenant_id_fkey.Append a rename for every FK constraint named *_legal_entity_id_fkey and every constraint carrying legal_entity in the name. Pull the full list from dump/rename-audit.md Step 2. Example block (extend to full list):
-- Phase 5: FK constraint renames.
ALTER TABLE ap_acquisition_llm_stat RENAME CONSTRAINT acquisition_llm_stat_legal_entity_id_fkey TO acquisition_llm_stat_tenant_id_fkey;
ALTER TABLE ap_datev_export_audit RENAME CONSTRAINT datev_export_audit_legal_entity_id_fkey TO datev_export_audit_tenant_id_fkey;
ALTER TABLE ap_doc_source RENAME CONSTRAINT doc_source_legal_entity_id_fkey TO doc_source_tenant_id_fkey;
ALTER TABLE ap_qa_dataset_item RENAME CONSTRAINT qa_dataset_item_legal_entity_id_fkey TO qa_dataset_item_tenant_id_fkey;
ALTER TABLE api_key RENAME CONSTRAINT api_key_legal_entity_id_fkey TO api_key_tenant_id_fkey;
ALTER TABLE api_request_log RENAME CONSTRAINT api_request_log_legal_entity_id_fkey TO api_request_log_tenant_id_fkey;
ALTER TABLE booking_history_item RENAME CONSTRAINT booking_history_item_legal_entity_id_fkey TO booking_history_item_tenant_id_fkey;
ALTER TABLE booking_history_upload RENAME CONSTRAINT booking_history_upload_legal_entity_id_fkey TO booking_history_upload_tenant_id_fkey;
ALTER TABLE business_partner_dataset RENAME CONSTRAINT business_partner_dataset_legal_entity_id_fkey TO business_partner_dataset_tenant_id_fkey;
ALTER TABLE cost_center_dataset RENAME CONSTRAINT cost_center_dataset_legal_entity_id_fkey TO cost_center_dataset_tenant_id_fkey;
ALTER TABLE datev_rewe_link RENAME CONSTRAINT datev_rewe_link_legal_entity_id_fkey TO datev_rewe_link_tenant_id_fkey;
ALTER TABLE document RENAME CONSTRAINT document_legal_entity_id_fkey TO document_tenant_id_fkey;
ALTER TABLE fpna_data_map RENAME CONSTRAINT fpna_data_map_legal_entity_id_fkey TO fpna_data_map_tenant_id_fkey;
ALTER TABLE gl_accounts_dataset RENAME CONSTRAINT gl_accounts_dataset_legal_entity_id_fkey TO gl_accounts_dataset_tenant_id_fkey;
ALTER TABLE tenant_datev_integration RENAME CONSTRAINT legal_entity_datev_integration_legal_entity_id_fkey TO tenant_datev_integration_tenant_id_fkey;
ALTER TABLE tenant_oauth_integration RENAME CONSTRAINT legal_entity_oauth_integration_legal_entity_id_fkey TO tenant_oauth_integration_tenant_id_fkey;
ALTER TABLE tenant_prompt_customization RENAME CONSTRAINT legal_entity_prompt_customization_legal_entity_id_fkey TO tenant_prompt_customization_tenant_id_fkey;
ALTER TABLE notification_channel RENAME CONSTRAINT notification_channel_legal_entity_id_fkey TO notification_channel_tenant_id_fkey;
ALTER TABLE notification_routing RENAME CONSTRAINT notification_routing_legal_entity_id_fkey TO notification_routing_tenant_id_fkey;
ALTER TABLE ap_supplier_verification RENAME CONSTRAINT supplier_verification_legal_entity_id_fkey TO supplier_verification_tenant_id_fkey;
-- Child-table pkeys from the renamed legal_entity_* tables:
ALTER TABLE tenant_datev_integration RENAME CONSTRAINT legal_entity_datev_integration_pkey TO tenant_datev_integration_pkey;
ALTER TABLE tenant_oauth_integration RENAME CONSTRAINT legal_entity_oauth_integration_pkey TO tenant_oauth_integration_pkey;
ALTER TABLE tenant_prompt_customization RENAME CONSTRAINT legal_entity_prompt_customization_pkey TO tenant_prompt_customization_pkey;
-- Organization FK on new tenant table:
ALTER TABLE tenant RENAME CONSTRAINT legal_entity_organization_fk TO tenant_organization_fk;
The constraint names above are reference-only — CROSS-CHECK each one against dump/rename-audit.md and adjust. Include ANY additional constraints you find.
Append, adjusting each entry against the enumeration from dump/rename-audit.md Step 3:
-- Phase 6: index renames.
ALTER INDEX idx_acquisition_llm_stat_legal_entity RENAME TO idx_acquisition_llm_stat_tenant;
ALTER INDEX idx_datev_export_audit_legal_entity_id RENAME TO idx_datev_export_audit_tenant_id;
ALTER INDEX idx_qa_dataset_item_legal_entity RENAME TO idx_qa_dataset_item_tenant;
ALTER INDEX idx_notification_routing_legal_entity RENAME TO idx_notification_routing_tenant;
-- ... continue for every index in Task 1 Step 3 output ...
For each function enumerated in Task 1 Step 4 whose body references legal_entity_id, emit a CREATE OR REPLACE FUNCTION with:
"legal_entity_id" → "tenant_id", and if the function also emits the old-sense tenant_id, that key becomes "organization_id".NEW.legal_entity_id → NEW.tenant_id, NEW.tenant_id (old-sense) → NEW.organization_id.Example shape (adjust against the actual function body captured in rename-audit.md):
-- Phase 7: trigger function bodies.
CREATE OR REPLACE FUNCTION notify_ingestion_event() RETURNS trigger AS $$
BEGIN
PERFORM pg_notify(
'ingestion_event',
json_build_object(
'ingestion_id', NEW.id,
'document_id', NEW.document_id,
'tenant_id', NEW.tenant_id, -- was: legal_entity_id
'status', NEW.status
)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Include notify_datev_export_event, notify_document_classified, notify_ingestion_event, and any other function captured in Step 4. Use the captured body — do NOT paraphrase. The only changes are identifier + key renames.
For each (table, column, jsonb_path) triple flagged by dump/rename-audit.md Step 6, append an UPDATE statement. Two patterns:
Flat key rename inside an object:
UPDATE api_key
SET permissions = permissions - 'legal_entity_id' || jsonb_build_object('tenant_id', permissions->'legal_entity_id')
WHERE permissions ? 'legal_entity_id';
Array-of-objects key rename (e.g. permissions is [{"legal_entity_id": ...}, ...]):
UPDATE api_key
SET permissions = (
SELECT jsonb_agg(
CASE WHEN elem ? 'legal_entity_id'
THEN (elem - 'legal_entity_id') || jsonb_build_object('tenant_id', elem->'legal_entity_id')
ELSE elem
END)
FROM jsonb_array_elements(permissions) elem
)
WHERE permissions::text LIKE '%legal_entity_id%';
Pick the variant matching the actual shape observed during Task 1 Step 6. If audit shows zero rows affected, omit that statement entirely — do NOT ship no-op updates.
legal_entity.accounting_system (now tenant.accounting_system) and the enum type.Append:
-- Phase 9: drop accounting_system column and enum.
-- No longer needed: DATEV vs other accounting system is derivable from
-- tenant_datev_integration row existence (or .is_active field).
-- Per spec discrepancy note 2: column is on legal_entity (post-rename: tenant),
-- not on the old tenant.
ALTER TABLE tenant DROP COLUMN accounting_system;
DROP TYPE accounting_system;
Confirm accounting_system has no other usages:
psql -h localhost -U postgres -d orcha -c "SELECT table_name, column_name FROM information_schema.columns WHERE udt_name='accounting_system';"
Expected: zero rows (after the column drop would leave it unused; before it, only legal_entity.accounting_system).
Run:
psql -h localhost -U postgres -d orcha -f resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.up.sql --set ON_ERROR_STOP=on
Expected: commits successfully. If it errors, fix the offending statement and re-run after manually reverting any partial changes (or drop + reinit the local DB — use bb dev:db:init or equivalent).
Run:
psql -h localhost -U postgres -d orcha -c "\dt" | grep -E "tenant|organization|legal"
psql -h localhost -U postgres -d orcha -c "SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public' AND column_name IN ('tenant_id','legal_entity_id','organization_id') ORDER BY table_name;"
Expected: no legal_entity* tables; organization, organization_membership, tenant, tenant_datev_integration, tenant_oauth_integration, tenant_prompt_customization all present; no legal_entity_id columns remain; notification_channel_teams.tenant_id (TEXT, external) unchanged.
git add resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.up.sql
git commit -m "chore(db): add legal_entity → tenant rename migration (up)"
.down.sql migrationFiles:
Create: resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.down.sql (same timestamp as .up.sql).
Step 1: Reverse the up migration phase-by-phase, in OPPOSITE order.
Write a header explaining the known asymmetry (accounting_system column restored as NULL; accounting_system enum restored; JSONB reverts applied; trigger functions restored to pre-rename bodies — use the bodies from rename-audit.md Step 4 verbatim).
Order:
Phase 9 reverse: recreate accounting_system enum + column (NULLABLE; original values lost).
Phase 8 reverse: reverse JSONB key renames.
Phase 7 reverse: restore trigger function bodies from rename-audit.md.
Phase 6 reverse: rename indexes back.
Phase 5 reverse: rename FK constraints back.
Phase 4 reverse: rename tenant → legal_entity, child tables back.
Phase 3 reverse: rename tenant_id columns back to legal_entity_id.
Phase 2 reverse: rename organization → tenant, organization_membership → tenant_membership, all index/constraint normalizations undone.
Phase 1 reverse: rename organization_id columns back to tenant_id (on the now-restored legal_entity and tenant_membership).
Wrap in BEGIN; ... COMMIT;.
accounting_system restore, recreate the enum first, then the column.-- Phase 9 reverse: recreate enum and column. Enum values lost on drop; they
-- must be re-specified literally. Source: resources/migrations/20260219091057-squashed-to-v1-963.up.sql
-- defined the enum as a single value: `('datev')`.
CREATE TYPE accounting_system AS ENUM ('datev');
ALTER TABLE legal_entity ADD COLUMN accounting_system accounting_system;
Re-confirm the value list before committing by scanning the squash migration:
grep -n "CREATE TYPE accounting_system" resources/migrations/20260219091057-squashed-to-v1-963.up.sql
Expected: CREATE TYPE accounting_system AS ENUM ('datev');. If any subsequent migration ever added values via ALTER TYPE ... ADD VALUE, include those too (unlikely — no such migration exists at time of writing).
.down.sql from a migrated state.# Assuming up has already been applied:
psql -h localhost -U postgres -d orcha -f resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.down.sql --set ON_ERROR_STOP=on
psql -h localhost -U postgres -d orcha -c "\dt" | grep -E "tenant|organization|legal"
Expected: tenant, tenant_membership, legal_entity, legal_entity_datev_integration, legal_entity_oauth_integration, legal_entity_prompt_customization. No organization* tables.
Run up again to leave the DB in the migrated state for the subsequent code work:
psql -h localhost -U postgres -d orcha -f resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.up.sql --set ON_ERROR_STOP=on
git add resources/migrations/YYYYMMDDHHMMSS-rename-legal-entity-to-tenant.down.sql
git commit -m "chore(db): add legal_entity → tenant rename migration (down)"
tenant → organizationThe branch must stay compilable between Task 4 and Task 5. Stage 1 keeps legal-entity meaning tenant, so the codebase is internally coherent: organization is the new name for the grouping, legal-entity is still the operational unit.
Files: every file under src/, test/, dev/, plus resources/com/getorcha/config.edn. Exact list enumerated inside the steps below.
tenant.grep -rln ':tenant/\|:tenant-id\|tenant-id\|"tenants"\|"Tenant"\|"Tenants"\|/tenants\|*tenant\*\|defn.*tenant\|ns.*tenant\|tenant-membership\|tenant_membership' src/ test/ dev/ resources/ 2>/dev/null | grep -v legal_entity > /tmp/stage1-candidates.txt
wc -l /tmp/stage1-candidates.txt
Review the list manually — some matches will be false positives (e.g. tokens in strings, unrelated comments). Narrow it against the naming-pattern table from the spec.
git mv the schema file.git mv src/com/getorcha/schema/tenant.clj src/com/getorcha/schema/organization.clj
Inside src/com/getorcha/schema/organization.clj:
(ns com.getorcha.schema.tenant ...) → (ns com.getorcha.schema.organization ...).:com.getorcha.schema.tenant/id (and any other tenant-sense registry keys) → :com.getorcha.schema.organization/id.tenant-schema → organization-schema, etc.Commit:
git add src/com/getorcha/schema/organization.clj
git commit -m "refactor(schema): rename old-sense tenant namespace to organization"
git mv only the top-level admin file; leave the tenants/ directory in place.git mv src/com/getorcha/admin/http/tenants.clj src/com/getorcha/admin/http/organizations.clj
Do NOT move the admin/http/tenants/ directory. Its contents (file_store.clj, prompt_customizations.clj) are per-legal-entity pages, which after Stage 2 become per-new-tenant pages — so the directory name tenants/ is semantically correct post-rename and needs no further move. This avoids a Stage-1-then-undo dance in Stage 2. Final SP1 layout:
admin/http/organizations.clj ← old tenants.clj (org + tenant list page)
admin/http/tenants/
file_store.clj ← per-tenant, unchanged location
prompt_customizations.clj ← per-tenant, unchanged location
SP2 will later add admin/http/tenants.clj (the tenant detail page handler) alongside the directory; that's not SP1's concern.
Inside admin/http/organizations.clj:
Update (ns com.getorcha.admin.http.tenants ...) → (ns com.getorcha.admin.http.organizations ...).
Update any :require aliases in OTHER files that pointed at com.getorcha.admin.http.tenants (the old-sense namespace) — find them:
grep -rn "com.getorcha.admin.http.tenants" src/ test/ dev/
These fall into two groups: requires of the old tenants.clj (→ organizations), and requires of the nested namespaces com.getorcha.admin.http.tenants.file-store / com.getorcha.admin.http.tenants.prompt-customizations. The nested ones keep the same require string — their namespace stays com.getorcha.admin.http.tenants.file-store because the directory didn't move. Only update requires of the top-level tenants namespace.
Step 4: Update routes from /tenants/* to /organizations/* in the admin router.
Find the route registrations:
grep -rn '"/tenants' src/com/getorcha/admin/
In each hit, replace /tenants → /organizations. Example:
;; before
["/tenants" {:get ... :post ...}]
["/tenants/-/generate-slug" ...]
["/tenants/:id" ...]
;; after
["/organizations" {:get ... :post ...}]
["/organizations/-/generate-slug" ...]
["/organizations/:id" ...]
The nested legal-entity sub-routes go through two path segments of rename. Before Stage 1 they look like /tenants/-/legal-entities/:id/... (where the leading /tenants/ is old-sense = organization). Stage 1 flips only the leading segment: /organizations/-/legal-entities/:id/.... Stage 2 (Task 5 Step 4) then flips the nested segment: /organizations/-/tenants/:id/....
:tenant/* Malli registry keys and *tenant* / *tenant-id* dynamic vars.Find:
grep -rn ':com.getorcha.schema.tenant/' src/ test/ dev/ # old-sense registry key
grep -rn '\*tenant\*\|\*tenant-id\*\|\*current-tenant' src/ test/ dev/
Each usage: replace tenant → organization in the keyword / symbol. Tenant-id middleware (src/com/getorcha/logging/mdc.clj, src/com/getorcha/app/http/middleware/auth.clj, src/com/getorcha/http/middleware/xray.clj) typically carries these vars — confirm.
grep -rn '::tenant\b\|:com.getorcha.tenant/\|:com.getorcha/tenant\b' src/ resources/ dev/
For every match in old sense (grouping), rewrite tenant → organization. Update resources/com/getorcha/config.edn correspondingly.
grep -rn '"Tenants"\|"Tenant "\|"tenant "\|"tenant list"\|"tenant page"' src/com/getorcha/admin/
Rewrite to "Organizations" / "Organization" where the context is the grouping (today's admin tenant list shows the old-sense concept).
clj-kondo --lint src test dev
clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Execution error|failed because|Ran .* tests)"
Fix every kondo problem. Every test must pass. No ^:private shortcuts to silence unused-var warnings — if a rename created dead code, remove the dead code.
git add -A
git commit -m "refactor(schema): rename old-sense tenant to organization (code side)"
legal_entity → tenantNow the codebase is internally consistent: organization means grouping, legal-entity means operational unit. This stage flips legal-entity to tenant.
legal-entity / legal_entity / :legal-entity.grep -rln "legal[-_]entity\|:legal-entity" src/ test/ dev/ resources/ 2>/dev/null > /tmp/stage2-candidates.txt
wc -l /tmp/stage2-candidates.txt
Expect ~146 files.
git mv the schema file.git mv src/com/getorcha/schema/legal_entity.clj src/com/getorcha/schema/tenant.clj
Inside:
(ns com.getorcha.schema.legal-entity ...) → (ns com.getorcha.schema.tenant ...).
Malli registry key :com.getorcha.schema.legal-entity/id → :com.getorcha.schema.tenant/id.
All symbols: legal-entity-schema → tenant-schema, legal-entity-* → tenant-*, etc.
Step 3: No file moves needed for the nested admin namespaces.
The admin/http/tenants/ directory was left in place during Stage 1. Its contents (file_store.clj, prompt_customizations.clj) were already per-legal-entity and are now per-new-tenant — the directory name is correct post-rename. No git mv in this step.
What does change inside those files is covered by Step 5 (global legal-entity → tenant code rename): (ns com.getorcha.admin.http.tenants.file-store ...) retains its namespace form, but every legal-entity identifier inside the body becomes tenant.
/legal-entities/* to /tenants/* (nested under /organizations).grep -rn 'legal-entities\|"/legal-entit' src/com/getorcha/admin/
Rewrite each route segment:
/organizations/-/legal-entities → /organizations/-/tenants
/organizations/-/legal-entities/:id → /organizations/-/tenants/:id
/organizations/-/legal-entities/:id/file-store* → /organizations/-/tenants/:id/file-store*
/organizations/-/legal-entities/:id/prompts* → /organizations/-/tenants/:id/prompts*
Step 5: Global code rename — legal-entity → tenant, legal_entity → tenant, legalEntity → tenant.
Prefer IDE-assisted rename. As a fallback, per-file sed is acceptable but risky — always pair with a compile + test run. The pattern order matters:
legal_entity_id → tenant_id (the SQL column token — appears in HoneySQL strings, log messages, JSON parsing).:legal-entity-id → :tenant-id.legal-entity-id → tenant-id (symbol).legal-entity (kebab) → tenant.legal_entity (snake — any remaining) → tenant.legalEntity (camel) → tenant."Legal Entity" UI string → "Tenant"."Legal Entities" UI string → "Tenants".:com.getorcha.schema.legal-entity/ (keyword) → :com.getorcha.schema.tenant/.Example sed invocation for one file:
sed -i \
-e 's/legal_entity_id/tenant_id/g' \
-e 's/:legal-entity-id/:tenant-id/g' \
-e 's/legal-entity-id/tenant-id/g' \
-e 's/legal-entity/tenant/g' \
-e 's/legal_entity/tenant/g' \
-e 's/legalEntity/tenant/g' \
-e 's/"Legal Entity"/"Tenant"/g' \
-e 's/"Legal Entities"/"Tenants"/g' \
-e 's/:com\.getorcha\.schema\.legal-entity\//:com.getorcha.schema.tenant\//g' \
src/com/getorcha/path/to/file.clj
Process files in batches, compile after each batch:
clj -M:dev -e "(require 'com.getorcha.main :reload-all)"
le- abbreviations to tenant- abbreviations.The codebase uses le-id / le-ids / primary-le-id / all-le-ids / le-filter-ids / le-name / le-param / le-id-set / le-id-str / other-le-id / new-le-ids / authorized-le-ids / effective-le-ids / active-le-ids as short forms of legal-entity-id. These are NOT caught by the legal-entity → tenant rename above. Rename them explicitly to their tenant- equivalents (tenant-id, primary-tenant-id, all-tenant-ids, etc.).
Enumerate current usages (re-run before starting so the list reflects the live branch — origin/master merges may have added new ones):
grep -rnE '\ble-(id|ids|name|param|id-set|id-str|filter|filter-ids)\b|\b([a-z-]+)-le-(id|ids)\b' src/ test/ dev/ 2>/dev/null
Prefer IDE-assisted rename per symbol. As a sed fallback, the regex needs a word boundary (to avoid accidentally matching table-id, simple-id, etc.):
# Match `le-` at start-of-line OR after a non-letter character.
sed -i -E 's/(^|[^a-zA-Z])le-([a-z])/\1tenant-\2/g' src/com/getorcha/path/to/file.clj
Apply to one file at a time, compile after each. DO NOT run as a bulk one-liner over the whole tree without reviewing the diff per file — le- is short and the false-negative risk (from word-boundary edge cases) is real.
After the pass, re-run the enumeration command; it should return zero lines.
Find code that reads "legal_entity_id" as a string key (JSONB path) — this is separate from identifier references:
grep -rn '"legal_entity_id"' src/ test/ dev/
Each match is either:
"tenant_id"."legal_entity_id" — after the migration's Phase 8 rewrite, these must read "tenant_id".Update to "tenant_id".
Workers consume pg_notify events emitted by the trigger functions. Phase 7 of the migration changed the JSON keys. Find parsers:
grep -rn 'pg-notify\|listen\|:payload\|:legal_entity_id\|"legal_entity_id"' src/com/getorcha/workers/ src/com/getorcha/notifications/
For each event-parsing callsite, update:
(:legal_entity_id payload) → (:tenant_id payload)
(get payload "legal_entity_id") → (get payload "tenant_id")
Old-sense (:tenant_id payload) → (:organization_id payload) (if present in any emitted payloads, cross-check Phase 7 of the migration).
Step 9: Run tests + kondo iteratively until green.
clj-kondo --lint src test dev
clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Execution error|failed because|Ran .* tests)"
Iterate:
If kondo flags an unresolved symbol, it's almost always a missed rename. Fix it.
If a test fails with column "legal_entity_id" does not exist, there's an un-renamed HoneySQL query. Fix it.
If (reset) fails to boot (Integrant), the config.edn or an ig/init-key handler refers to a renamed namespace.
Step 10: Boot the system and smoke-test locally.
clj -M:dev
;; in the REPL:
(integrant.repl/reset)
;; then open admin UI in browser:
;; http://localhost:8080/admin/organizations
;; create an organization
;; create a tenant under it
;; edit both
;; verify stats pages load
Golden path: ingest a test document end-to-end (use /reingest-doc skill or the admin ingestion form). Verify the document routes to the correct tenant. Verify notification send (Slack/Teams if configured locally).
If Slack/Teams/DATEV credentials aren't available locally, note as "skipped — unverified" in the commit message, and verify in staging.
git add -A
git commit -m "refactor(schema): rename legal_entity to tenant (code side)"
legal_entity or old-sense tenantfind src test dev resources -type f \( -name '*legal_entity*' -o -name '*legal-entity*' \)
find src test dev resources -type f -name '*tenant*' | xargs grep -l "old-sense tenant means" 2>/dev/null
# Second line is a no-op heuristic; inspect each tenant-named file manually to
# decide whether it refers to old-sense tenant (→ rename to organization) or
# new-sense tenant (→ keep).
git mv each legacy-name file to its new name.One commit per git mv (or one commit per batch of moves). Update :require clauses in consumers after each move.
clj-kondo --lint src test dev
clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Execution error|failed because|Ran .* tests)"
git add -A
git commit -m "refactor: rename remaining legacy-named files to new terminology"
docs/architecture/*.mdgrep -rln 'legal[-_ ]entity\|\bTenant\b' docs/architecture/ 2>/dev/null
legal entity / legal_entity / Legal Entity → tenant / Tenant, and tenant (old sense, grouping) → organization (where context makes the old sense clear).Don't rename the files themselves unless the filename embeds the old vocabulary AND a rename improves discoverability. Document content is the priority.
git add docs/architecture/
git commit -m "docs(architecture): update vocabulary after legal_entity → tenant rename"
Filter: only docs whose meaning would be confusing to read today without context. Do NOT mass-rename historical text — add the header and move on.
grep -rln 'legal[-_ ]entity' docs/plans/ docs/superpowers/plans/ docs/superpowers/specs/ 2>/dev/null
For each: open it. If it's a completed historical plan that reads clearly as-written (e.g. date of the doc predates today and it uses legal_entity as an internal implementation detail), add the note. If it's a future/active plan that references the old names as if still valid, add the note AND decide whether to update content (only if the content is still actionable).
> **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.
Leave a blank line after the blockquote.
git add docs/
git commit -m "docs: add legal_entity → tenant rename note to affected plans/specs"
The spec says external API consumers are "declared none". Confirm empirically before merge.
psql -h localhost -U postgres -d orcha -c "SELECT api_key_id, COUNT(*) FROM api_request_log WHERE requested_at > now() - interval '90 days' GROUP BY api_key_id ORDER BY 2 DESC LIMIT 20;"
(Do this against prod when shipping — local dev DB won't have traffic.)
External consumers read API responses; the JSON output of handlers uses new names after the rename. If any client is observed relying on legal_entity_id response keys, stop and discuss compatibility with the user — this would be a spec revision, not an implementation detail.
No commit here — the result of this task either confirms the assumption (PR proceeds) or blocks it (stop-and-discuss).
clj-kondo --lint src test dev
Expected: 0 problems, 0 warnings, 0 errors.
clj -X:test:silent 2>&1 | tee /tmp/test-out.log | grep -E "(FAIL in|ERROR in|Execution error|Ran .* tests)"
Expected: Ran N tests containing M assertions. 0 failures, 0 errors. and no ERROR lines earlier.
# Drop + recreate DB, run ALL migrations including the new one, then boot.
bb dev:db:init # or equivalent reset command
clj -M:dev
;; in REPL:
(integrant.repl/go)
Expected: clean boot, no errors.
Obtain a recent prod dump (coordinate with the user; do NOT pull prod data without explicit approval — ask before doing this). Restore into a scratch local DB, apply .up.sql, then .down.sql, then .up.sql again. All three runs must succeed. Note the asymmetry in the accounting_system column (values will be NULL after down+up cycle — acceptable).
This step MAY be skipped if the user confirms staging verification is sufficient, but default is to run it.
Cover:
Load /admin/organizations — list renders.
Create a new organization — form submits, row appears.
Click into an organization — tenant list shows.
Create a tenant — form submits.
Edit the tenant — changes persist.
Load the end-user app / — no broken links.
Settings pages render correctly (/settings/*).
Ingest a test document — appears on the correct tenant.
Notifications fire (if credentials available).
Step 6: Commit any final polish.
git add -A
git commit -m "refactor: final polish for legal_entity → tenant rename"
(Only if there are pending changes. If clean, skip.)
Include:
Link to the spec (docs/superpowers/specs/2026-04-24-rename-legal-entity-to-tenant-design.md).
Link to this plan.
The "Spec discrepancies discovered" section from the top of this plan (they inform reviewers of decisions made).
Rollback instruction: "Run .down.sql, revert the merge commit, redeploy."
Test-plan checklist (repeat Task 10 steps 1-5).
Step 2: Do NOT merge to master without a quiet-window agreement from the user. The rename is a coordinated DB+code atomic deploy — not something to slip in silently.
The plan stops here. Sub-project 2 (admin UI restructure) is a separate plan.
Spec coverage:
datev_export_audit.organization_id, qa_dataset_item.organization_id, acquisition_llm_stat.organization_id) — addressed in the "Spec discrepancies" section as no-ops. ✅Placeholder scan: No TBD / "implement later" / naked "add error handling" / unstated code. Every step has either an exact command, exact SQL, or a concrete file-level instruction.
Type consistency: Malli registry keys, namespace paths, and route strings are consistent across tasks. Column names (tenant_id, organization_id) match the phase ordering in the migration.
Known remaining risks surfaced in the plan:
accounting_system enum values in .down.sql must be recovered from git history before committing the down migration.