admin — module #20 (reopened 2026-07-09 — approvals extraction)
10 tables, 122 columns — first v2 pass, built 2026-07-07 (PROJECT_DECISIONS #34/#36), plus a Remediation Phase 3 addition (setting_definition, 2026-07-08, PROJECT_DECISIONS #39) and a Remediation Phase 4 addition (integration_provider_catalog + custom_field_definition + compliance_document.entity_id, 2026-07-08, PROJECT_DECISIONS #40). admin is the tenant's own TECHNICAL/OPERATIONAL configuration: generic settings, hardware registry, integration/webhook config, API keys, compliance docs, and branding. v1 locked this module 2026-06-10 at 11 tables / 145 cols and it was never rebuilt for v2 until now. This build drops tenant_business_profile (17 cols) entirely — the governing boundary decision (PROJECT_DECISIONS #34) established that platform.tenant_profile is the single source of truth for tenant IDENTITY, not Admin. 16 of tenant_business_profile's 17 columns MOVED to platform.tenant_profile (executed as Platform's 4th reopen, PROJECT_DECISIONS #35); the 1 remaining column (attributes) was dropped outright with no successor (zero known consumers, no defined shape). The full 17-column fate mapping is the permanent record in PROJECT_DECISIONS #34, Block 3 — cited here, not re-derived. Depends on platform (tenant — every table's tenant scope; tenant_profile — the identity read Admin never duplicates; legal_entity — the Phase 4 entity-scoping seam), multi_loc (site — the site-override/site-scoping seam), identity (actor — all actor-attribution).
2026-07-09 (approvals-module reopen — REVERSES PROJECT_DECISIONS #34 Section 5's "Option B" call; Sections 1–4 of #34 untouched):
approval_workflow(10 cols),approval_routing_rule(11 cols), andapproval_request(18 cols) — 3 tables / 39 cols — MOVED out ofadminentirely into a new, dedicatedapprovalsmodule (8 tables / 97 cols, adding 5 net-new tables:approval_policy,approval_step,approval_delivery,approval_token,approval_event). Admin drops from 13 tables / 161 cols to 10 tables / 122 cols (161 − 39 = 122, exact); the remaining 10 tables are unchanged in shape. Seedocs/database/schema_docs/approvals.mdfor the new module's full definition.
PROJECT_DECISIONS entries: #34 (governing rule — Admin↔Platform data-ownership boundary), #35 (Platform's 4th reopen executing the identity absorption), #36 (this build), #39 (Remediation Phase 3 Item 13 — setting_definition), #40 (Remediation Phase 4 Items 15/17d/19 — legal_entity.entity_id seam, integration_provider_catalog, custom_field_definition).
Groups: Presentational (tenant_branding, compliance_document) / Technical-Operational Config (tenant_setting, setting_definition, hardware_device, integration_config, integration_provider_catalog, webhook_config, api_key) / Tenant Extensibility (custom_field_definition). (The former Tenant-Side Approval Engine group — approval_workflow/approval_routing_rule/approval_request — moved to the new approvals module at the 2026-07-09 reopen; see below.)
Global rules for this schema:
- Admin does NOT own tenant identity.
platform.tenant_profileis the single source of truth for legal name, EIN, business type, addresses, DBAs, NAICS classification (PROJECT_DECISIONS #34). Every Admin table carriestenant_id → platform.tenant.idfor tenant scoping only; none carries a copy of any identity-shaped fact. Where Admin needs an identity fact (e.g. rendering a tenant's legal name on a compliance document), it is a service-layer read ofPlatformService— never a cross-schema FK, matching this codebase's standing convention that tenant identity is never looked up cross-schema by FK. - Faithful port of v1, with exactly one schema-level change. The 7 tables that remain from v1's faithful port (
tenant_branding,compliance_document,tenant_setting,hardware_device,integration_config,webhook_config,api_key—tenant_business_profiledropped entirely at the 2026-07-07 build,approval_workflow/approval_routing_rule/approval_requestsince moved out to the newapprovalsmodule at the 2026-07-09 reopen) keep the same columns, types, CHECK constraints, and indexes as v1'sschema_admin.md. The only schema-level change: the module-wide actor-attribution retarget fromidentity.identity_usertoidentity.actor, applied to the 3 columns that carry a*_by_user_idshape (tenant_setting.updated_by_actor_id,api_key.created_by_actor_id,api_key.revoked_by_actor_id) — this matches every other module built since 2026-06-28. - Uniform tenant-scoping — all 10 tables carry
tenant_id NOT NULLFK →platform.tenant, RLS enabled with a permissive<table>_tenant_isolationpolicy oncurrent_setting('app.current_tenant_id')::uuid,FOR ALL TO authenticated,USING/WITH CHECKboth scoped to tenant. Plaintenant_idindex on all 10. updated_attrigger-maintained viaplatform.set_updated_at()(shared, reused) on all 10 tables.- Soft delete (
deleted_at) on all 10; every UNIQUE constraint isWHERE deleted_at IS NULL(or narrower, see per-table notes). - Low-autonomy module by design — now entirely human-only/flagging-only. With
approval_request(the module's one FULL-autonomy surface) moved to the newapprovalsmodule at the 2026-07-09 reopen, Admin now has ZERO tables reaching FULL autonomy. 4 of the remaining 10 tables reach a narrow LIGHT flagging-only surface:hardware_device/integration_config/webhook_config/api_key(detection only, e.g. sync-failure flagging, staleness flagging, expiry-sweep flagging; never autonomous write access to credentials/endpoints/issuance).tenant_brandingis explicitly human-only: branding is a subjective creative/business decision with zero legitimate autonomy surface. Noautomation_source/review_status/decision_provenancecolumns were added to any of the 10 tables — a deliberate, disclosed decision (Section 0's valid "human-only, explicitly ruled out" outcome): v1 never had these columns and most of Admin's tables have no legitimate agent-autonomy surface to justify adding them speculatively. - The approval engine moved out of Admin — REVERSES the prior Option-B call (PROJECT_DECISIONS #34 Section 5).
approval_workflow/approval_routing_rule/approval_requestno longer live in this schema — the 2026-07-09 reopen extracted them into a new, dedicatedapprovalsmodule (8 tables / 97 cols).platform.contract's own separate Vrida-operator review gate remains untouched and on a different plane regardless of this move. Seedocs/database/schema_docs/approvals.mdfor the current definition and the shared-mechanism rationale (purchasing/pricing/payments convergence opportunities are now logged in that module's own OPEN_ITEMS, not here). - Config-surface decision tree (PROJECT_DECISIONS #34 Section 6, 7 branches) governs where a new tenant-config-shaped setting belongs:
admin.tenant_settingis branch 7 (the tenant's own tunable business-process rule — the default/fallback);admin.integration_config.settingsis branch 5 (named 3rd-party connector config);admin.hardware_device.configis branch 6 (physical device config). Cite #34 for the full tree. - v1 baseline vs. this build — v1's
schema_admin.mdspecified 11 tables / 145 cols. This build: 10 tables / 128 cols (145 − 17 = 128, exact).tenant_business_profile(17 cols) DROPPED entirely (16 cols MOVED toplatform.tenant_profile, 1 col —attributes— dropped outright). The 10 surviving tables are unchanged in column count from v1:tenant_branding(12),compliance_document(14),tenant_setting(11),hardware_device(13),integration_config(13),webhook_config(12),api_key(14),approval_workflow(10),approval_routing_rule(11),approval_request(18). - Remediation Phase 3 (2026-07-08) added an 11th table,
setting_definition(12 cols) — see the dedicated subsection below. 128 + 12 = 140 cols, 11 tables, verified live. - Remediation Phase 4 (2026-07-08) added a 12th and 13th table,
integration_provider_catalog(7 cols, Item 17d) andcustom_field_definition(12 cols, Item 19), plus 1 new column on an existing table (compliance_document.entity_id, Item 15) — see the dedicated subsection below. 140 + 7 + 12 + 1 (existing table) = 161 cols, 13 tables (integration_configitself also gainsprovider_id, already counted in its own 13-col total below), verified live.
Cross-Phase / Cross-Module Foreign Keys (admin)
| Column | Target | Notes |
|---|---|---|
*.tenant_id (all 8 tenant-scoped tables — setting_definition/integration_provider_catalog excluded, see below) |
platform.tenant |
NOT NULL |
| — (service-layer read, no FK) | platform.tenant_profile |
Admin reads tenant legal name/business type/addresses/DBAs/NAICS/EIN-vault-ref via PlatformService; never stored as a column or FK anywhere in Admin — the standing "identity never looked up cross-schema by FK" convention |
tenant_setting.site_id, hardware_device.site_id |
multi_loc.site |
nullable on tenant_setting.site_id (NULL = tenant-wide, SET = site-level override); hardware_device.site_id is NOT NULL (every device belongs to a site). (approval_routing_rule.site_id moved to the new approvals module, 2026-07-09 reopen.) |
tenant_setting.updated_by_actor_id |
identity.actor |
nullable — retargeted from v1's identity.identity_user |
api_key.created_by_actor_id, api_key.revoked_by_actor_id |
identity.actor |
nullable — retargeted from v1's identity.identity_user |
tenant_branding.logo_ref |
files.file.id |
FORWARD-REF, unchanged from v1 — plain text, no FK; Files module not built |
compliance_document.document_ref |
files.file.id |
FORWARD-REF, unchanged from v1 — plain text, no FK; Files module not built |
compliance_document.entity_id |
platform.legal_entity |
nullable — Remediation Phase 4, Item 15 — which of a tenant's legal entities the document belongs to; NULL = the tenant's primary entity |
integration_config.credentials_ref |
vault (no schema target) | Vault reference — no vault-encryption service exists anywhere in the codebase yet (confirmed via grep); the same net-new dependency platform.tenant_profile.ein_ref already depends on, not a second one |
integration_config.provider_id |
admin.integration_provider_catalog |
nullable — Remediation Phase 4, Item 17d — additive-interim catalog FK, coexists with the existing free-form integration_type CHECK-enum column; not yet kept in sync |
webhook_config.secret_ref |
vault (no schema target) | Same vault dependency as credentials_ref above |
custom_field_definition.tenant_id |
platform.tenant |
NOT NULL — Remediation Phase 4, Item 19 |
integrations.connector.integration_config_id (inbound) |
→ admin.integration_config |
Real, already-documented FK target — Integrations module not yet built; Admin's own schema needs no placeholder, nothing incomplete on Admin's side |
integrations.webhook_delivery.webhook_config_id (inbound) |
→ admin.webhook_config |
Real, already-documented FK target — same as above |
admin.tenant_branding (12 cols, human-only)
One row per tenant. Logo, colors, fonts, slogan, and social handles for customer-facing theming. Owns the logo (logo_ref) — platform.tenant_profile.logo_url was deprecated in favor of this (PROJECT_DECISIONS #35).
Tenant-scoped. RLS enabled —
tenant_branding_tenant_isolation. Soft delete:deleted_at.updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
logo_ref |
text | nullable | — | FORWARD-REF — Files module object key; not built |
primary_color |
text | nullable | — | Hex color string, e.g. '#2D6A4F' |
secondary_color |
text | nullable | — | |
accent_color |
text | nullable | — | |
font_family |
text | nullable | — | Google Fonts family name or system font |
slogan |
text | nullable | — | Short tagline for receipts / app |
social_handles |
jsonb | nullable | — | {"instagram":"@handle","facebook":"page-slug","twitter":"@handle"} |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
CHECK constraints: none.
Indexes: PK; tenant_id; UNIQUE on (tenant_id) WHERE deleted_at IS NULL — one branding record per tenant.
Autonomy: human-only, explicitly ruled out — branding is a subjective creative/business decision with zero legitimate autonomy surface.
admin.compliance_document (14 cols, LIGHT — flagging-only)
Tenant's own business permits, insurance certs, and compliance documents. Expiry tracking for renewal alerts.
Tenant-scoped. RLS enabled —
compliance_document_tenant_isolation. Soft delete:deleted_at.updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
document_type |
text | NOT NULL | — | CHECK IN (business_license,insurance,seller_permit,resale_cert,other) |
name |
text | NOT NULL | — | Human-readable label, e.g. "Workers Comp – 2026" |
document_ref |
text | nullable | — | FORWARD-REF — Files module object key for the PDF; not built |
issuing_authority |
text | nullable | — | State agency, insurer, etc. |
identifier |
text | nullable | — | License or permit number |
issued_date |
date | nullable | — | |
expires_at |
date | nullable | — | Expiry date; NULL = no expiry |
status |
text | NOT NULL | 'active' |
CHECK IN (active,expired,pending,revoked) |
note |
text | nullable | — | |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
entity_id |
UUID | nullable | — | NEW, Remediation Phase 4, Item 15 — FK → platform.legal_entity; which of a tenant's legal entities this document belongs to; NULL = the tenant's primary entity |
CHECK constraints (2): chk_compliance_document_document_type; chk_compliance_document_status.
Indexes: PK; tenant_id; document_type; expires_at.
Autonomy: flagging-only surface possible (e.g. expiry-alert detection) but no autonomy columns added this pass — v1 never had them and none were justified speculatively.
admin.tenant_setting (11 cols, human-only)
Generic key-value configuration store. One row per (tenant, category, key) pair, with an optional site-level override. Absorbs settings deferred from POS, Orders, Billing, and notification preferences. This is config-surface-decision-tree branch 7 — the tenant's own tunable business-process rule, the default/fallback (PROJECT_DECISIONS #34 Section 6).
Tenant-scoped. RLS enabled —
tenant_setting_tenant_isolation. Soft delete:deleted_at.updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
site_id |
UUID | nullable | — | FK → multi_loc.site; NULL = tenant-wide default; NOT NULL = site-level override |
category |
text | NOT NULL | — | Logical grouping, e.g. 'pos', 'orders', 'billing', 'notifications', 'general' |
key |
text | NOT NULL | — | Setting identifier within category, e.g. 'hold_expiry_hours', 'receipt_footer' |
value |
jsonb | NOT NULL | — | Flexible value — shape defined per (category, key) at service layer |
description |
text | nullable | — | Human-readable note for the Owner Dashboard |
updated_by_actor_id |
UUID | nullable | — | FK → identity.actor — retargeted from v1's identity.identity_user |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
CHECK constraints: none.
Indexes: PK; tenant_id; (tenant_id,category); UNIQUE on (tenant_id,category,key) WHERE site_id IS NULL AND deleted_at IS NULL — one tenant-wide value per key (a single 4-col unique with nullable site_id would NOT block duplicate tenant-wide rows, since NULL ≠ NULL in Postgres — the 2-partial-unique split is v1's own pattern, preserved verbatim); UNIQUE on (tenant_id,category,key,site_id) WHERE site_id IS NOT NULL AND deleted_at IS NULL — one site-level override per (tenant, category, key, site); tenant-wide and site-specific rows for the same key coexist by design.
Autonomy: human-only, explicitly ruled out.
admin.setting_definition (12 cols, human-only) — Remediation Phase 3, Item 13 (2026-07-08)
NEW table, added Remediation Phase 3 (PROJECT_DECISIONS #39). The config-key catalog: a canonical registry of valid (category, key) pairs for admin.tenant_setting. Before this table, 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. setting_definition closes that gap by giving each (category, key) pair a display name, description, declared value type, default value, and editability flags.
Global reference data — NOT tenant-scoped. No tenant_id column, no RLS, no soft-delete deleted_at; instead an is_active flag, exactly mirroring identity.permission / identity.agent_type_catalog's own established catalog-table precedent. Zero autonomy/automation_source/review columns — this is Vrida-engineering-maintained static reference metadata, never an agent decision, same as those two precedent tables.
Global, not tenant-scoped. No RLS. No soft delete — uses
is_activeinstead.updated_at: trigger-maintained viaplatform.set_updated_at()(confirmed live).
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
category |
text | NOT NULL | — | Logical grouping, matches tenant_setting.category, e.g. 'pos', 'orders', 'billing' |
key |
text | NOT NULL | — | Setting identifier within category, matches tenant_setting.key |
display_name |
text | NOT NULL | — | Human-readable label for the Owner Dashboard |
description |
text | nullable | — | Longer explanation of what the setting controls |
value_type |
text | NOT NULL | — | CHECK chk_setting_definition_value_type IN (boolean,number,string,json,array) |
default_value |
jsonb | nullable | — | Default value if the tenant has not set an override |
is_site_scopable |
boolean | NOT NULL | false |
Whether this setting may carry a tenant_setting.site_id override |
is_tenant_editable |
boolean | NOT NULL | true |
false = platform-managed, not tenant-editable via the Owner Dashboard |
is_active |
boolean | NOT NULL | true |
Catalog-entry lifecycle flag — no soft-delete column on this table |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
CHECK constraints (1): chk_setting_definition_value_type.
Indexes: PK; UNIQUE setting_definition_category_key_unique on (category,key).
Autonomy: human-only, explicitly ruled out — this is Vrida-engineering-maintained static reference metadata, the same rationale as identity.permission/identity.agent_type_catalog.
Scope — deliberately narrow. Covers admin.tenant_setting ONLY. Four other free-form config surfaces confirmed live are explicitly OUT of scope for this catalog, 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 disclosed, deferred gap. No FK or trigger ties tenant_setting.(category, key) to this catalog. 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. Logged to OPEN_ITEMS, not silently assumed.
Additive, zero risk. Brand-new table, zero rows at build time — zero backfill risk.
Migration: packages/db/migrations/20260708260000_phase3_item13_setting_definition.sql.
admin.hardware_device (13 cols, LIGHT — flagging-only)
Physical device registry per site. Records every register, printer, cash drawer, card reader, and scanner. This is config-surface-decision-tree branch 6 — physical device config (PROJECT_DECISIONS #34 Section 6).
Tenant-scoped. RLS enabled —
hardware_device_tenant_isolation. Soft delete:deleted_at.updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
site_id |
UUID | NOT NULL | — | FK → multi_loc.site |
device_type |
text | NOT NULL | — | CHECK IN (register,printer,cash_drawer,card_reader,scanner,other) |
name |
text | NOT NULL | — | Human label, e.g. "Front Counter Register" |
identifier |
text | nullable | — | Serial number, Stripe reader ID, MAC address, etc. |
config |
jsonb | nullable | — | Device-specific configuration (Bluetooth pairing info, print settings, etc.) — branch 6 of the config-surface tree |
status |
text | NOT NULL | 'active' |
CHECK IN (active,inactive,maintenance) |
last_seen_at |
timestamptz | nullable | — | Last heartbeat or diagnostic ping — the staleness-flagging surface |
note |
text | nullable | — | |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
CHECK constraints (2): chk_hardware_device_device_type; chk_hardware_device_status.
Indexes: PK; tenant_id; site_id; device_type; UNIQUE on (tenant_id,site_id,name) WHERE deleted_at IS NULL.
Autonomy: LIGHT, narrow flagging-only surface — e.g. last_seen_at staleness detection. Never autonomous write access to device config/identifiers.
admin.integration_config (13 cols, LIGHT — flagging-only)
Per-tenant integration settings — enabled/disabled toggle, credentials vault reference, and sync schedule. Config only; runtime and connector code live in the Integrations module (real, already-documented inbound FK target: integrations.connector.integration_config_id → admin.integration_config). This is config-surface-decision-tree branch 5 — named 3rd-party connector config (PROJECT_DECISIONS #34 Section 6).
Tenant-scoped. RLS enabled —
integration_config_tenant_isolation. Soft delete:deleted_at.updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
integration_type |
text | NOT NULL | — | CHECK IN (quickbooks,stripe,mailchimp,twilio,resend,other) |
is_enabled |
boolean | NOT NULL | false |
Tenant-level on/off toggle |
credentials_ref |
text | nullable | — | Vault reference — OAuth tokens, API keys; no vault-encryption service exists yet (same dependency as platform.tenant_profile.ein_ref) |
settings |
jsonb | nullable | — | Integration-specific options (field mappings, preferences, etc.) — branch 5 of the config-surface tree |
sync_schedule |
text | nullable | — | Cron expression or named interval, e.g. '0 2 * * *' |
last_sync_at |
timestamptz | nullable | — | When last sync completed — Integrations writes this back |
last_sync_status |
text | nullable | — | CHECK (last_sync_status IS NULL OR last_sync_status IN ('success','failed','partial')) — the sync-failure-flagging surface |
status |
text | NOT NULL | 'inactive' |
CHECK IN (active,inactive,error) |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
provider_id |
UUID | nullable | — | NEW, Remediation Phase 4, Item 17d — FK → admin.integration_provider_catalog; additive-interim catalog reference, coexists with integration_type (the existing free-form CHECK-enum column); not yet kept in sync with it |
CHECK constraints (3): chk_integration_config_integration_type; chk_integration_config_status; chk_integration_config_last_sync_status.
Indexes: PK; tenant_id; integration_type; UNIQUE on (tenant_id,integration_type) WHERE deleted_at IS NULL — one config row per integration per tenant.
Autonomy: LIGHT, narrow flagging-only surface — e.g. sync-failure flagging via last_sync_status. Never autonomous write access to credentials_ref or connector behavior.
admin.integration_provider_catalog (7 cols, human-only) — Remediation Phase 4, Item 17d (2026-07-08)
NEW table, added Remediation Phase 4 (PROJECT_DECISIONS #40). A global reference catalog of known integration providers, mirroring the same additive-interim enum-to-catalog pattern applied to POS tender types, payment terms, and tax jurisdiction levels in this same phase (Items 17a–c). The existing integration_config.integration_type free-form CHECK-enum column is left completely unchanged; integration_config.provider_id is a new, independently-nullable FK added alongside it. This interim gap (the FK column is not yet constrained to stay in sync with the legacy CHECK-enum) is disclosed durably in the Drizzle TypeScript source (admin/config.ts), not just a migration-file SQL comment.
Global reference data — NOT tenant-scoped. No tenant_id column, no RLS, no soft-delete deleted_at; uses is_active instead, matching setting_definition's own Phase 3 catalog-table precedent (and identity.permission/identity.agent_type_catalog before that). Zero autonomy/automation_source/review columns — static reference metadata, never an agent decision.
Global, not tenant-scoped. No RLS. No soft delete — uses
is_activeinstead.updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
code |
text | NOT NULL | — | Provider code, e.g. 'quickbooks', 'stripe' |
name |
text | NOT NULL | — | Human-readable display name |
category |
text | nullable | — | e.g. 'accounting', 'payments', 'marketing', 'communications' |
is_active |
boolean | NOT NULL | true |
Catalog-entry lifecycle flag — no soft-delete column on this table |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
CHECK constraints: none.
Indexes: PK; UNIQUE integration_provider_catalog_code_unique on (code).
Seed data (6 rows, live-verified): quickbooks (QuickBooks, accounting), stripe (Stripe, payments), mailchimp (Mailchimp, marketing), twilio (Twilio, communications), resend (Resend, communications), other (Other, category NULL).
Autonomy: human-only, explicitly ruled out — static reference metadata, same rationale as setting_definition.
Not DB-enforced against integration_config.integration_type — a disclosed, deferred gap, identical in shape to setting_definition's own non-seam against tenant_setting: no FK or trigger keeps the legacy CHECK-enum and the new catalog FK in sync. Logged to OPEN_ITEMS.
Additive, zero risk. Brand-new table; integration_config had 0 rows at build time — zero backfill risk.
Migration: packages/db/migrations/20260709030000_phase4_item17_enum_to_catalog.sql (part d).
admin.webhook_config (12 cols, LIGHT — flagging-only)
Tenant-configured outbound webhook endpoints. Delivery runtime lives in the Integrations module (real, already-documented inbound FK target: integrations.webhook_delivery.webhook_config_id → admin.webhook_config).
Tenant-scoped. RLS enabled —
webhook_config_tenant_isolation. Soft delete:deleted_at.updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
name |
text | NOT NULL | — | Human label |
target_url |
text | NOT NULL | — | Destination endpoint |
event_subscriptions |
jsonb | NOT NULL | — | Array of event types to deliver: ["pos.sale_completed","inventory.stock_low",...] |
secret_ref |
text | nullable | — | Vault reference — HMAC signing secret; same dependency as integration_config.credentials_ref |
is_active |
boolean | NOT NULL | true |
|
last_delivery_at |
timestamptz | nullable | — | When last event was delivered |
last_delivery_status |
text | nullable | — | CHECK (last_delivery_status IS NULL OR last_delivery_status IN ('success','failed')) — the delivery-failure-flagging surface |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
CHECK constraints (1): chk_webhook_config_last_delivery_status.
Indexes: PK; tenant_id; is_active.
Autonomy: LIGHT, narrow flagging-only surface — e.g. delivery-failure flagging via last_delivery_status. Never autonomous write access to secret_ref or endpoint config.
admin.api_key (14 cols, LIGHT — flagging-only)
Tenant API keys for external integrations or third-party access (Enterprise tier). Stored hashed — raw value shown once at creation.
Tenant-scoped. RLS enabled —
api_key_tenant_isolation. Soft delete:deleted_at.updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
name |
text | NOT NULL | — | Human label for the key |
key_prefix |
text | NOT NULL | — | First 8 chars of the raw key — displayed for identification; not secret |
token_hash |
text | NOT NULL | — | SHA-256 hash of the raw key; raw value shown once at creation, never stored |
scopes |
jsonb | nullable | — | Permissions array: ["inventory.read","orders.write",...] |
last_used_at |
timestamptz | nullable | — | |
expires_at |
timestamptz | nullable | — | NULL = never expires — the expiry-flagging surface |
revoked_at |
timestamptz | nullable | — | When the key was revoked; NULL = active |
revoked_by_actor_id |
UUID | nullable | — | FK → identity.actor — retargeted from v1's identity.identity_user |
created_by_actor_id |
UUID | nullable | — | FK → identity.actor — retargeted from v1's identity.identity_user |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
deleted_at |
timestamptz | nullable | — | Soft delete |
CHECK constraints: none.
Indexes: PK; tenant_id; UNIQUE on (token_hash) WHERE deleted_at IS NULL — inbound key validation lookup; key_prefix — display listing; expires_at WHERE expires_at IS NOT NULL AND deleted_at IS NULL AND revoked_at IS NULL — expiry sweep.
Autonomy: LIGHT, narrow flagging-only surface — e.g. expiry-sweep flagging. Never autonomous write access to issuance or revocation.
Moved to the approvals module (2026-07-09 reopen)
admin.approval_workflow (10 cols), admin.approval_routing_rule (11 cols), and admin.approval_request (18 cols) — 3 tables / 39 cols — moved out of this schema entirely into a new, dedicated approvals module (8 tables / 97 cols, adding 5 net-new tables: approval_policy, approval_step, approval_delivery, approval_token, approval_event). This REVERSES PROJECT_DECISIONS #34 Section 5's "stays Admin-internal, Option B" call; Sections 1–4 of #34 (the tenant-identity absorption into platform.tenant_profile) are untouched. See docs/database/schema_docs/approvals.md for the current, full column-level definition of all 8 tables.
admin.custom_field_definition (12 cols, human-only) — Remediation Phase 4, Item 19 (2026-07-08)
NEW table, added Remediation Phase 4 (PROJECT_DECISIONS #40). A tenant-defined custom-field registry governing the 7 confirmed-ungoverned attributes JSONB columns found live across the schema: crm.customer, inventory.item, inventory.item_variant, orders.order_header, purchasing.purchase_receipt, purchasing.vendor, purchasing.vendor_item. Before this table, any tenant wanting to extend one of those 7 entities with its own custom attribute had no registry of what fields exist, what type they hold, or whether they're required — just an ungoverned free-form JSONB blob. custom_field_definition lets a tenant declare (entity_type, field_key) pairs with a display name, description, declared value type, default value, and required/active flags.
Tenant-scoped — unlike setting_definition/Item 17's catalogs, which are Vrida-wide. Each tenant defines its own custom fields, so this table carries tenant_id NOT NULL, RLS enabled, and no soft-delete deleted_at (uses is_active instead, matching the catalog-table convention for lifecycle state).
Tenant-scoped. RLS enabled —
custom_field_definition_tenant_isolation. No soft delete — usesis_activeinstead.updated_at: trigger-maintained viaplatform.set_updated_at().
| Column | Type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
UUID | NOT NULL | gen_random_uuid() |
PK |
tenant_id |
UUID | NOT NULL | — | FK → platform.tenant |
entity_type |
text | NOT NULL | — | CHECK chk_custom_field_definition_entity_type — restricted to exactly 7 values (see below) |
field_key |
text | NOT NULL | — | The custom field's key within the target entity's attributes JSONB |
display_name |
text | NOT NULL | — | Human-readable label for the Owner Dashboard |
description |
text | nullable | — | Longer explanation of what the field captures |
field_type |
text | NOT NULL | — | CHECK chk_custom_field_definition_field_type IN (boolean,number,string,json,array,date) |
default_value |
jsonb | nullable | — | Default value if the entity row has not set an override |
is_required |
boolean | NOT NULL | false |
Whether the field must be populated |
is_active |
boolean | NOT NULL | true |
Catalog-entry lifecycle flag — no soft-delete column on this table |
created_at |
timestamptz | NOT NULL | now() |
|
updated_at |
timestamptz | NOT NULL | now() |
Trigger-maintained |
CHECK constraints (2): chk_custom_field_definition_entity_type — restricted to exactly 7 values: crm.customer, inventory.item, inventory.item_variant, orders.order_header, purchasing.purchase_receipt, purchasing.vendor, purchasing.vendor_item (the confirmed-ungoverned attributes JSONB columns codebase-wide, independently re-confirmed to be exactly 7, no more no fewer); chk_custom_field_definition_field_type — restricted to 6 value types (boolean,number,string,json,array,date).
Indexes: PK; tenant_id; UNIQUE custom_field_definition_tenant_entity_key_unique on (tenant_id,entity_type,field_key) WHERE is_active = true — a partial unique that allows re-registering a retired key (deactivate, then re-add under the same key).
Autonomy: human-only, explicitly ruled out — a custom-field DEFINITION is a tenant business-configuration decision, the same class of decision as approval_workflow/approval_routing_rule.
Not DB-enforced against the 7 target entities' attributes JSONB columns — a disclosed, deferred gap, the same shape as setting_definition's own non-seam against tenant_setting: a Postgres CHECK cannot validate another table's JSONB shape. Logged to OPEN_ITEMS.
Additive, zero risk. Brand-new table, zero rows at build time — zero backfill risk.
Migration: packages/db/migrations/20260709050000_phase4_item19_custom_field_definition.sql.
The seams
The Platform identity seam (service-layer read, no FK). Admin never stores tenant legal name, business type, addresses, DBAs, NAICS, or EIN — it reads them from PlatformService.getTenantProfile(tenantId) at render/config time. This is the standing convention: tenant identity is never looked up cross-schema by FK in this codebase. See PROJECT_DECISIONS #34/#35 and CROSS_MODULE_CONTRACTS.md's governing-rule note.
The Files forward-refs (unchanged from v1). tenant_branding.logo_ref and compliance_document.document_ref are plain-text object-key seams into files.file.id — no FK, Files module not built. Already a documented seam in CROSS_MODULE_CONTRACTS.md.
The vault-ref seams (unchanged from v1, dependency-blocked). integration_config.credentials_ref and webhook_config.secret_ref await a vault-encryption service that does not exist anywhere in the codebase yet (confirmed via grep — zero Vault/Encryption service classes in apps/api/src). This is the SAME net-new dependency platform.tenant_profile.ein_ref already depends on, not a second one.
The Integrations inbound seams (real, already-documented FK targets). integrations.connector.integration_config_id → admin.integration_config and integrations.webhook_delivery.webhook_config_id → admin.webhook_config — Integrations module not yet built, but Admin's own schema needs no placeholder for these; nothing incomplete on Admin's side.
The approval engine moved out of Admin (2026-07-09 reopen). approval_workflow/approval_routing_rule/approval_request — including the approval_request.source_ref polymorphic seam (no FK to pos.sale/purchasing.purchase_order/orders.order_header, discriminated by source_module/source_type) — are no longer part of this schema; they moved to the new approvals module. This REVERSES the prior "stays Admin-internal, Option B" call (PROJECT_DECISIONS #34 Section 5). See docs/database/schema_docs/approvals.md for the current definition, including the purchasing-convergence opportunity (now logged in that module's own OPEN_ITEMS). platform.contract's own separate Vrida-operator review gate (review_status/review_reason/reviewed_by_actor_id/reviewed_at) remains untouched and on a different plane regardless of this move — it never converged with the tenant-side approval engine, and still doesn't.
The config-catalog non-seam (Remediation Phase 3, disclosed, deferred). admin.setting_definition.(category, key) is NOT DB-enforced against admin.tenant_setting.(category, key) — no FK (a CHECK cannot reference another table), no validation trigger (deliberately deferred, not bundled into the catalog add). See the dedicated Remediation Phase 3 subsection below.
The legal-entity seam (Remediation Phase 4, Item 15). compliance_document.entity_id is a nullable FK → platform.legal_entity — one of the 10 header-table entity_id additions this phase rolled out codebase-wide (financial/legal-document-bearing HEADER tables only, never line items, a disclosed scoping rule). NULL means "the tenant's primary entity," today's implicit, unchanged behavior.
The integration-provider-catalog non-seam (Remediation Phase 4, Item 17d, disclosed, deferred). admin.integration_config.provider_id is a new, independently-nullable FK → admin.integration_provider_catalog, additive alongside the existing free-form integration_type CHECK-enum column. Not yet kept in sync with it — the same disclosed "additive interim step" pattern applied to POS tender types, payment terms, and tax jurisdiction levels in this same phase.
The custom-field-registry non-seam (Remediation Phase 4, Item 19, disclosed, deferred). admin.custom_field_definition.(entity_type, field_key) is NOT DB-enforced against the 7 target entities' attributes JSONB columns — a Postgres CHECK cannot validate another table's JSONB shape, the same limitation setting_definition already discloses against tenant_setting.
admin — Design Patterns Summary
Remediation Phase 1 (2026-07-08)
A cross-cutting senior-architect review pass added 1 new CHECK constraint to this module, closing a fail-open self-approval gap. No column/table count change (128 cols unchanged). Full cross-module record: PROJECT_DECISIONS #37.
admin.approval_requestgainedchk_approval_request_reviewer_not_creator— live-verified shape:CHECK (resolved_by_actor_id IS NULL OR requested_by_actor_id IS NULL OR resolved_by_actor_id <> requested_by_actor_id). Closes a gap where the same actor could both request and resolve (self-approve) an approval request.
Remediation Phase 3 (2026-07-08)
Item 13 of the cross-cutting Remediation Phase 3 pass added 1 brand-new table to this module: admin.setting_definition (12 cols) — the config-key catalog for admin.tenant_setting. Table count 10 → 11; column count 128 → 140. Full cross-module record: PROJECT_DECISIONS #39.
admin.setting_definition— global (not tenant-scoped) reference table registering valid(category, key)pairs, their declaredvalue_type, default value, and editability flags, closing the gap wheretenant_settinghad nothing to validate a category/key typo or wrong-shaped value against. Mirrorsidentity.permission/identity.agent_type_catalog's catalog-table precedent (is_activeflag, no tenant scoping, no RLS, no autonomy columns).- Scope is deliberately narrow: covers
tenant_settingonly.admin.integration_config.settings,admin.hardware_device.config,identity.agent_identity.config, and theaimodule's own config fields are explicitly OUT of scope — each is vendor-shaped or system/agent-managed, not a tenant-facing "setting." Logged to OPEN_ITEMS as a deliberate deferral. - Not DB-enforced against
tenant_setting— no FK (a Postgres CHECK cannot reference another table's columns) and no validation trigger (deliberately deferred rather than bundled into this catalog add). A disclosed, logged gap, not an oversight. - Additive, zero risk — brand-new table, zero rows at build time, no backfill.
- Migration:
packages/db/migrations/20260708260000_phase3_item13_setting_definition.sql. - Separately (documentation-only, no schema change): this same Item 13 pass also documented, for the first time, the canonical money-unit-suffix convention (
_cents/_minor_units/_millicents) indocs/database/SCHEMA_CONVENTIONS.md§8.1 — a cross-module convention, not an Admin-specific one; the authoritative text lives there, not duplicated here.
Remediation Phase 4 (2026-07-08)
Items 15, 17d, and 19 of the cross-cutting Remediation Phase 4 pass (the final phase of the 4-phase remediation plan) added 2 brand-new tables and 1 new column to this module. Table count 11 → 13; column count 140 → 161. Full cross-module record: PROJECT_DECISIONS #40.
admin.integration_provider_catalog(7 cols, Item 17d) — a global (not tenant-scoped) reference catalog of known integration providers, 6 seeded rows (quickbooks,stripe,mailchimp,twilio,resend,other). Mirrors the same additive-interim enum-to-catalog pattern applied to POS tender types, payment terms, and tax jurisdiction levels in the same phase (Items 17a–c):integration_config.integration_type(the existing free-form CHECK-enum) is left completely unchanged, and a new, independently-nullableintegration_config.provider_idFK is added alongside it — not yet kept in sync. Zero rows inintegration_configat build time, zero backfill risk.admin.custom_field_definition(12 cols, Item 19) — a tenant-scoped (unlike the Item 17 catalogs, which are Vrida-wide) custom-field 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 those 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 — brand-new table, zero rows, zero backfill risk.admin.compliance_document.entity_id(+1 col, Item 15) — nullable FK →platform.legal_entity, part of Item 15's 10-tableentity_idrollout (financial/legal-document-bearing HEADER tables that could plausibly differ per legal entity within one tenant — a disclosed scoping rule: header tables only, never line items). NULL = the tenant's primary entity, today's implicit, unchanged behavior. Zero rows incompliance_documentat build time, zero backfill risk.- Both new non-seams are disclosed, deferred gaps, not oversights:
integration_provider_catalogis not DB-enforced againstintegration_config.integration_type(no sync constraint yet), andcustom_field_definitionis not 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 same disclosed-gap shapesetting_definitionalready established againsttenant_settingin Phase 3. - All 3 changes purely additive — zero rows affected, zero backfill risk, zero
DROP TABLE/DROP COLUMNanywhere in this phase (independently confirmed by adversarial verification). - Migrations:
packages/db/migrations/20260709030000_phase4_item17_enum_to_catalog.sql(part d,integration_provider_catalog+integration_config.provider_id);packages/db/migrations/20260709050000_phase4_item19_custom_field_definition.sql(custom_field_definition);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' schemas).
Approvals-module extraction reopen (2026-07-09)
The 2026-07-09 reopen extracted admin.approval_workflow (10 cols), admin.approval_routing_rule (11 cols), and admin.approval_request (18 cols) — 3 tables / 39 cols — into a new, dedicated approvals module (8 tables / 97 cols total, including 5 net-new tables: approval_policy, approval_step, approval_delivery, approval_token, approval_event). This REVERSES PROJECT_DECISIONS #34 Section 5's "stays Admin-internal, Option B" call; Sections 1–4 of #34 (the tenant-identity absorption into platform.tenant_profile) are untouched. Table count 13 → 10; column count 161 → 122 (161 − 39 = 122, exact).
- See
docs/database/schema_docs/approvals.mdfor the new module's full column-level definition, autonomy design, and its own OPEN_ITEMS rows — including the purchasing/pricing/payments convergence opportunity this engine was designed to support, formerly logged here. - Migration:
packages/db/migrations/20260709070000_approvals_module.sql.
Column-count reconciliation
| Table | Cols | Table | Cols | |
|---|---|---|---|---|
tenant_branding |
12 | webhook_config |
12 | |
compliance_document |
15 | api_key |
14 | |
tenant_setting |
11 | custom_field_definition |
12 | |
setting_definition |
12 | |||
hardware_device |
13 | |||
integration_config |
14 | |||
integration_provider_catalog |
7 | |||
| Total | 122 |
Verified live via information_schema.columns, schema admin: 10 tables, 122 columns (following the 2026-07-09 approvals-extraction reopen; 13 tables/161 cols was the pre-reopen figure verified at Remediation Phase 4). 8 of the 10 tables are tenant-scoped and retain RLS + a <table>_tenant_isolation policy + set_updated_at trigger; setting_definition and integration_provider_catalog (both global, not tenant-scoped) have no RLS but do have the set_updated_at trigger, confirmed live. All 3 retargeted actor-attribution columns (tenant_setting.updated_by_actor_id, api_key.created_by_actor_id, api_key.revoked_by_actor_id) confirmed targeting identity.actor.
v1 → v2 reconciliation: 145 (v1, 11 tables) − 17 (tenant_business_profile, dropped entirely) = 128 (v2 first pass, 10 tables) + 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 end-state) − 39 (approval_workflow/approval_routing_rule/approval_request, moved to the new approvals module, 2026-07-09 reopen) = 122 (current, 10 tables) — exact.
Service layer
No AdminService yet — schema-only this pass, same as every module's deferred service layer.
JSONB columns
tenant_branding.social_handles, tenant_setting.value (shape per (category,key), service-layer-defined), setting_definition.default_value (shape matches the row's own value_type), hardware_device.config, integration_config.settings, webhook_config.event_subscriptions, api_key.scopes, custom_field_definition.default_value (shape matches the row's own field_type — Remediation Phase 4) — all v1 example shapes preserved verbatim, plus the setting_definition.default_value column (Phase 3) and custom_field_definition.default_value column (Phase 4). (approval_workflow.steps moved to the new approvals module, 2026-07-09 reopen; approval_request.step_history was dropped entirely in that move, superseded by approvals.approval_step.)
Open items carried forward (see OPEN_ITEMS.md)
Files forward-refs (tenant_branding.logo_ref, compliance_document.document_ref) — Files module not built; vault-encryption service — blocks integration_config.credentials_ref and webhook_config.secret_ref (same dependency as platform.tenant_profile.ein_ref); no AdminService yet; tenant_setting.value JSONB shape formalized only at service-layer implementation time; compliance_document.expires_at partial-index refinement (WHERE deleted_at IS NULL) deferred to when the expiry-alert job is written; (Remediation Phase 3) setting_definition is not yet DB-enforced against tenant_setting.(category, key) — no FK or validation trigger, deliberately deferred; the 4 other free-form config surfaces (integration_config.settings, hardware_device.config, identity.agent_identity.config, ai's own config fields) are deliberately OUT of this catalog's scope, not a gap; (Remediation Phase 4) integration_provider_catalog is not yet DB-enforced against integration_config.integration_type — no sync constraint, deliberately deferred (same disclosed shape as Phase 3's setting_definition gap); 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, deliberately 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 — a systemic gap in the Postgres role/grant model, not introduced by this phase, 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 — affects compliance_document.entity_id's eventual consumers). (2026-07-09 approvals reopen) the approval engine's own open items (purchasing/pricing/payments convergence, agent-as-approver enablement, token/step/delivery follow-ups, etc.) now live in the approvals module's own OPEN_ITEMS rows, not here — see docs/database/schema_docs/approvals.md.