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
identityrecords 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
taxmodule, module #17, for the calculation record;adminstill 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_keyis Admin-owned tenant-to-Vrida API access, distinct fromidentity's user-login auth tokens. The raw token is shown once at creation and never stored; onlytoken_hashpersists (hash-only storage, same discipline asidentity'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 4 — entity_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 4 — provider_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).adminreads this viaPlatformService, 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
taxmodule, 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 callingIdentityService. - Connector runtime/execution (OAuth flows, sync jobs, webhook delivery, vendor SDKs) — the future
integrationsmodule's domain.admin.integration_config/admin.webhook_configare the config Integrations reads; Admin owns no runtime code. - Cross-tenant/Vrida-operator review —
platform.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_profileisn'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 toplatform.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 inCROSS_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 inapps/api/src) — the same net-new dependencyplatform.tenant_profile.ein_refalready carries, not a second one.integration_configandwebhook_configare 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: ifpurchasing.purchase_order.approval_status(its own bespoke PO-approval column) is ever migrated to route through the shared approval engine, that is apurchasingreopen, not an Admin concern —approvals.approval_request.source_module/.source_type/.source_refalready accepts'purchasing'as a validsource_moduletoday with zero schema change needed if/when purchasing adopts it. (2026-07-09: this engine, includingapproval_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/phoneduplicated under identical names acrossadmin.tenant_business_profileandplatform.tenant_profile, plus a genuine security inconsistency (admin'sein_refwas a vault reference;platform'stax_idwas plaintextfor 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_requestwere tenant-scoped, tenant-RLS, and assumed the reviewer is the tenant — a fundamentally different shape fromplatform.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/paymentsreview_statuscolumns 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_requeston 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'sidentity.identity_usertoidentity.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 intoapprovals, where they were further renamed and hardened (initiator_actor_id, now NOT NULL — closing a null-approver bypass;resolved_by_actor_idunchanged 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 newtaxmodule (module #17) does not change Admin's own scope — Admin still owns zero tax-rate logic;taxpersists 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 left — admin: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_settingreads/writes MUST respect the two-partial-unique NULL-distinctness pattern (tenant_id, category, keywheresite_id IS NULLvs.tenant_id, category, key, site_idwheresite_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_keyissuance MUST show the raw token exactly once at creation time and persist onlytoken_hash— never log or store the raw value anywhere, including application logs.custom_field_definitionreads/writes (Remediation Phase 4) MUST validateentity_typeagainst the 7 allowed values andfield_typeagainst 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 = truescoping) when re-registering a retired key.
10. Cross-Module Seams
- admin → platform: every table's
tenant_id → platform.tenant.id(identity itself is read viaPlatformService, 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-tableentity_idrollout). - admin → multi_loc:
tenant_setting.site_id(nullable override) andhardware_device.site_id(required) →multi_loc.site.id. (approval_routing_rule.site_idwas this seam's 3rd member until 2026-07-09 — it moved toapprovalsalong 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 asplatform.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 intopurchasing/pos/orders; this seam moved out of Admin along with the table it belongs to (§7 DR-H) and is now documented indocs/modules/module_spec/approvals.md, not here. - admin.setting_definition → admin.tenant_setting (Remediation Phase 3, NOT DB-enforced):
(category, key)is intended to matchtenant_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_idis a new, independently-nullable FK added alongside the existing free-formintegration_typeCHECK-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 theattributesJSONB columns oncrm.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 thesetting_definitionnon-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_definition ↔ tenant_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_catalog ↔ integration_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_requestcould be self-approved (the same actor as both requester and resolver) with no DB-level guard. Fixed by addingchk_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_definitionconfig-key catalog (2026-07-08). Admin gained a new 11th table,admin.setting_definition(12 cols), closing the gap wheretenant_settingwas pure free-form JSONB with nothing to validate a(category, key)typo or a wrong-shapedvalueagainst, and no way to distinguish a tenant-editable setting from a platform-managed one. It is global reference data — notenant_id, no RLS — mirroringidentity.permission/identity.agent_type_catalog's own catalog-table precedent exactly (anis_activeflag 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 coversadmin.tenant_settingonly. 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 theaimodule'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 againsttenant_setting— a Postgres CHECK cannot reference another table's columns, and retrofitting a validation trigger onto the already-lockedtenant_settingtable 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) indocs/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 17d —
admin.integration_provider_catalog(7 cols, 6 seeded rows:quickbooks/stripe/mailchimp/twilio/resend/other) — global reference data (notenant_id, no RLS), mirroringsetting_definition's own Phase 3 precedent, added alongsideadmin.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-formintegration_typeCHECK-enum column onintegration_configis 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 19 —admin.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-ungovernedattributesJSONB 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_typerestricts to exactly these 7 values;chk_custom_field_definition_field_typerestricts to 6 value types (boolean/number/string/json/array/date);custom_field_definition_tenant_entity_key_uniqueis a partial unique WHEREis_active = true, allowing a retired key to be re-registered. RLS enabled with the standard tenant-isolation policy. Item 15 —admin.compliance_documentgainedentity_id(+1 col), a nullable FK →platform.legal_entity, one of 10 header-tableentity_idadditions 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 additive —integration_configandcompliance_documentboth had 0 rows at build time,custom_field_definitionis brand-new — zero backfill risk anywhere, zeroDROP TABLE/DROP COLUMNin this phase (independently confirmed by adversarial verification). Both new non-seams are disclosed, deferred gaps:integration_provider_catalogis not yet DB-enforced againstintegration_config.integration_type(no sync constraint), andcustom_field_definitionis not yet DB-enforced against the 7 target entities'attributesJSONB columns (a Postgres CHECK cannot validate another table's JSONB shape) — both logged to OPEN_ITEMS, the identical disclosed-gap shapesetting_definitionalready established againsttenant_settingin 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(thecompliance_document.entity_idALTER — 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
approvalssplit 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'sapproval_workflow/approval_routing_rule/approval_requesttrio 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_requestMOVED fromadminto a newapprovalsschema viaALTER TABLE ... SET SCHEMA(RLS/triggers/grants carried over automatically, confirmed live, not assumed) —approval_workflowextended 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_requestextended 18→20 cols (requested_by_actor_idrenamed toinitiator_actor_idand made NOT NULL, closing a null-approver bypass;step_historydropped, superseded by a new queryableapproval_steptable). Both tables'workflow_typeCHECK-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 areSECURITY 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.