admin — Phase 6

Schema locked 2026-06-10. 11 tables, 145 cols: tenant_business_profile (17), 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).

Tenant configuration module only — business profile, branding, generic settings (absorbs config deferred from POS/Orders/Billing), compliance docs, hardware registry, integration/webhook/API config, and approval workflow definitions. HR/scheduling/labor is out of scope for Vrida. Roles/permissions/users live in identity. See PROJECT_DECISIONS "Admin Module Scope (Decided 2026-06-10)".

Design rules:

  • All 11 tables are tenant-scoped with RLS on tenant_id.
  • tenant_setting is the generic catch-all for all module config. site_id NULL = tenant-wide default; site_id SET = site-level override. Consuming modules call AdminService.getSetting(tenantId, category, key, siteId?).
  • Secrets stored as *_ref vault references only — never raw credentials.
  • File assets (logos, PDFs) stored as R2 text key forward-refs — Files module owns the storage layer.
  • Approval workflows are cross-module infrastructure. The engine routes; source records (pos.sale, purchasing.purchase_order) record outcomes. No change to locked schemas — source_ref is a plain UUID discriminated by source_module.

Cross-Phase FK seams:

Column References Status
*.tenant_id platform.tenant Locked — enforced
tenant_setting.site_id, hardware_device.site_id, approval_routing_rule.site_id multi_loc.site Locked — enforced (nullable)
*.updated_by_user_id, *.created_by_user_id, *.revoked_by_user_id, *.requested_by_user_id, *.resolved_by_user_id identity.identity_user Locked — enforced (nullable)
compliance_document.document_ref, tenant_branding.logo_ref Files module (R2 key) FORWARD-REF — plain text seam; FK added when Files module locks
integration_config.credentials_ref, webhook_config.secret_ref Vault — secrets Text seam — never enforced as FK
api_key.token_hash Hashed — stored token hash, raw shown once at creation Hash — never FK
approval_request.source_ref pos.sale.id / purchasing.purchase_order.id / orders.order_header.id — discriminated by source_module Plain UUID — cross-module; not enforced FK; consuming schemas are locked and unchanged

Deferred items (explicit triggers):

  • tenant_setting.value JSONB shape per (category, key) pair: documented at service layer when each consuming module reads the setting.
  • approval_workflow.steps JSONB step schema: formal validation deferred to implementation.
  • approval_request.step_history JSONB per-step audit: formal shape deferred to implementation.
  • tenant_business_profile.business_type CHECK enum vs. free-text: when tenant_business_profile service is built (currently open text with examples).
  • compliance_document.expires_at partial index (add WHERE deleted_at IS NULL): when expiry alert job is written.

admin.tenant_business_profile — 17 cols

One row per tenant. Holds the legal identity, operating addresses, contact info, and trade name list.

RLS: tenant-isolated on tenant_id.

Column Type Nullable Default Constraints / Notes
id UUID NOT NULL uuid_generate_v4() PK
tenant_id UUID NOT NULL FK → platform.tenant
created_at timestamptz NOT NULL now()
updated_at timestamptz NOT NULL now()
deleted_at timestamptz nullable Soft delete
legal_name text NOT NULL Legal business name
display_name text nullable Trade name / DBA used in customer-facing UI; falls back to legal_name if NULL
ein_ref text nullable Vault reference — encrypted EIN; never stored raw
business_type text nullable e.g. 'LLC', 'Corporation', 'SoleProprietorship', 'Partnership'
business_classification text nullable NAICS code or similar
primary_address JSONB nullable {"line1":"...","line2":"...","city":"...","state":"...","postal":"...","country":"..."}
mailing_address JSONB nullable Same shape as primary_address; NULL = same as primary
phone text nullable Primary business phone
email text nullable Primary business email
website text nullable
dbas JSONB nullable Array of doing-business-as names: ["Garden Supply Co","Rose Garden Nursery"]
attributes JSONB nullable Tenant-custom extensible fields

Indexes:

  • PK on id
  • on (tenant_id)
  • UNIQUE on (tenant_id) WHERE deleted_at IS NULL — one profile per tenant

admin.tenant_branding — 12 cols

One row per tenant. Logo, colors, fonts, slogan, and social handles for customer-facing theming.

RLS: tenant-isolated on tenant_id.

Column Type Nullable Default Constraints / Notes
id UUID NOT NULL uuid_generate_v4() PK
tenant_id UUID NOT NULL FK → platform.tenant
created_at timestamptz NOT NULL now()
updated_at timestamptz NOT NULL now()
deleted_at timestamptz nullable Soft delete
logo_ref text nullable FORWARD-REF — R2 object key; Files module owns storage
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"}

Indexes:

  • PK on id
  • on (tenant_id)
  • UNIQUE on (tenant_id) WHERE deleted_at IS NULL — one branding record per tenant

admin.compliance_document — 14 cols

Tenant's own business permits, insurance certs, and compliance documents. Expiry tracking for renewal alerts.

RLS: tenant-isolated on tenant_id.

Column Type Nullable Default Constraints / Notes
id UUID NOT NULL uuid_generate_v4() PK
tenant_id UUID NOT NULL FK → platform.tenant
created_at timestamptz NOT NULL now()
updated_at timestamptz NOT NULL now()
deleted_at timestamptz nullable Soft delete
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 — R2 object key for the PDF; Files module owns storage
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

Indexes:

  • PK on id
  • on (tenant_id)
  • on (document_type)
  • on (expires_at) — renewal alert sweeps

admin.tenant_setting — 11 cols

Generic key-value configuration store. One row per (tenant, category, key) pair, with an optional site-level override. Absorbs all settings deferred from POS (hold expiry, receipt config, discount-approval thresholds), Orders, Billing, and notification preferences.

RLS: tenant-isolated on tenant_id.

Column Type Nullable Default Constraints / Notes
id UUID NOT NULL uuid_generate_v4() PK
tenant_id UUID NOT NULL FK → platform.tenant
created_at timestamptz NOT NULL now()
updated_at timestamptz NOT NULL now()
deleted_at timestamptz nullable Soft delete
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', 'discount_approval_threshold_cents'
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_user_id UUID nullable FK → identity.identity_user — who last changed this setting

Indexes:

  • PK on id
  • on (tenant_id)
  • on (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; prevents duplicate tenant-wide settings (a single four-column unique on (tenant_id, category, key, site_id) cannot block two rows both with site_id=NULL — NULL ≠ NULL in Postgres)
  • 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

admin.hardware_device — 13 cols

Physical device registry per site. Records every register, printer, cash drawer, card reader, and scanner. POS register may reference a hardware_device for its card reader; this table is the physical inventory.

RLS: tenant-isolated on tenant_id.

Column Type Nullable Default Constraints / Notes
id UUID NOT NULL uuid_generate_v4() PK
tenant_id UUID NOT NULL FK → platform.tenant
created_at timestamptz NOT NULL now()
updated_at timestamptz NOT NULL now()
deleted_at timestamptz nullable Soft delete
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.)
status text NOT NULL 'active' CHECK IN ('active','inactive','maintenance')
last_seen_at timestamptz nullable Last heartbeat or diagnostic ping
note text nullable

Indexes:

  • PK on id
  • on (tenant_id)
  • on (site_id)
  • on (device_type)
  • UNIQUE on (tenant_id, site_id, name) WHERE deleted_at IS NULL

admin.integration_config — 13 cols

Per-tenant integration settings — enabled/disabled toggle, credentials vault reference, and sync schedule. Config only; runtime and connector code live in the Integrations or Payments modules.

RLS: tenant-isolated on tenant_id.

Column Type Nullable Default Constraints / Notes
id UUID NOT NULL uuid_generate_v4() PK
tenant_id UUID NOT NULL FK → platform.tenant
created_at timestamptz NOT NULL now()
updated_at timestamptz NOT NULL now()
deleted_at timestamptz nullable Soft delete
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; never raw
settings JSONB nullable Integration-specific options (field mappings, preferences, etc.)
sync_schedule text nullable Cron expression or named interval, e.g. '0 2 * * *'
last_sync_at timestamptz nullable When last sync completed
last_sync_status text nullable CHECK (last_sync_status IS NULL OR last_sync_status IN ('success','failed','partial'))
status text NOT NULL 'inactive' CHECK IN ('active','inactive','error')

Indexes:

  • PK on id
  • on (tenant_id)
  • on (integration_type)
  • UNIQUE on (tenant_id, integration_type) WHERE deleted_at IS NULL — one config row per integration per tenant

admin.webhook_config — 12 cols

Tenant-configured outbound webhook endpoints. Delivery runtime lives in the Integrations module.

RLS: tenant-isolated on tenant_id.

Column Type Nullable Default Constraints / Notes
id UUID NOT NULL uuid_generate_v4() PK
tenant_id UUID NOT NULL FK → platform.tenant
created_at timestamptz NOT NULL now()
updated_at timestamptz NOT NULL now()
deleted_at timestamptz nullable Soft delete
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; never stored raw
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'))

Indexes:

  • PK on id
  • on (tenant_id)
  • on (is_active)

admin.api_key — 14 cols

Tenant API keys for external integrations or third-party access (Enterprise tier). Stored hashed — raw value shown once at creation.

RLS: tenant-isolated on tenant_id.

Column Type Nullable Default Constraints / Notes
id UUID NOT NULL uuid_generate_v4() PK
tenant_id UUID NOT NULL FK → platform.tenant
created_at timestamptz NOT NULL now()
updated_at timestamptz NOT NULL now()
deleted_at timestamptz nullable Soft delete
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
revoked_at timestamptz nullable When the key was revoked; NULL = active
revoked_by_user_id UUID nullable FK → identity.identity_user
created_by_user_id UUID nullable FK → identity.identity_user

Indexes:

  • PK on id
  • on (tenant_id)
  • UNIQUE on (token_hash) WHERE deleted_at IS NULL — inbound key validation lookup
  • on (key_prefix) — display listing
  • on (expires_at) WHERE expires_at IS NOT NULL AND deleted_at IS NULL AND revoked_at IS NULL — expiry sweep

admin.approval_workflow — 10 cols

Configurable approval chain definition. Each workflow type has one active definition per tenant; the steps JSONB defines the ordered sequence of approvers and conditions.

RLS: tenant-isolated on tenant_id.

Column Type Nullable Default Constraints / Notes
id UUID NOT NULL uuid_generate_v4() PK
tenant_id UUID NOT NULL FK → platform.tenant
created_at timestamptz NOT NULL now()
updated_at timestamptz NOT NULL now()
deleted_at timestamptz nullable Soft delete
name text NOT NULL Human label, e.g. "Large PO Approval"
workflow_type text NOT NULL CHECK IN ('po_approval','discount_approval','refund_approval','other')
is_active boolean NOT NULL true Only one active workflow per (tenant, workflow_type) — DB-enforced via partial unique (see indexes)
steps JSONB NOT NULL Ordered approval steps: [{"step":1,"approver_role":"manager","condition":"amount_cents > 50000"},...]
description text nullable

Indexes:

  • PK on id
  • on (tenant_id)
  • on (workflow_type)
  • UNIQUE on (tenant_id, name) WHERE deleted_at IS NULL
  • UNIQUE on (tenant_id, workflow_type) WHERE is_active = true AND deleted_at IS NULL — one active workflow per type per tenant; DB-enforced

admin.approval_routing_rule — 11 cols

Routes a workflow trigger to the correct approval_workflow based on type, amount threshold, and optional site. Multiple rules per type allowed; priority breaks ties.

RLS: tenant-isolated on tenant_id.

Column Type Nullable Default Constraints / Notes
id UUID NOT NULL uuid_generate_v4() PK
tenant_id UUID NOT NULL FK → platform.tenant
created_at timestamptz NOT NULL now()
updated_at timestamptz NOT NULL now()
deleted_at timestamptz nullable Soft delete
approval_workflow_id UUID NOT NULL FK → admin.approval_workflow
workflow_type text NOT NULL CHECK IN ('po_approval','discount_approval','refund_approval','other')
threshold_cents bigint nullable Amount above which this rule applies; NULL = applies always
site_id UUID nullable FK → multi_loc.site; NULL = tenant-wide rule
priority integer NOT NULL 0 Higher value = higher priority when multiple rules match
is_active boolean NOT NULL true

Indexes:

  • PK on id
  • on (tenant_id)
  • on (workflow_type)
  • on (approval_workflow_id)

admin.approval_request — 18 cols

In-flight and resolved approval instances. The engine routes; the source record (pos.sale, purchasing.purchase_order) records the outcome — no changes to locked schemas.

RLS: tenant-isolated on tenant_id.

Column Type Nullable Default Constraints / Notes
id UUID NOT NULL uuid_generate_v4() PK
tenant_id UUID NOT NULL FK → platform.tenant
created_at timestamptz NOT NULL now()
updated_at timestamptz NOT NULL now()
deleted_at timestamptz nullable Soft delete
approval_workflow_id UUID NOT NULL FK → admin.approval_workflow
workflow_type text NOT NULL CHECK IN ('po_approval','discount_approval','refund_approval','other')
source_module text NOT NULL CHECK IN ('purchasing','pos','orders','other')
source_type text NOT NULL e.g. 'purchase_order', 'sale_discount', 'sale_refund'
source_ref UUID NOT NULL The record needing approval — pos.sale.id / purchasing.purchase_order.id / etc.; plain UUID, cross-module by source_module discriminator
amount_cents bigint nullable The amount that triggered approval routing
status text NOT NULL 'pending' CHECK IN ('pending','approved','rejected','cancelled')
current_step integer NOT NULL 1 Which step in the workflow is currently awaiting action
requested_by_user_id UUID nullable FK → identity.identity_user — who triggered the approval request
resolved_by_user_id UUID nullable FK → identity.identity_user — who made the final decision
resolved_at timestamptz nullable When status reached 'approved'/'rejected'/'cancelled'
resolution_note text nullable Approver comment
step_history JSONB nullable Per-step audit: [{"step":1,"approver_id":"uuid","decision":"approved","at":"...","note":"..."},...]

Indexes:

  • PK on id
  • on (tenant_id)
  • on (status) WHERE status = 'pending' — pending-approval queue
  • on (source_module, source_ref) — look up approval state for a given source record
  • on (approval_workflow_id)

Column counts: tenant_business_profile(17) + 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) = 145


Last modified: Jun 17, 2026, 8:29 PM PT
On this page
Esc