admin — Module Spec

1. Purpose

admin owns the tenant's own TECHNICAL/OPERATIONAL configuration — hardware pairing, integration/webhook config, API keys, tenant-tunable settings, presentational branding, and compliance documents. This is the first v2 pass of this module: v1 was locked 2026-06-10 at 11 tables / 145 columns and was never rebuilt for v2 until now (PROJECT_DECISIONS #34/#36).

2026-07-09 update: admin originally also owned the generic tenant-side approval engine (approval_workflow/approval_routing_rule/approval_request). That engine MOVED OUT into its own dedicated approvals schema on 2026-07-09, reversing this doc's own DR-B decision (Option B, "stays Admin-internal") — see §7 DR-B and the new §7 DR-H below, and docs/modules/module_spec/approvals.md for the new module's own spec. Everywhere below that still narrates the approval engine as part of Admin is describing the historical, now-superseded 2026-06-10→2026-07-08 shape, not the current one.

admin does not own tenant identity. platform.tenant_profile is the single source of truth for the tenant's legal name, EIN, business type, addresses, DBAs, and NAICS classification (PROJECT_DECISIONS #34 — the governing Admin↔Platform data-ownership boundary, which reverses v1's own "Admin Module Scope" decision). admin references platform.tenant for identity via tenant_id FKs on every table; it never duplicates an identity fact. This reversal is the defining event of this v2 pass: v1's tenant_business_profile table (17 cols) is dropped entirely, 16 of its 17 columns absorbed into platform.tenant_profile (Platform's 4th reopen, PROJECT_DECISIONS #35), and the module's remaining 10 tables are ported forward faithfully.

Carried forward unchanged from v1's own design rationale (docs/old/design_rationale/rationale_admin.md):

  • HR/labor/scheduling is permanently out of Vrida — not deferred, not future. The only "staff" data in Vrida is identity records plus actor-stamp columns on operational tables. Businesses use Gusto/ADP/Homebase/Deputy.
  • No local tax rate/jurisdiction tables. Stripe Tax owns rate lookup and calculation (v1's own guard — v2 additionally now has the dedicated tax module, module #17, for the calculation record; admin still owns none of it).
  • Security policy config lives in Identity, not Admin. password_policy/sso_provider/session_config/ip_whitelist-shaped config belongs with auth ownership.
  • api_key is Admin-owned tenant-to-Vrida API access, distinct from identity's user-login auth tokens. The raw token is shown once at creation and never stored; only token_hash persists (hash-only storage, same discipline as identity's own token handling).

2. Ownership

Owns — 10 tables, 122 columns (as of the 2026-07-09 approvals split; was 13 tables / 161 cols under Remediation Phase 4):

Table Cols Autonomy Role
tenant_branding 12 Human-only Logo (Files forward-ref), colors, font, slogan, social handles — one active row per tenant
compliance_document 15 LIGHT (flag-only) Business license/insurance/permit tracking, expiry-dated; +1 col, Remediation Phase 4entity_id FK → platform.legal_entity
tenant_setting 11 LIGHT (flag-only) Generic key-value catch-all config, nullable site_id override
setting_definition 12 Human-only NEW, Remediation Phase 3 — config-key catalog for tenant_setting; global reference data, not tenant-scoped
hardware_device 13 LIGHT (flag-only) Register/printer/cash-drawer/card-reader/scanner pairing per site
integration_config 14 LIGHT (flag-only) Per-tenant 3rd-party connector config (QuickBooks/Stripe/Mailchimp/Twilio/Resend); +1 col, Remediation Phase 4provider_id FK → integration_provider_catalog
integration_provider_catalog 7 Human-only NEW, Remediation Phase 4 — global integration-provider catalog for integration_config; not tenant-scoped
webhook_config 12 LIGHT (flag-only) Outbound webhook endpoint + event subscriptions
api_key (Drizzle export adminApiKey) 14 LIGHT (flag-only) Tenant-to-Vrida programmatic access, hash-only token storage
custom_field_definition 12 Human-only NEW, Remediation Phase 4 — tenant-defined custom-field registry governing 7 confirmed-ungoverned attributes JSONB columns codebase-wide
Total 122

Sum reconciles exactly: 145 (v1) − 17 (tenant_business_profile, dropped) = 128 (first v2 pass) + 12 (setting_definition, Remediation Phase 3) = 140 (11 tables) + 7 (integration_provider_catalog) + 12 (custom_field_definition) + 1 (compliance_document.entity_id) = 161 (13 tables, Remediation Phase 4) − 10 (approval_workflow) − 11 (approval_routing_rule) − 18 (approval_request) = 122 (10 tables, 2026-07-09 approvals split — see §7 DR-H). The 3 subtracted tables did not disappear — they moved to approvals (via ALTER TABLE ... SET SCHEMA) and two were extended there (approval_workflow 10→12 cols, approval_request 18→20 cols); see docs/modules/module_spec/approvals.md.

Does NOT own:

  • Tenant identity — legal name, EIN, business type, addresses, DBAs, NAICS classification. Owned by platform.tenant_profile (PROJECT_DECISIONS #34). admin reads this via PlatformService, a service-layer call, never a cross-schema FK (this codebase's Rule 1: tenant identity is never looked up cross-schema by FK).
  • HR/labor/scheduling — permanently out of Vrida entirely (v1's own guard, re-confirmed, not revisited).
  • Tax rates/jurisdiction rules — Stripe Tax's domain; the calculation record lives in the dedicated tax module, not here.
  • Auth/security policy tables (password_policy, sso_provider, session_config, ip_whitelist) — identity's domain; Admin would only ever be the UI surface calling IdentityService.
  • Connector runtime/execution (OAuth flows, sync jobs, webhook delivery, vendor SDKs) — the future integrations module's domain. admin.integration_config/admin.webhook_config are the config Integrations reads; Admin owns no runtime code.
  • Cross-tenant/Vrida-operator reviewplatform.contract's own separate review gate (review_status/reviewed_by_actor_id), untouched, a different plane (see §7 DR-B / DR-H below).

3. Layer & Dependencies

Tenant-scoped configuration layer, referenced by (not depended on by) every other tenant-operational module for its own settings surface. Depends on: platform (tenant, set_updated_at — and, at the service layer, tenant_profile for identity reads), multi_loc (site — nullable override FK on tenant_setting, required FK on hardware_device), identity (actor — the 3 remaining retargeted attribution columns; 2 more retargeted columns left with approval_request when it moved to approvals on 2026-07-09, see §7 DR-C/DR-H). Depended on by (future, already-documented seams): integrations (connector.integration_config_id → admin.integration_config, webhook_delivery.webhook_config_id → admin.webhook_config — both real FK targets Admin needs no placeholder for), purchasing/pricing/payments (a genuine future opportunity, not a current dependency — see §7 DR-B).

4. Design-Phase Integrity — the 4 retained blocks

Full retained record lives in PROJECT_DECISIONS #36 (Admin's first v2 build, executing #34's Admin-side mechanics) and #34 (the governing rule itself) — cited here, not duplicated word-for-word.

  • Block 1 — counts. v1: 11 tables / 145 cols. v2: 10 tables / 128 cols. Delta: −1 table, −17 cols, both fully accounted for by tenant_business_profile's removal (§2 above; PROJECT_DECISIONS #34 Block 3 for the column-by-column fate).
  • Block 2 — consolidation. N/A — no two v1 tables were merged into one. The only structural change is a MOVED classification: tenant_business_profile isn't consolidated with another Admin table, its identity-shaped content is relocated to a different module's table (platform.tenant_profile), which PROJECT_DECISIONS #34 treats as its own named category, distinct from an in-module consolidation.
  • Block 3 — table fate. 10 of 11 v1 tables survive BUILT, unchanged in shape (faithful port — same columns, types, CHECK constraints, indexes). 1 of 11 (tenant_business_profile) is DROPPED, its columns' individual fates fully enumerated in PROJECT_DECISIONS #34 Block 3 (16 MOVED to platform.tenant_profile, 1 — attributes — DROPPED OUTRIGHT with no successor, zero known consumers, no defined shape).
  • Block 4 — dependency register (already established, not new discoveries). tenant_branding.logo_ref / compliance_document.document_ref — unchanged forward-refs to the not-yet-built Files module (files.file.id), already documented in CROSS_MODULE_CONTRACTS.md. integration_config.credentials_ref / webhook_config.secret_ref — vault references with no vault-encryption service existing anywhere in the codebase (confirmed via grep: zero Vault/Encryption service classes in apps/api/src) — the same net-new dependency platform.tenant_profile.ein_ref already carries, not a second one. integration_config and webhook_config are the real, already-documented FK targets for the not-yet-built Integrations module — nothing incomplete on Admin's own schema for these. A genuine future opportunity, not a blocker: if purchasing.purchase_order.approval_status (its own bespoke PO-approval column) is ever migrated to route through the shared approval engine, that is a purchasing reopen, not an Admin concern — approvals.approval_request.source_module/.source_type/.source_ref already accepts 'purchasing' as a valid source_module today with zero schema change needed if/when purchasing adopts it. (2026-07-09: this engine, including approval_request, no longer lives in Admin — see §7 DR-H.)

5. Part D — Capability Discovery (honest framing)

Admin is honestly a low-autonomy, mostly-human-configuration module. Its Part D capability discovery and autonomy/schema-translation pass ran during the design phase (the adversarially-verified Part B design document); only 2 of the original 10 tables reach a genuine autonomy surface beyond pure human config. The Remediation Phase 3 addition (setting_definition) and Remediation Phase 4 additions (integration_provider_catalog, custom_field_definition) are also human-only — see below.

D7 — Autonomy boundary, per table

Table Tier Rationale
tenant_branding Human-only Branding is a subjective creative/business decision — zero legitimate autonomy surface.
compliance_document LIGHT (flag-only) An expiry/staleness-flagging agent surface is legitimate (e.g., "insurance cert expires in 14 days"); the document itself is human-uploaded, never agent-authored. entity_id (Remediation Phase 4) is a plain scoping FK, not an autonomy surface.
tenant_setting LIGHT (flag-only) Same class — detection only, never autonomous write access to a tenant's own business-rule config.
setting_definition Human-only NEW, Remediation Phase 3 — static reference metadata Vrida engineering maintains, never an agent decision; same rationale as identity.permission/identity.agent_type_catalog.
hardware_device LIGHT (flag-only) Sync-failure / staleness flagging is legitimate; device credentials/pairing stay human-only.
integration_config LIGHT (flag-only) Sync-failure flagging is legitimate; never autonomous write access to connector credentials. provider_id (Remediation Phase 4) is a plain catalog reference, not an autonomy surface.
integration_provider_catalog Human-only NEW, Remediation Phase 4 — static reference metadata, the same rationale as setting_definition.
webhook_config LIGHT (flag-only) Delivery-failure flagging is legitimate; never autonomous write access to endpoints/secrets.
api_key (adminApiKey) LIGHT (flag-only) Staleness/unused-key flagging is legitimate; issuance/revocation of API credentials stays human-only — never autonomous.
custom_field_definition Human-only NEW, Remediation Phase 4 — a tenant business-configuration decision, the same class as workflow/routing-rule definitions; not an agent decision.

(2026-07-09: approval_workflow/approval_routing_rule (Human-only) and approval_request (the module's one-time FULL surface) moved to approvals — see §7 DR-H. Admin now has zero FULL-autonomy tables; its ceiling is LIGHT (flag-only).)

No automation_source/review_status/decision_provenance columns were added to any of the 10 tables. This is a deliberate, disclosed decision — a valid Section 0 outcome ("human-only, explicitly ruled out") — not an oversight: v1 never had these columns, and most of Admin's tables have no legitimate agent-autonomy surface to justify adding them speculatively. Where a future flagging surface is genuinely built (compliance/setting/hardware/integration/webhook/api_key staleness detection), it will be added at that time, not stubbed in now. setting_definition (Remediation Phase 3) and integration_provider_catalog/custom_field_definition (Remediation Phase 4) follow the identical pattern — zero autonomy columns, catalog-table/tenant-config-definition precedent.

6. Config-Surface Decision Tree

Where a new tenant-config-shaped setting belongs is governed by a 7-branch decision tree, already fully documented in PROJECT_DECISIONS #34 Section 6 (cited here, not repeated in full). The three branches most relevant to Admin's own tables:

  • Branch 5 — a named 3rd-party connector's own config → admin.integration_config.settings.
  • Branch 6 — a physical device's own config → admin.hardware_device.config.
  • Branch 7 — the tenant's own tunable business-process rule, with no more specific home → admin.tenant_setting (the default/fallback branch).

Remediation Phase 3 (2026-07-08, PROJECT_DECISIONS #39) added admin.setting_definition, a global catalog registering valid (category, key) pairs for branch 7 (tenant_setting) only — it does not extend to branches 5/6 (integration_config.settings, hardware_device.config), nor to identity.agent_identity.config or the ai module's own config fields, all deliberately left out of scope. See §11 DR-F below.

Remediation Phase 4 (2026-07-08, PROJECT_DECISIONS #40) added admin.integration_provider_catalog, a global catalog for branch 5 (integration_config's own connector identity) — an additive-interim provider_id FK alongside the existing free-form integration_type CHECK-enum, not yet kept in sync with it. Separately, Item 19 added admin.custom_field_definition, which does not sit on this 7-branch tree at all — it governs the 7 confirmed-ungoverned attributes JSONB columns on OTHER modules' entities (crm/inventory/orders/purchasing), not a tenant-config-shaped setting on an Admin table. See §11 DR-G below.

7. Design Rationale (DR-A…D)

  • DR-A — the Admin↔Platform identity reversal is the module's defining event, not a footnote. v1 explicitly assigned legal name/business type/EIN/addresses/DBAs/branding-adjacent identity to Admin's tenant_business_profile. A full column-level duplication scan found this created a real defect: legal_name/business_type/phone duplicated under identical names across admin.tenant_business_profile and platform.tenant_profile, plus a genuine security inconsistency (admin's ein_ref was a vault reference; platform's tax_id was plain text for the identical fact). PROJECT_DECISIONS #34 is the single governing rule; #35 executes Platform's absorption side; this doc + #36 execute Admin's drop side. See §4 Block 1–3 above for the mechanical accounting.
  • DR-B — (SUPERSEDED 2026-07-09 — see DR-H) the approval engine stays tenant-side (Option B), deliberately not converged with platform.contract's review gate. This was DECIDED Option B (PROJECT_DECISIONS #34 Section 5) at the time this doc was first written, and the reasoning below is retained as the historical record of why: approval_workflow/approval_routing_rule/approval_request were tenant-scoped, tenant-RLS, and assumed the reviewer is the tenant — a fundamentally different shape from platform.contract's own bolted-on review gate (review_status/review_reason/reviewed_by_actor_id/reviewed_at, added 2026-07-06), which is Vrida-operator-reviewed control-plane data on the opposite side of the Admin↔Platform boundary. The two did not converge because their RLS models were incompatible, not because no one considered it. Admin's engine was explicitly designed as the shared mechanism any future tenant-side approval need (purchasing/pricing/payments) could route through: purchasing already has its own bespoke, unrelated PO-approval column (purchase_order.approval_status); pricing/payments review_status columns are agent-anomaly review gates, a different shape/purpose entirely, not tenant approval routing. That forward-compatibility argument is exactly what motivated splitting the engine OUT into its own module on 2026-07-09 rather than leaving it Admin-internal — see DR-H.
  • DR-C — the module-wide actor-attribution retarget was originally 5 columns across 3 tables; 2 of those 5 left with approval_request on 2026-07-09 (see DR-H). tenant_setting.updated_by_actor_id, api_key.created_by_actor_id, api_key.revoked_by_actor_id — these 3, across 2 tables, are all that remain in Admin, retargeted from v1's identity.identity_user to identity.actor, matching every other module built since 2026-06-28. The other 2 (approval_request.requested_by_actor_id/.resolved_by_actor_id) moved with their table into approvals, where they were further renamed and hardened (initiator_actor_id, now NOT NULL — closing a null-approver bypass; resolved_by_actor_id unchanged in name). Admin is built correctly the first time here; it was never built stale and then patched.
  • DR-D — no local tax-rate/jurisdiction tables, HR/labor tables, or auth-policy tables — all re-confirmed, none revisited. These are v1's own explicit guards (docs/old/design_rationale/rationale_admin.md) and this v2 pass changes none of them. The existence of the new tax module (module #17) does not change Admin's own scope — Admin still owns zero tax-rate logic; tax persists Stripe Tax's calculation results, a different table entirely, in a different schema.

8. Agent Authority Mapping

No new authority mechanism — pure consumer of identity.agent_duty_grant, exactly as every module since crm. As of the 2026-07-09 approvals split (§7 DR-H), Admin has no FULL-autonomy surface leftadmin:approval_request:route (mechanical routing + SLA/anomaly flagging, the module's one-time genuine agent-actor permission) moved to approvals along with the table it governed. What remains is narrow flagging-only permission codes on the 6 LIGHT-tier tables (e.g., admin:integration_config:flag_sync_failure, admin:api_key:flag_stale, admin:hardware_device:flag_offline, admin:webhook_config:flag_delivery_failure, admin:compliance_document:flag_expiring) — all draft_only/observational, logging to ai.agent_execution (target_module='admin'). tenant_branding has no agent permission codes at all — human-only by design (§5).

9. Service Contract — AdminService

Not built this pass. No AdminService exists yet — schema-only build, matching every product module's first-pass precedent. Binding future requirements once built:

  • Must read tenant identity (legal name, EIN, business type, etc.) from PlatformService, never from a local column — Admin's own schema has no identity-shaped columns anywhere to accidentally read from instead (DR-A).
  • tenant_setting reads/writes MUST respect the two-partial-unique NULL-distinctness pattern (tenant_id, category, key where site_id IS NULL vs. tenant_id, category, key, site_id where site_id IS NOT NULL) — a single naive upsert against a 4-column key would violate the tenant-wide/site-override coexistence the schema is built to allow.
  • api_key issuance MUST show the raw token exactly once at creation time and persist only token_hash — never log or store the raw value anywhere, including application logs.
  • custom_field_definition reads/writes (Remediation Phase 4) MUST validate entity_type against the 7 allowed values and field_type against the 6 allowed value types at the application layer too, not rely on the DB CHECKs alone for UX-quality error messages; MUST respect the partial-unique reuse-after-deactivation pattern (is_active = true scoping) when re-registering a retired key.

10. Cross-Module Seams

  • admin → platform: every table's tenant_id → platform.tenant.id (identity itself is read via PlatformService, never FK'd — Rule 1). Remediation Phase 4: compliance_document.entity_id → platform.legal_entity.id (nullable — which of a tenant's legal entities the document belongs to; 1 of Item 15's 10-table entity_id rollout).
  • admin → multi_loc: tenant_setting.site_id (nullable override) and hardware_device.site_id (required) → multi_loc.site.id. (approval_routing_rule.site_id was this seam's 3rd member until 2026-07-09 — it moved to approvals along with its table; see §7 DR-H.)
  • admin → identity: the 3 remaining retargeted actor-attribution columns (§7 DR-C) → identity.actor.id.
  • admin → Files (deferred): tenant_branding.logo_ref, compliance_document.document_ref — plain text, no FK, Files not built.
  • admin → vault (deferred): integration_config.credentials_ref, webhook_config.secret_ref — plain text, no FK, no vault-encryption service exists yet anywhere in the codebase (same net-new dependency as platform.tenant_profile.ein_ref).
  • Integrations → admin (forward, real FK targets, Integrations not yet built): integrations.connector.integration_config_id → admin.integration_config.id; integrations.webhook_delivery.webhook_config_id → admin.webhook_config.id.
  • (relocated, 2026-07-09) approvals.approval_request → other modules (polymorphic, loose by design): source_module/source_type/source_ref — not an enforced FK into purchasing/pos/orders; this seam moved out of Admin along with the table it belongs to (§7 DR-H) and is now documented in docs/modules/module_spec/approvals.md, not here.
  • admin.setting_definition → admin.tenant_setting (Remediation Phase 3, NOT DB-enforced): (category, key) is intended to match tenant_setting.(category, key) but carries no FK (a CHECK cannot reference another table's columns) and no validation trigger — deliberately deferred, a disclosed gap, not silently assumed.
  • admin.integration_config → admin.integration_provider_catalog (Remediation Phase 4, additive-interim, NOT yet synced): provider_id is a new, independently-nullable FK added alongside the existing free-form integration_type CHECK-enum column — not yet kept in sync with it, the same disclosed "additive interim step" pattern as Items 17a–c.
  • admin.custom_field_definition → 7 other-module entities (Remediation Phase 4, NOT DB-enforced): (entity_type, field_key) is intended to govern the attributes JSONB columns on crm.customer, inventory.item, inventory.item_variant, orders.order_header, purchasing.purchase_receipt, purchasing.vendor, purchasing.vendor_item — no FK/trigger validates this (a CHECK cannot validate another table's JSONB shape), a disclosed gap, the same shape as the setting_definition non-seam above.

11. Deferred / Future Items

Files-module forward-refs (logo_ref, document_ref); vault-encryption service (credentials_ref, secret_ref — shared dependency with platform.tenant_profile.ein_ref); no AdminService yet; the Integrations module itself (Admin is only the config target, nothing deferred on Admin's own schema); the genuine future opportunity of migrating purchasing's bespoke PO-approval onto the shared approvals engine (not a current gap, and no longer Admin's own engine to migrate onto — see §4 Block 4, §7 DR-H); (Remediation Phase 3) setting_definitiontenant_setting validation (no FK/trigger, deferred); the 4 other free-form config surfaces (integration_config.settings, hardware_device.config, identity.agent_identity.config, ai's own config fields) deliberately left out of the setting_definition catalog's scope — a disclosed deferral, not an oversight; (Remediation Phase 4) integration_provider_catalogintegration_config.integration_type sync validation (no constraint yet, deferred, same disclosed shape as the Phase 3 gap above); custom_field_definition ↔ the 7 target entities' attributes JSONB validation (no FK/trigger possible against another table's JSONB shape, deferred); the cross-cutting catalog-table grant-model gap (all Vrida-wide catalog tables, including integration_provider_catalog, are writable/deletable by any tenant-scoped session today — a systemic, pre-existing Postgres role/grant-model gap this phase's new catalogs simply inherited, not introduced; logged for whoever owns the grant model); the legal_entity new-tenant-provisioning gap (no trigger/service hook creates a legal_entity row for tenants created after the Phase 4 backfill, which affects compliance_document.entity_id's eventual consumers).

  • DR-E — Remediation Phase 1 self-approval fix (2026-07-08). A cross-cutting senior-architect review found approval_request could be self-approved (the same actor as both requester and resolver) with no DB-level guard. Fixed by adding chk_approval_request_reviewer_not_creator: CHECK (resolved_by_actor_id IS NULL OR requested_by_actor_id IS NULL OR resolved_by_actor_id <> requested_by_actor_id). No column/table count change. Full cross-module record: PROJECT_DECISIONS #37.
  • DR-F — Remediation Phase 3, Item 13: the setting_definition config-key catalog (2026-07-08). Admin gained a new 11th table, admin.setting_definition (12 cols), closing the gap where tenant_setting was pure free-form JSONB with nothing to validate a (category, key) typo or a wrong-shaped value against, and no way to distinguish a tenant-editable setting from a platform-managed one. It is global reference data — no tenant_id, no RLS — mirroring identity.permission/identity.agent_type_catalog's own catalog-table precedent exactly (an is_active flag instead of soft delete; zero autonomy/automation_source/review columns, since this is Vrida-engineering-maintained static reference metadata, never an agent decision). Scope is deliberately narrow: it covers admin.tenant_setting only. Four other confirmed-live free-form config surfaces are explicitly OUT of scope, not an oversight — admin.integration_config.settings, admin.hardware_device.config, identity.agent_identity.config, and the ai module's own config fields — each is vendor-shaped or system/agent-managed, not a tenant-facing "setting" in this same sense; logged to OPEN_ITEMS as a deliberate deferral. Not DB-enforced against tenant_setting — a Postgres CHECK cannot reference another table's columns, and retrofitting a validation trigger onto the already-locked tenant_setting table was deliberately deferred rather than bundled into this catalog add; also logged to OPEN_ITEMS. Purely additive — brand-new table, zero rows, zero backfill risk. Table count 10 → 11; column count 128 → 140. Migration: packages/db/migrations/20260708260000_phase3_item13_setting_definition.sql. Full cross-module record: PROJECT_DECISIONS #39. Separately (documentation-only, no schema impact): this same Item 13 pass also documented the canonical money-unit-suffix convention (_cents/_minor_units/_millicents) in docs/database/SCHEMA_CONVENTIONS.md §8.1 for the first time — a cross-module convention, not specific to Admin.
  • DR-G — Remediation Phase 4, Items 15/17d/19: legal-entity scoping, the integration-provider catalog, and the custom-field registry (2026-07-08, final phase of the 4-phase remediation plan). Admin gained 2 new tables and 1 new column, bringing it to 13 tables / 161 columns. Item 17dadmin.integration_provider_catalog (7 cols, 6 seeded rows: quickbooks/stripe/mailchimp/twilio/resend/other) — global reference data (no tenant_id, no RLS), mirroring setting_definition's own Phase 3 precedent, added alongside admin.integration_config.provider_id (a new, independently-nullable FK). This is an additive-interim enum-to-catalog step matching Items 17a–c (POS tender types, payment terms, tax jurisdiction levels) in the same phase — the existing free-form integration_type CHECK-enum column on integration_config is left completely unchanged, and the new FK is NOT yet kept in sync with it; disclosed durably in the Drizzle TypeScript source (admin/config.ts), not just a migration comment. Item 19admin.custom_field_definition (12 cols: id, tenant_id, entity_type, field_key, display_name, description, field_type, default_value, is_required, is_active, created_at, updated_at) — a TENANT-SCOPED (unlike Item 17's catalogs) registry governing the 7 confirmed-ungoverned attributes JSONB columns codebase-wide: crm.customer, inventory.item, inventory.item_variant, orders.order_header, purchasing.purchase_receipt, purchasing.vendor, purchasing.vendor_item. chk_custom_field_definition_entity_type restricts to exactly these 7 values; chk_custom_field_definition_field_type restricts to 6 value types (boolean/number/string/json/array/date); custom_field_definition_tenant_entity_key_unique is a partial unique WHERE is_active = true, allowing a retired key to be re-registered. RLS enabled with the standard tenant-isolation policy. Item 15admin.compliance_document gained entity_id (+1 col), a nullable FK → platform.legal_entity, one of 10 header-table entity_id additions this phase rolled out codebase-wide (financial/legal-document-bearing HEADER tables only — a disclosed scoping rule: never line items, since a line item's entity is always inherited via its header's FK). NULL = the tenant's primary entity, today's implicit, unchanged behavior. All 3 changes purely additiveintegration_config and compliance_document both had 0 rows at build time, custom_field_definition is brand-new — zero backfill risk anywhere, zero DROP TABLE/DROP COLUMN in this phase (independently confirmed by adversarial verification). Both new non-seams are disclosed, deferred gaps: integration_provider_catalog is not yet DB-enforced against integration_config.integration_type (no sync constraint), and custom_field_definition is not yet DB-enforced against the 7 target entities' attributes JSONB columns (a Postgres CHECK cannot validate another table's JSONB shape) — both logged to OPEN_ITEMS, the identical disclosed-gap shape setting_definition already established against tenant_setting in Phase 3. Table count 11 → 13; column count 140 → 161. Migrations: packages/db/migrations/20260709030000_phase4_item17_enum_to_catalog.sql (part d); packages/db/migrations/20260709050000_phase4_item19_custom_field_definition.sql; packages/db/migrations/20260709010000_phase4_item15_legal_entity.sql (the compliance_document.entity_id ALTER — 1 of the 10-table rollout; the other 9 ALTERs live in their own modules). Full cross-module record: PROJECT_DECISIONS #40. This completes the 4-phase remediation plan.
  • DR-H — the 2026-07-09 approvals split reverses DR-B (PROJECT_DECISIONS #44). A codebase-wide sweep of every approval/review/workflow mechanism (across all 15 then-locked modules) found 17 distinct real business-process approvals, of which Admin's approval_workflow/approval_routing_rule/approval_request trio was one — and the only one living inside a config/settings module rather than the domain module whose action it gates. A design proposal to consolidate the 17 into one dedicated module went through 2 rounds of adversarial verification (a 3-lens pass, then a 7-lens pass explicitly targeting domain-agnosticism, the C8 money boundary, self-approval defeatability including the null-approver path, token security, polymorphic integrity, autonomy-seam exclusion, and admin-reopen scoping) before any schema was touched. The build: approval_workflow/approval_routing_rule/approval_request MOVED from admin to a new approvals schema via ALTER TABLE ... SET SCHEMA (RLS/triggers/grants carried over automatically, confirmed live, not assumed) — approval_workflow extended 10→12 cols (+step_mode, +blocks_agent_approver — the C8 boundary anchor, write-locked via table-level REVOKE + per-column re-GRANT after a live-reproduced Postgres-ACL bug: a column-level REVOKE alone does not subtract from an existing broader table-level GRANT), approval_request extended 18→20 cols (requested_by_actor_id renamed to initiator_actor_id and made NOT NULL, closing a null-approver bypass; step_history dropped, superseded by a new queryable approval_step table). Both tables' workflow_type CHECK-enum was also removed in the move — baking business-process names into the engine's own schema was a domain-knowledge leak the design-phase review caught. Plus 5 net-new tables (approval_policy, approval_step, approval_delivery, approval_token, approval_event) with 6 critical guards, all live-reproduced with real before/after proof, not asserted. A post-build independent adversarial verification pass then found one further real bug not caught by the build's own reproductions — a cross-tenant FK bypass on the 4 new child tables (Postgres FK checks bypass RLS; the new guard triggers are SECURITY INVOKER, so a crafted cross-tenant reference silently defeated them) — closed via composite FKs (child_col, tenant_id) → parent(id, tenant_id), a deliberately stronger fix than this codebase's existing trigger-based cross-tenant precedent (tax.trg_tax_calculation_validate_supersession). Net effect on Admin: 13 tables/161 cols → 10 tables/122 cols (−3 tables/−39 cols); Admin's one FULL-autonomy table is gone, its autonomy ceiling is now LIGHT (flag-only) — see §5. Full record: PROJECT_DECISIONS #44; new module spec: docs/modules/module_spec/approvals.md; new schema docs: docs/database/schema_docs/approvals.md.

12. v1 Exclusions Re-Confirmed

1 of 11 v1 tables dropped: tenant_business_profile. Not a capability loss in the ordinary sense — 16 of its 17 columns MOVED to platform.tenant_profile (full column-by-column fate in PROJECT_DECISIONS #34 Block 3); only attributes (1 column) is a genuine, disclosed, zero-known-consumer drop. Of the 10 tables Admin owns today, 7 (tenant_branding, compliance_document, tenant_setting, hardware_device, integration_config, webhook_config, api_key) are a faithful, unconsolidated v1 port — zero v1 columns/CHECKs/indexes dropped from any of them, only the module-wide actor-attribution retarget (§7 DR-C) applied. The other 3 (setting_definition, integration_provider_catalog, custom_field_definition) have no v1 precedent at all — brand-new Remediation Phase 3/4 additions (§11 DR-F/DR-G). Admin's original v2 pass also carried forward v1's approval_workflow/approval_routing_rule/approval_request trio, faithfully ported at the time — those 3 tables were later split out to the new approvals schema on 2026-07-09 (§7 DR-H) and are no longer part of Admin's own v1-exclusions accounting.

Last modified: Jul 9, 2026, 5:17 PM PT
On this page
Esc