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), and approval_request (18 cols) — 3 tables / 39 cols — MOVED out of admin 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). Admin drops from 13 tables / 161 cols to 10 tables / 122 cols (161 − 39 = 122, exact); the remaining 10 tables are unchanged in shape. See docs/database/schema_docs/approvals.md for 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_profile is the single source of truth for legal name, EIN, business type, addresses, DBAs, NAICS classification (PROJECT_DECISIONS #34). Every Admin table carries tenant_id → platform.tenant.id for 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 of PlatformService — 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_keytenant_business_profile dropped entirely at the 2026-07-07 build, approval_workflow/approval_routing_rule/approval_request since moved out to the new approvals module at the 2026-07-09 reopen) keep the same columns, types, CHECK constraints, and indexes as v1's schema_admin.md. The only schema-level change: the module-wide actor-attribution retarget from identity.identity_user to identity.actor, applied to the 3 columns that carry a *_by_user_id shape (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 NULL FK → platform.tenant, RLS enabled with a permissive <table>_tenant_isolation policy on current_setting('app.current_tenant_id')::uuid, FOR ALL TO authenticated, USING/WITH CHECK both scoped to tenant. Plain tenant_id index on all 10.
  • updated_at trigger-maintained via platform.set_updated_at() (shared, reused) on all 10 tables.
  • Soft delete (deleted_at) on all 10; every UNIQUE constraint is WHERE 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 new approvals module 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_branding is explicitly human-only: branding is a subjective creative/business decision with zero legitimate autonomy surface. No automation_source/review_status/decision_provenance columns 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_request no longer live in this schema — the 2026-07-09 reopen extracted them into a new, dedicated approvals module (8 tables / 97 cols). platform.contract's own separate Vrida-operator review gate remains untouched and on a different plane regardless of this move. See docs/database/schema_docs/approvals.md for 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_setting is branch 7 (the tenant's own tunable business-process rule — the default/fallback); admin.integration_config.settings is branch 5 (named 3rd-party connector config); admin.hardware_device.config is branch 6 (physical device config). Cite #34 for the full tree.
  • v1 baseline vs. this build — v1's schema_admin.md specified 11 tables / 145 cols. This build: 10 tables / 128 cols (145 − 17 = 128, exact). tenant_business_profile (17 cols) DROPPED entirely (16 cols MOVED to platform.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) and custom_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_config itself also gains provider_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 via platform.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 via platform.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 via platform.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.actorretargeted 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_active instead. updated_at: trigger-maintained via platform.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 via platform.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 via platform.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_active instead. updated_at: trigger-maintained via platform.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 via platform.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 via platform.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.actorretargeted from v1's identity.identity_user
created_by_actor_id UUID nullable FK → identity.actorretargeted 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 — uses is_active instead. updated_at: trigger-maintained via platform.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_request gained chk_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 declared value_type, default value, and editability flags, closing the gap where tenant_setting had nothing to validate a category/key typo or wrong-shaped value against. Mirrors identity.permission/identity.agent_type_catalog's catalog-table precedent (is_active flag, no tenant scoping, no RLS, no autonomy columns).
  • Scope is deliberately narrow: covers tenant_setting only. admin.integration_config.settings, admin.hardware_device.config, identity.agent_identity.config, and the ai module'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) in docs/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-nullable integration_config.provider_id FK is added alongside it — not yet kept in sync. Zero rows in integration_config at 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-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 those 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 — 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-table entity_id rollout (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 in compliance_document at build time, zero backfill risk.
  • Both new non-seams are disclosed, deferred gaps, not oversights: integration_provider_catalog is not DB-enforced against integration_config.integration_type (no sync constraint yet), and custom_field_definition is not 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 same disclosed-gap shape setting_definition already established against tenant_setting in Phase 3.
  • All 3 changes purely additive — zero rows affected, zero backfill risk, zero DROP TABLE/DROP COLUMN anywhere 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 (the compliance_document.entity_id ALTER, 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.md for 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.

Last modified: Jul 9, 2026, 4:45 PM PT
On this page
Esc