Module Index

Cross-reference of all modules, their schemas, table/column counts, lock dates, ownership summary, and dependencies.

The navigable catalog of all modules: schema, table/column counts, ownership, and dependencies.


Authoritative Inventory override — Stock Transfer schema lock, 2026-07-16: the Inventory row is 29 base tables / 425 base-table columns / 1 view. Transfer contributes exactly 3 tables / 63 columns / 28 CHECKs / 20 FKs / 11 triggers. Consequential Transfer wrappers are built but dormant to every runtime role; no Transfer service or application wiring exists. Historical count narratives retained in the long row below are superseded by this override.

Authoritative Multi-Location companion override — 2026-07-16: multi_loc remains 1 table / 36 columns. The long row's old “Transfers deferred to v1.5” wording is superseded: Inventory now owns the schema-only Transfer capability, while Multi-Location supplies tenant-safe site parents and the nonterminal site soft-delete guard. Runtime orchestration remains deferred.

Locked Schemas — 26 schemas · 233 tables · 3,466 cols (as of 2026-07-06; tax added 2026-07-07 as a genuinely new 24th schema; approvals added 2026-07-09 as a genuinely new 25th schema — net +5 tables/+58 cols: +8 tables/+97 cols for the new schema, −3 tables/−39 cols from the same-day admin reopen that moved 3 tables out to it; receiving added 2026-07-10 as a genuinely new 26th schema — net 0 tables/+5 cols: +2 tables/+62 cols for the new schema, −2 tables/−57 cols from the same-day purchasing reopen that moved 2 tables out to it, see below)

⚠️ Known header/row discrepancy (flagged 2026-06-30, not yet resolved): summing the 23 rows below gives 233 tables / 3,467 cols, not 228/3,389 (updated 2026-07-06 for the crm build — first product module; crm was already one of the 23 rows, holding a stale v1-carryover placeholder of 8 tables/108 cols, so this is a +5 tables/+81 cols delta to an existing row, not a new schema — and previously for the agent_duty_grant build — identity +1 table/+25 cols, PROJECT_DECISIONS #22 — and the autonomy-first backfill across platform/identity/shared/multi_loc; none of these is a discrepancy-causing event, the underlying gap itself is unchanged), and now for the inventory build — biggest module, first real shared.plant consumer; inventory was already one of the 23 rows, holding the v1-derived 21 tables/238 cols baseline, so this is a +3 tables/+97 cols delta to an existing row, not a new schema — and now for the ai build — the agent-runtime schema layer; ai was already one of the 23 rows, holding the v1-derived 4 tables/71 cols baseline, so this is a +3 tables/+47 cols delta to an existing row, not a new schema — and now for the pricing build — the first module of the sell path; pricing was already one of the 23 rows, holding the v1-derived 4 tables/56 cols baseline, so this is a +0 tables/+19 cols delta to an existing row, not a new schema — and now for the pos build — the only offline-first module; pos was already one of the 23 rows, holding the v1-derived 19 tables/280 cols baseline, so this is a -10 tables/-142 cols delta to an existing row, not a new schema — and now for the 2026-07-07 crm/inventory/pricing erosion-audit reopen (PROJECT_DECISIONS #28): inventory +0 tables/+1 col (stock_reservation.created_by_actor_id), pricing +0 tables/+3 cols (price_rule.rule_kind/.name, price_list_assignment.is_active), crm +0 tables/+0 cols (constraint/nullability/default changes only, no new columns) — a +0 tables/+4 cols delta across 2 existing rows, not new schemas — and now for the orders build (module #15, PROJECT_DECISIONS #29) — completes the sell path; orders was already one of the 23 rows, holding a stale v1-carryover placeholder of 7 tables/133 cols, never a real v2 build, so this is a +0 tables/+40 cols delta to an existing row, not a new schema — and now for the purchasing build (module #16, PROJECT_DECISIONS #30) — the BUY path; purchasing was already one of the 23 rows, holding the v1-derived 16 tables/341 cols baseline, so this is a +0 tables/+57 cols delta to an existing row, not a new schema — and now, genuinely differently, for the tax build (module #17, PROJECT_DECISIONS #31): tax was NOT one of the original 23 rows — v1 never had a Tax module at all (confirmed via find docs/old -iname "*tax*" returning zero results; tax was assumed 100% outsourced to Stripe Tax). This is the first TRUE +1 schema / +2 tables / +37 cols addition in this entire discrepancy-tracking history — every prior entry was a delta to a pre-existing row — and now for the billing build (module #18, PROJECT_DECISIONS #32) — the SETTLE half of the financial layer, built same day immediately after tax; billing was already one of the 23 original rows, holding a real, locked v1 baseline of 8 tables/113 cols (not a stale placeholder — v1's own Billing module was fully designed), so this is a +1 table/+49 cols delta to an existing row (the NEW ar_adjustment table + the autonomy pack + the tax seam), not a new schema. Root-caused to the document's original authoring commit — the gap predates and is unrelated to any Platform/Identity/Shared/Multi-Location/CRM/Inventory/AI/Pricing/POS/Orders/Purchasing/Tax/Billing edit (all twelve are independently verified correct against the live DB). No live DB or per-module schema doc exists for the other 12 modules to determine whether a specific row or the header itself is the error. Summing the (still 24, since billing added rows not a schema) rows gives 226 tables / 3,531 cols — and now for the 2026-07-08 Remediation Phase 1 pass (PROJECT_DECISIONS #37, cross-cutting CHECK/RLS/trigger hardening across 10 modules): only inventory gained a column (stock_count.reconciled_at), a +0 tables/+1 col delta to an existing row — every other Phase 1 change (purchasing, ai, identity, pricing, payments, tax, billing, admin, platform) is CHECK/RLS/GRANT/trigger-only, zero column-count impact. Summing now gives 226 tables / 3,532 cols — and now for the 2026-07-08 Remediation Phase 2 pass (PROJECT_DECISIONS #38, UUIDv7 PK defaults on 25 append-only tables + the payments vendor-abstraction anchor): only payments gained a column (payment_intent.processor), a +0 tables/+1 col delta to an existing row — every other Phase 2 change (ai, billing, crm, identity, inventory, platform, pos, pricing, tax) is a PK-DEFAULT-generation-strategy change only (UUIDv4 → UUIDv7), zero column-count impact. Summing now gives 226 tables / 3,533 cols — and now for the 2026-07-08 Remediation Phase 3 pass (PROJECT_DECISIONS #39, missing capabilities across 6 modules): admin gained 1 table (setting_definition, +12 cols — the config-key catalog), identity gained +4 cols (agent_identity.status/suspended_at/suspended_by_actor_id/suspension_reason — the agent kill-switch), crm gained +2 cols (customer.credit_limit_cents/.credit_terms — the crm↔billing orphan fix), tax gained +2 cols (tax_calculation.calculation_type/.reversed_calculation_id — refund tax reversal), pos gained +5 cols (sale.sale_number +1, sale_refund_line.tax_amount_cents/.tax_rate/.item_variant_id +3, sale_refund.tax_refunded_amount_cents +1) — a +1 table/+25 cols delta across 5 existing rows, not a new schema; inventory's own Phase 3 change (stock_movement.movement_type widened to add 'produced') is CHECK-only, zero column-count impact. Summing now gives 227 tables / 3,558 cols — and now for the 2026-07-08 Remediation Phase 4 pass (PROJECT_DECISIONS #40, futureproofing across 11 modules, the final phase of the remediation plan): platform gained 3 tables (accounting_period/legal_entity/outbox, +32 cols total incl. 2 entity_id spot-cols), admin gained 2 tables (integration_provider_catalog/custom_field_definition, +21 cols), pos gained 1 table (tender_type_catalog, +17 cols incl. the anonymous-return CHECK relax + fiscal-period flag-trigger cols), tax gained 1 table (jurisdiction_level_catalog, +8 cols), shared gained 2 tables (exchange_rate/payment_terms_catalog, +28 cols — of which +16 is Phase 4's own addition and +12 is a disclosed drive-by correction of a pre-existing, Phase-4-unrelated stale baseline that never reflected entry #21's own FIX 1 review-seam addition), billing/purchasing/orders/crm/ai/inventory gained 0 tables each (entity_id/payment_terms_id spot-columns, the agent_memory GDPR seam, and inventory's stock.last_movement_id + its first-ever CREATE VIEW, which doesn't count toward the table total) for +2/+4/+1/+2/+3/+1 cols respectively — a +9 tables/+119 cols delta across 11 existing rows, not new schemas. Summing now gives 236 tables / 3,677 cols — and now for the 2026-07-09 operator identity build (PROJECT_DECISIONS #45): identity gained 2 tables (operator/operator_role_assignment, +22 cols) — a +2 tables/+22 cols delta to an existing row, not a new schema. Summing now gives 238 tables / 3,699 cols — and now for the 2026-07-10 Header/Line Remediation second batch: inventory +1 table/+13 cols (fixes #5/#9, PROJECT_DECISIONS #49), orders +0 tables/+1 col (fix #6, PROJECT_DECISIONS #50), billing +1 table/+12 cols (fix #2, PROJECT_DECISIONS #51), and now identity +1 table/+7 cols (fix #3 — invitation_site_assignment +1 table/+6 cols, user_site_assignment.created_from_invitation_site_assignment_id +1 col, PROJECT_DECISIONS #52) — a +3 tables/+33 cols delta across 4 existing rows, not new schemas — and now for the 2026-07-10 Header/Line Remediation batch 2's closing bare-FK fixes (PROJECT_DECISIONS #53): platform/purchasing/billing each had exactly one FK upgraded from bare to composite (payment.invoice_id, vendor_invoice_match.vendor_invoice_line_id, ar_charge.tax_calculation_id respectively) — constraint-shape changes only, +0 tables/+0 cols across all 3 rows. Summing remains 241 tables / 3,732 cols. This closes the entire 2-batch Header/Line Remediation effort (batch 1: PROJECT_DECISIONS #46-48; batch 2: PROJECT_DECISIONS #49-53). See OPEN_ITEMS for the full trace. The 2026-07-10 Receiving extraction (PROJECT_DECISIONS #55) adds a genuinely new receiving schema (+2 tables/+62 cols — goods_receipt 33 cols + goods_receipt_line 29 cols, moved+renamed from purchasing.purchase_receipt/.purchase_receipt_line's own 30+27 cols) split out of purchasing (−2 tables/−57 cols, now 15 tables/353 cols — every remaining table's own column count is unchanged), closing Purchasing's own long-named fix #7 (movement-line linkage) and fix #11 (over-receipt tolerance) gaps. inventory is unaffected at the row level (still 25 tables/351 cols — item_variant/lot/stock_movement/stock_movement_line each gained UNIQUE(id, tenant_id) only, the prerequisite for receiving's own new composite FKs). Net: 0 tables, +5 cols. Summing now gives 241 tables / 3,737 cols. The 2026-07-11 Consumer Layer build (PROJECT_DECISIONS #56) is the same shape of event as the tax/crm/orders/purchasing precedents above — consumer/rewards/offers were already 3 of these 26 rows, each holding a stale v1-carryover placeholder (4/47, 6/89, 4/68 — 14 tables/204 cols combined) never reconciled against a real v2 build; the real build is 22 tables/321 cols (consumer 10/104, rewards 6/102, offers 6/115), a +8 tables/+117 cols delta to those 3 existing rows, not new schemas. pos.sale also gained UNIQUE(id, tenant_id) as this build's own prerequisite (constraint-only, no column/table impact on pos). Summing now gives 249 tables / 3,854 cols. The 2026-07-11 Files build (PROJECT_DECISIONS #58/#59) is the same shape of event as the tax/crm/orders/purchasing/Consumer-Layer precedents above — files was already one of these 26 rows, holding a stale v1-carryover placeholder (3 tables/43 cols) locked as a design in 2026-06-11 but never actually migrated; the real build is 6 tables/89 cols, a +3 tables/+46 cols delta to that existing row, not a new schema. files.file also gained UNIQUE(id, tenant_id) as a green-field prerequisite from this build's own day one (a first for this codebase — every prior module needing this constraint added it via a later reopen). Summing now gives 252 tables / 3,900 cols. The 2026-07-11 rewards/offers proportional-clawback reopen (PROJECT_DECISIONS #60, same day as both modules' own initial lock) is a delta to 2 existing rows, not a new schema: rewards +1 table/+7 cols (new loyalty_point_ledger_reversal_tracker, closing the exact-negation-only limitation with a genuine proportional/partial-reversal capability, the cumulative reversed amount capped atomically) and offers +1 table/+8 cols (new offer_redemption_reversal_tracker, closing a REAL BUG — check_and_sync_offer_budget() had no magnitude check on a reversal, so a $10 redemption could be "reversed" by a fabricated $1000 row — proportional reversal now works correctly with the same cumulative cap). Of offers' own +8 cols, +7 is the new tracker table and +1 is a disclosed, this-fix-unrelated drift: offers.offer is live-confirmed at 43 cols, not the 42 this document's own offers row stated — the extra column, redemption_count, was added by the original 2026-07-11 build's own same-day lock-gate fix pass (PROJECT_DECISIONS #57) and never reflected in this row's count at the time, disclosed here rather than silently folded in, the same disclosure convention as shared's own 12-column Phase 4 drift above. A +2 tables/+15 cols delta across 2 existing rows, not new schemas. Summing now gives 254 tables / 3,915 cols. The 2026-07-11 returns module build (PROJECT_DECISIONS #61, module #27) is the same shape of event as tax's own #31 entry above, not the crm/orders/purchasing/Consumer-Layer/Files-shaped delta-to-an-existing-row event — returns was never one of these rows at all (no v1 precedent, no stale placeholder to reconcile), so this is a TRUE +1 schema / +9 tables / +152 cols addition: return_authorization (29), return_authorization_line (28), return_source_line_tracker (10, NEW), return_resolution (21), return_resolution_line (8), return_receipt (14), return_receipt_line (16), return_reason (10), warranty (16, revived v1 pos.guarantee). The same migration bundled 5 companion reopens — pos (sale_refund/.sale_refund_line gain UNIQUE(id, tenant_id)), orders (order_header/.order_line gain UNIQUE(id, tenant_id)), inventory (stock_movement.source_module CHECK widened for 'returns'), approvals (approval_request.source_module CHECK widened for 'returns'), files (attachment.entity_type CHECK widened for 'return_authorization') — each constraint/CHECK-shape only, +0 tables/+0 cols across all 5 existing rows. Summing now gives 263 tables / 4,067 cols. The 2026-07-12 Platform reopen (Phase 1 of a 6-phase agents-v2/v3 build, PROJECT_DECISIONS #62) is the same shape of event as the billing/Header-Line-Remediation precedents above — a delta to platform's own existing row, not a new schema: +7 tables/+64 cols (module_catalog, tier_module_entitlement, tenant_module_activation, module_dependency, ai_capacity_policy, tenant_ai_capacity_usage, tenant_regional_policy — 8+7+10+5+10+10+14 cols respectively), zero existing tables altered. Summing now gives 270 tables / 4,131 cols. The 2026-07-13 ai reopen (Phase 2 of the same 6-phase agents-v2/v3 build, PROJECT_DECISIONS #63) is the same shape of event as the billing/Header-Line-Remediation/Platform-Phase-1 precedents above — a delta to ai's own existing row, not a new schema: +42 tables/+106 cols (14 new table types contributing +102 cols — provider_registry/model_family/model_version/model_deployment+4 satellites/model_deployment_status_observation/prompt_definition/_version/prompt_model_compatibility/routing_policy/agent_memory_source — plus 28 new partition-child tables from partitioning agent_execution/agent_memory BY RANGE(created_at) contributing 0 additional cols, since a partition inherits its parent's columns, plus agent_execution +1 col/agent_memory +3 cols), zero tables dropped. Disclosed count note: PROJECT_DECISIONS #63's own summary sentence says "13 new tables" against an actual 14-item enumeration (independently cross-checked against the Drizzle schema files and the migration's own CREATE TABLE statements) — a pre-existing off-by-one in that entry's own summary, not corrected here (out of this docs-only pass's scope); see docs/database/schema_docs/ai.md's own disclosure note for the full accounting. Summing now gives 312 tables / 4,237 cols. The 2026-07-14 semantics build (Phase 3 of the same 6-phase agents-v2/v3 build authorization, PROJECT_DECISIONS #64) is the same shape of event as tax's own entry above, not a delta to an existing row — semantics was never one of these rows at all (no v1 precedent, no stale placeholder to reconcile, confirmed: v1 had no semantic-layer concept of any kind), so this is a TRUE +1 schema / +14 tables / +114 cols addition: approved_function_registry (11), metric_definition (6), metric_version (8), tenant_metric_binding (12), metric_dependency (4), entity_definition (6), entity_alias (5), dimension_definition (5), goal_definition (8), constraint_definition (7), tenant_goal_binding (15), tenant_constraint_binding (15), attribution_model_definition (4), attribution_model_version (8). Zero companion reopens bundled into this migration — no other module's table/column count moves. Summing now gives 326 tables / 4,351 cols. The 2026-07-15 signals build (Phase 4 of the same 6-phase agents-v2/v3 build authorization, this build's own explicitly-designated HIGHEST-RISK phase, PROJECT_DECISIONS #65) is the same shape of event as tax's/returns'/semantics's own entries above, not a delta to an existing row — signals was never one of these rows at all (no v1 precedent, no stale placeholder to reconcile, confirmed: v1 had no feature-store/forecast/anomaly/experimentation concept of any kind), so this is a TRUE +1 schema / +11 tables / +111 cols addition: feature_definition (6), feature_version (7), experiment (6), experiment_version (9), feature_value (10, PARTITIONED weekly by recorded_at), forecast (11, PARTITIONED weekly), anomaly_score (10, PARTITIONED weekly), experiment_assignment (19), experiment_exposure_event (5), outcome_observation (19, PARTITIONED monthly by created_at), outcome_authority (9, the split-authority pattern, deliberately unpartitioned). Zero companion reopens bundled into this migration — no other module's table/column count moves. Summing now gives 337 tables / 4,462 cols. The 2026-07-16 agents build (Phase 5 of the same 6-phase agents-v2/v3 build authorization, this build's own explicitly-designated module the entire authorization exists to build, PROJECT_DECISIONS #66) is the same shape of event as tax's/returns'/semantics's/signals's own entries above, not a delta to an existing row — agents was never one of these rows at all (no stale placeholder to reconcile; v1's own 21-table agent-orchestration baseline predates this reconciliation-tracking effort entirely and was never itself one of the original rows), so this is a TRUE +1 schema / +47 tables / +475 cols addition: the full v1 orchestration baseline (agent_task, agent_thread, agent_event_log, agent_schedule, agent_trigger, agent_eval_suite+_version, agent_eval_run, agent_eval_case, agent_performance_profile, agent_shadow_run, agent_shadow_decision, agent_autonomy_profile, agent_action, agent_decision [partitioned], rollback_recipe, rollback_execution, agent_tool_grant, agent_catalog_entry+_required_tool, tenant_agent_deployment, agent_incident) merged with v2's A1–A8 amendments and v3's BLOCKER1–8/I1–I8 corrections, organized across 12 Drizzle files (_schema.ts, tool.ts, skill.ts, marketplace.ts, task.ts, eval.ts, shadow.ts, certification.ts, decision_context.ts, action.ts, kill_switch.ts, policy.ts) — tool_catalog (v1) is SUPERSEDED by the new tool_definition/tool_version version-row pattern (zero live v1 rows), not carried forward as a 22nd v1 table. See the Agents row below for the full concern-group breakdown. This same migration also grows platform's own existing row by a further +1 table/+4 cols (polymorphic_target_registry, I6) — a delta to an existing row, not part of the new-schema addition; platform is now 35 tables/518 cols (up from 34/514), itself already lagging this document's own stale 23-table baseline in the running-total breakdown above, per the same disclosed-lag convention used throughout this footnote. Summing now gives 384 tables / 4,937 cols (the agents new-schema addition), plus the platform delta's own +1/+4 = 385 tables / 4,941 cols. The 2026-07-18 Phase 2 Nursery Vertical Extraction (PROJECT_DECISIONS #70) adds 2 new schemas: nursery_ref (+4 tables/+61 cols, moved verbatim out of shared's own existing row via ALTER TABLE ... SET SCHEMA) and nursery (+2 tables/+18 cols, a wholly new tenant-scoped extension with no v1 antecedent). shared's own row loses the same 4 tables/61 cols it hands to nursery_ref (net 0 across that pair, since the same physical tables simply changed schema), so the only genuinely new contribution to this running total is nursery's own +2 tables/+18 cols, alongside inventory (−1 col, item.plant_id dropped) and multi_loc (−1 col, site.climate_zone_code dropped); net 385→387 tables / 4,941→4,957 cols. Disclosed reconciliation flag: this 385/4,941 baseline itself does not yet include the 2026-07-17 identity Phase 6 drop (−2 tables, PROJECT_DECISIONS #67) or the 2026-07-18 Security & Integrity Remediation Phase 1 platform gain (+1 table/+6 cols, PROJECT_DECISIONS #68) — both post-date this paragraph's own last update (the agents build, #66) and were never folded back into this running sum; a pre-existing gap, not introduced or corrected by this edit. The 2026-07-18 Phase 3 Gift Card + Store Credit build (PROJECT_DECISIONS #71) is the same shape of event as the billing/Header-Line-Remediation precedents above — a delta to 2 existing rows, not a new schema: billing +6 tables/+84 cols (10→16 tables, 176→260 cols — gift_card 25, gift_card_transaction 14, gift_card_reversal_tracker 7, store_credit_account 17, store_credit_transaction 14, store_credit_reversal_tracker 7; the accompanying stored_value_liability VIEW doesn't count toward the table total, matching inventory.stock_reconciliation_shell's own precedent) and returns +0 tables/+1 col (return_resolution.store_credit_transaction_id, 152→153); the same migration's pos and crm companion reopens are constraint-shape only (real composite FKs replacing pos's gift_card/store_credit forward-refs, the fail-closed tender gate narrowed to 'reward'-only, and 2 prerequisite UNIQUE(id, tenant_id) constraints — zero table/column impact on either row). Summing now gives 393 tables / 5,042 cols.

Platform & Service Layer

Module Schema Tables Cols Owns Depends on
Platform platform 34 514 Vrida's control plane: tenant identity, SaaS subscriptions/billing, payments-to-Vrida, legal agreements, usage, onboarding, entitlements (source of truth for feature access), platform-wide announcements + settings, fiscal periods, legal entities, transactional outbox, module registry, AI capacity/regional policy. +2 skeleton tables 2026-06-30 (announcement, platform_setting) — schema locked, read-only service stubs, no write endpoint yet; see PROJECT_DECISIONS #15. Reopened 2026-07-06 for the autonomy-first backfill (actor attribution/review/provenance pattern, no new tables) — see PROJECT_DECISIONS #19. Reopened a 4th time 2026-07-07 for tenant-identity absorption from admin.tenant_business_profile (executes the ownership rule in PROJECT_DECISIONS #34: platform owns tenant identity, admin owns tenant technical/operational config): tenant_profile 34→39 cols (+5: business_email, legal_address, mailing_address, business_classification_code, ein_ref); trading_name (text) retyped to dbas (jsonb array, NOT NULL DEFAULT '[]', e.g. ["Acme Garden Co"] — confirmed zero non-null rows live before retype, zero risk). tax_id and logo_url DEPRECATED IN PLACE (column comments only, superseded by ein_ref and future admin.tenant_branding.logo_ref). Module-wide 403→408 cols (tables unchanged at 23). See PROJECT_DECISIONS #34 (the rule) and #35 (this build). Reopened a 5th time 2026-07-08, Remediation Phase 4 Items 14/15/20a (PROJECT_DECISIONS #40): +3 tablesaccounting_period (9 cols, EXCLUDE USING gist overlap prevention — this codebase's first use of btree_gist; a FLAG-NOT-REJECT trigger, not a hard block, since offline-sync needs a late-arriving sale to still land), legal_entity (8 cols, 1:N from tenant, backfilled 1 primary row per tenant, partial-unique enforcing exactly one primary per tenant), outbox (13 cols, a durable transactional-outbox event table, genuinely mutable — gen_random_uuid() PK, not uuid_generate_v7()). Plus nullable entity_id on contract/billing_account (+1 col each) — 2 of 10 tables across the codebase that gained this column (see the Admin/Tax/Billing/Purchasing/Orders/POS rows below for the other 8). 26 tables / 440 cols (408→440, +32). Reopened again 2026-07-10 for Header/Line Remediation fix #1 (PROJECT_DECISIONS #48) — the 3rd and final reopen of this effort (POS → Purchasing → Platform): new table subscription_invoice_line (10 cols, per-line decomposition of a subscription_invoice's total, write-once, PK platform.uuid_generate_v7()), reconciled LINES-ARE-TRUTH via a new trigger, trg_subscription_invoice_line_sync_totals (the opposite pattern from Purchasing's own header-is-truth vendor_credit_line) — required a new prerequisite UNIQUE(id, tenant_id) on subscription_invoice for the line table's composite FK. Design-phase verification caught and corrected a genuine migration-sequencing bug (installing the sync trigger before backfill+verify would have made the reconciliation check tautological) — the corrected 8-step order snapshots pre-backfill totals first and installs the trigger only after backfill+verification. Pre-migration inspection also disclosed that all 5 pre-existing live invoices had an empty line_items blob despite non-zero subtotal_cents — a genuine pre-existing data-quality gap, logged not silently fixed; line_items is now deprecated in place. 27 tables / 450 cols (440→450, +10). Reopened a 6th time 2026-07-12 for Phase 1 of a 6-phase agents-v2/v3 build (PROJECT_DECISIONS #62): +7 tables, 0 existing tables alteredmodule_catalog (A1, 8 cols — the global module registry, replacing the closed CHECK-enum module-tag pattern; lifecycle_status forward-only via a new trigger), tier_module_entitlement (7 cols — which modules a tier includes by default, replacing the dead tier_definition.entitled_modules JSONB), tenant_module_activation (10 cols — the REAL per-tenant module switch, replacing the functionally-dead is_toggleable mechanism, found to have zero callers), module_dependency (5 cols — module prerequisite graph, real recursive-CTE cycle detection + a reverse-dependency-on-deactivation guard), ai_capacity_policy (10 cols) / tenant_ai_capacity_usage (10 cols) (A8/E4 — the 6-level AI-capacity precedence chain's atomic enforcement half, platform.try_increment_ai_capacity_spend()), tenant_regional_policy (14 cols, E7 — one resolvable regional-placement policy per tenant). An independent lock-gate verification found and fixed 4 issues before Phase 2 was authorized to proceed: a live GRANT-additivity bug exposing the platform-wide AI cost ceiling to tenant-scoped writes (closed via explicit REVOKE), a refund/correction bypass in the spend-increment function (negative deltas now always succeed unconditionally), a vestigial priority tie-break column (now honored), and a module_catalog seed misclassification (multi_loc corrected to layer='business'). 34 tables / 514 cols (450→514, +64). Schema-only — no service-layer changes this phase. identity
Identity identity 38 429 Authorization layer: staff users (via polymorphic actor root), tenant membership, roles, permissions, role assignments, role templates, permission-group bundle wiring, site access, SSO config, invitations, invitation-time site-access staging, support-access grants, password policy, SCIM config, actor groups, permission-group bundles, SoD rules/violations, session tracking, access requests, machine identity (service accounts + API keys), AI agent identity (agent_type_catalog, agent_identity, agent_skill, agent_skill_assignment, agent_duty_grant), Vrida operator identity (operator, operator_role_assignment), per-tenant security policy. (Supabase Auth owns authN; Identity owns authZ.) Batch A LOCKED 2026-06-28 — Pass 1 (actor model) + Pass 2 (groups, role inheritance, permission_group bundles). Batch B Pass 1 LOCKED 2026-06-28 — role management core (role_assignment, role_permission_group, role_template, role_template_permission_group; tenant_user.role_id removed). Batch B Pass 2 LOCKED 2026-06-28 — governance, session, access management (sod_rule, sod_rule_permission, sod_violation, identity_session, access_request; +identity_access_event.session_id + event_type 14→23). ALL OF BATCH B COMPLETE. Batch C LOCKED 2026-06-28 — machine + AI-agent identity (agent_type_catalog, service_account, api_key, agent_identity, agent_skill, agent_skill_assignment + trg_tenant_user_actor_type_check, the schema's first trigger). Batch D LOCKED 2026-06-28 — tenant_security_policy (per-tenant session/MFA/concurrent-session/api-key-rotation config); consent_record NOT BUILT (no v1 gap — DR-29). IDENTITY SCHEMA DESIGN COMPLETE (A–D). 34 tables / 371 cols (post-2026-06-29 reconciliation + 2026-07-06 autonomy-first backfill). AGENT-DUTY-GRANT BUILT 2026-07-06 — the A5 agent-authority passport (+agent_duty_grant, +25 cols; PROJECT_DECISIONS #22) + identity_access_event.event_type 39→42. 35 tables / 396 cols. Reopened 2026-07-08, Remediation Phase 3 Item 12 (PROJECT_DECISIONS #39): agent_identity +4 cols (status active/suspended/killed + suspended_at/suspended_by_actor_id/suspension_reason — the agent kill-switch, one link in a documented precedence chain: tenant status > platform.ai_credit_account status > this agent's own status > agent_duty_grant > agent_skill_assignment > role assignment > feature flags). 35 tables / 400 cols. Reopened 2026-07-09 — isolated Vrida-operator identity (PROJECT_DECISIONS #45): +operator (14 cols) + operator_role_assignment (8 cols), replacing the identity_user.is_platform_user boolean overlay; actor.actor_type CHECK widened to add 'operator'. Belt-and-suspenders security backstop (explicit REVOKE + RLS-with-zero-policies), NULL-safe self-issue guard on role assignment, all live-reproduced; 7 same-pass code retargets (AdminAuthGuard, recordLogin, grantSupportAccess, listSupportAccessGrants, getUser, create-admin-user.ts, test fixtures) plus 3 more found during the build (platform.service.ts's activity-log name join, admin-console-seed.ts's seeded operator, the admin login page's 401/403 copy). 37 tables / 422 cols. Reopened a 4th time 2026-07-10 for Header/Line Remediation fix #3 (PROJECT_DECISIONS #52) — Identity's own contribution to the second batch of the coordinated effort (Inventory #49 → Orders #50 → Billing #51 → Identity #52): new table invitation_site_assignment (6 cols, pre-acceptance staging of intended site access, mirroring user_site_assignment's own shape; site_id deliberately carries no FK, matching user_site_assignment.site_id's own pre-existing, disclosed gap) plus user_site_assignment.created_from_invitation_site_assignment_id (1 col, nullable composite FK back to the new table, corrected from the design draft's own bare-FK mistake before build). identity.invitation gained a prerequisite UNIQUE(id, tenant_id); its site_assignments JSONB is now deprecated in place (confirmed zero live rows — no backfill needed or possible). A mandatory pre-migration orphan-check audit found ONE live orphaned row in user_site_assignment.site_id (absent from multi_loc.site) — because of this, the optional bundle wiring all 3 site_id-shaped columns to real multi_loc.site FKs was deliberately NOT taken; flagged for a human decision. Independent verification: PASS on every check, zero findings of concern, zero scope creep. 38 tables / 429 cols. Reopened a 5th time same day (2026-07-10) — resolves the human decision above (PROJECT_DECISIONS #54). Orphan investigated (isolated dev-seed fixture junk, confirmed via a DB-wide tenant-isolation scan) and deleted; user_site_assignment.site_id, tenant_user.default_site_id, invitation_site_assignment.site_id are now real composite FKs → multi_loc.site(id, tenant_id) (multi_loc's 1st reopen since lock, adding the prerequisite UNIQUE(id, tenant_id)). Pure constraint-shape change — table/column count unchanged, 38 tables / 429 cols. platform.tenant.primary_site_id, identity.user_permission_override.scope_id, and a previously-untracked 4th sibling surfaced during this pass, identity.access_request.requested_scope_id, remain genuinely deferred — see OPEN_ITEMS. Independent verification: CLEAN, zero findings. IdentityService COMPLETE 2026-06-29 — all 5 phases, 50/50 tests (no service methods yet for agent_duty_grant/the kill-switch/the operator tables/the new invitation-site-assignment tables — schema-only this pass for the newest additions). Pages remain (Phase 6). platform
Shared shared 8 75 LOCKED 2026-07-05. Non-tenant global reference data: currency, country, administrative_region (ISO 3166-2, replaces v1's us_state), language, locale, unit_of_measure, climate_zone (multi-system: USDA/RHS/AHS Heat/Australian/EU, replaces v1's usda_hardiness_zone), plant + plant_common_name + plant_climate_zone, exchange_rate, payment_terms_catalog. Natural-key PKs except plant/exchange_rate (UUID — taxonomic names aren't permanently stable; no composite-PK precedent exists anywhere in this codebase, confirmed via grep, so exchange_rate uses a surrogate PK + UNIQUE(from_currency_code, to_currency_code, effective_date) over a natural composite PK); see PROJECT_DECISIONS #17. Seeded: 152 currencies, 193 countries, 186 administrative regions (comprehensive for US/Canada/Mexico/UK/Australia, representative for 5 more countries — scaled down from a larger target, see #17), 94 languages, 36 locales, 31 units of measure, 60 climate zones, 114 plants (scaled down similarly), 70 common names, 68 plant-climate-zone links. +7 cols 2026-07-06 (autonomy-first backfill: data_source/is_verified on plant_common_name/plant_climate_zone, created_by_actor_id on all 3 plant tables) — see PROJECT_DECISIONS #19. +2 tables 2026-07-08, Remediation Phase 4 Items 16/17b (PROJECT_DECISIONS #40): exchange_rate (7 cols, a reference table with no consumer FK yet — the currency-agreement enforcement it enables lives on billing.ar_payment_application) and payment_terms_catalog (9 cols, 10 seeded rows incl. 2_10_net_30, deliberately kept pure-global with no tenant_id after an earlier draft considered a tenant-custom variant). Both confirmed zero exception to shared's 100%-global/no-RLS convention. Also corrects a pre-existing, Phase-4-unrelated stale count: this row's own baseline had read "108 cols" since the 2026-07-06 autonomy backfill note above was written, but never reflected entry #21's own FIX 1 review-seam addition (review_status/review_reason/reviewed_by_actor_id/reviewed_at × 3 plant tables = +12 cols) — the true pre-Phase-4 baseline, live-confirmed, was 120 cols, not 108. 12 tables / 136 cols (120 + 16 of Phase 4's own 2 new tables = 136; stated as 108→136, +28, of which +16 is genuinely new and +12 is this drive-by correction). Reopened 2026-07-18 for Phase 2 (Nursery Vertical Extraction + shared Lockdown, PROJECT_DECISIONS #70): climate_zone (10 cols), plant (22 cols), plant_common_name (15 cols), plant_climate_zone (14 cols) — 4 tables/61 cols — MOVED verbatim to a new nursery_ref schema via ALTER TABLE ... SET SCHEMA (zero column change during the move), closing a vertical-neutral-core violation 3 independent external design reviews had found. shared is now also write-locked to authenticated (REVOKE INSERT/UPDATE/DELETE + ALTER DEFAULT PRIVILEGES for future tables), live-reproduced via a scoped, rolled-back GRANT proving the REVOKE — not RLS — rejects the write. 12→8 tables, 136→75 cols. identity
Nursery Reference nursery_ref 4 61 SCHEMA LOCKED 2026-07-18 (PROJECT_DECISIONS #70) — Phase 2 of the Nursery Vertical Extraction + shared Lockdown effort. Global, Vrida/AI-curated botanical reference dictionary — climate_zone/plant/plant_common_name/plant_climate_zone — relocated verbatim out of shared via ALTER TABLE ... SET SCHEMA, closing a vertical-neutral-core violation 3 independent external design reviews had found (nursery-specific botanical concepts living inside the tenant-neutral shared schema). Now write-locked to authenticated (REVOKE INSERT/UPDATE/DELETE + ALTER DEFAULT PRIVILEGES for future tables), mirroring shared's own same-day lockdown, live-reproduced via a scoped, rolled-back GRANT proving the REVOKE — not RLS — rejects the write. Depends on shared for a locale FK only. Introduces no new authority mechanism. Schema-only — no service layer yet. See PROJECT_DECISIONS #70 and docs/database/SCHEMA_CONVENTIONS.md §21. shared
Multi-Location multi_loc 1 36 LOCKED 2026-07-05. The site concept — a physical nursery property; global from day 1 (flat address lines + natural-key FKs into shared.country/administrative_region/climate_zone, replacing v1's US-shaped address JSONB). Currency/locale resolve via country_code join, not stored. measurement_system and climate_zone_code's system-appropriateness are service/UI-enforced, not DB-enforced (no shared lookup exists) — see PROJECT_DECISIONS #18. Transfer schema is now Inventory-owned and built; runtime orchestration remains deferred. +8 cols 2026-07-06 (autonomy-first backfill: actor attribution, automation_source, decision_provenance, review seam on site) — see PROJECT_DECISIONS #19. Reopened 2026-07-10 (1st reopen since lock)site gained UNIQUE(id, tenant_id), the prerequisite for 3 identity-side composite FKs (user_site_assignment.site_id, tenant_user.default_site_id, invitation_site_assignment.site_id) to be wired for real, closing part of the long-deferred FK gap. Pure constraint-shape change — table/column count unchanged, 1 table / 37 cols. See PROJECT_DECISIONS #54. Reopened again 2026-07-18 for Phase 2 (Nursery Vertical Extraction, PROJECT_DECISIONS #70): site.climate_zone_code DROPPED, superseded by the new nursery.site_profile tenant-scoped extension table (which also replaces 3 nursery-only site_type values previously living here). 37→36 cols. Table count unchanged, still 1 table. platform, shared, identity
Pricing pricing 4 81 SCHEMA LOCKED 2026-07-06, reopened 2026-07-07 for 3 erosion fixes (PROJECT_DECISIONS #28), reopened again 2026-07-20 for Gap-Fill A1 (PROJECT_DECISIONS #74) — the first module of the sell path. Odoo-style price lists, per-variant rules, customer/group assignments, quantity breaks, dated sales. (Layers on inventory base price.) Supersede-don't-edit price history (never mutate a live rule — supersede it), explicit applies_all_sites site-scoping for customer-scoped rules, tax_treatment, cost_plus_percent markup pricing, campaign_label. +3 cols 2026-07-07: price_rule.rule_kind/.name + price_list_assignment.is_active (restore v1 capabilities the crm/inventory/pricing erosion audit found silently dropped). +3 cols 2026-07-20 (Gap-Fill A1): item_variant_id relaxed nullable; new item_scope_type (variant/category/brand/all — a distinct WHAT-axis column, deliberately NOT reusing the pre-existing scope_type column, which governs the WHO axis) + nullable category_id/brand_id composite FKs, enabling category- and brand-scoped rules ("20% off all perennials" as one rule, not one per variant). Resolution precedence (variant > category > brand > all) is a documented future PricingService rule, not schema-enforced. 78→81 cols. Pure consumer of identity.agent_duty_grant — introduces no new authority mechanism. Schema-only this pass — no PricingService yet. inventory, crm
Payments payments 9 159 SCHEMA LOCKED 2026-07-07 (module #19), reopened 2026-07-08 for Remediation Phase 2 (PROJECT_DECISIONS #38) — the money-MOVEMENT layer, executes what billing (#18) records. +1 col 2026-07-08: payment_intent.processor (NOT NULL DEFAULT 'stripe', CHECK-constrained to ('stripe') only for now) — the minimal-now vendor-de-primitivization anchor; the full de-primitivization (19 other stripe_* columns across 6 schemas, 2 table renames, this CHECK's own widening, tax.provider's CHECK widening) is deferred to OPEN_ITEMS, triggered "before a second payment processor is integrated." Up from the real, locked v1 baseline of 8/112 (self-verified, not a stale placeholder) — all 8 v1 tables preserved (stripe_connect_account 16 LIGHT, payment_intent 32 FULL, payment_refund 21 FULL, payout 20 FULL, dispute 21 FULL, payment_method 15 LIGHT, stripe_event_log 8 ZERO, stripe_event_dead_letter 10 ZERO) + terminal_reader (15 cols, LIGHT, NEW — closes OPEN_ITEMS row 185's hardware-pairing gap). payment_intent.source_ref polymorphic → pos.sale_payment/orders.order_payment/billing.ar_payment (plain uuid, no FK — 3-way widened from v1's pos/billing-only pair). chk_payment_intent_source_pair is a genuine fix (adversarial-caught pre-build — v1 never had this CHECK either): rejects source_module='pos'+source_type='ar_payment'-style invalid combos, mirroring tax.tax_calculation/billing.ar_charge's own source-pair CHECK. The idempotency/double-charge guard was checked specifically against the same-day billing.ar_charge NULL-distinctness bug class and found clean (both partial uniques are single-nullable-column with matching WHERE clauses). terminal_reader.stripe_reader_id carries the UNIQUE every other Stripe-ID column in this module has (an adversarial-caught omission, fixed pre-build). 5 hardcoded 'USD' defaults removed (global from day 1). The money boundary: billing records, payments executes, platform is Vrida's own unrelated SaaS revenue — platform.subscription.status's column comment corrected at this build to name a distinct Vrida-billing service. No agent ever moves money — charge execution is system/human-triggered only; refund execution requires human approval when agent-drafted; agents only flag (fraud/anomaly, reconciliation breaks, chargebacks). First module built under the evidenced independent-verification gate (SCHEMA_DESIGN_RUNBOOK §2.3.7/2.7/6.1a). Consumes identity.agent_duty_grant. Schema-only — no PaymentsService yet. See PROJECT_DECISIONS #33. platform, identity, crm, multi_loc, shared, pos, orders, billing
Integrations integrations 9 127 Generic external-connector runtime: connectors, OAuth credential rotation, sync runs, provider-call log, inbound/outbound webhooks, field mapping. (Config in Admin; executes/syncs here.) platform, admin
Files files 6 89 SCHEMA LOCKED 2026-07-11 (module #26) — the generic file-metadata registry, first real v2 build (supersedes the stale v1-carryover placeholder count this row previously held, 3 tables/43 cols, locked as a design 2026-06-11 but never migrated). Cloudflare R2 is the sole storage of record; AWS S3 is transient staging ONLY for AWS Textract's async extraction path, never a durable tier this module models (PROJECT_DECISIONS #58 — the full storage-architecture decision, incl. cost model). Every v1 table/column survives: file (19→27 cols — 16 verbatim + r2_key/r2_bucketstorage_key/storage_bucket renamed, uploaded_by_user_iduploaded_by_actor_id retargeted to identity.actor, status CHECK widened +ready, +8 new cols incl. consumer_id LOOSE/no-FK, storage_provider, scan_status, extraction_status, textract_job_id, pending_expires_at), file_access_grant (16 cols, v1 verbatim), file_storage_usagetenant_storage_usage (8 cols, renamed, usage-only per DR6 — no limit column, ever). Plus 3 genuinely NEW tables: attachment (17 cols — the polymorphic many-to-many join, entity_type a CLOSED CHECK enum mirroring approvals.approval_request.source_module's own precedent, role open) and the "vector spine," built now per this session's own locked decision and populated later — document_index (10 cols) + document_chunk (11 cols, embedding vector(1024) sized for Amazon Titan Text Embeddings V2, left NULL until semantic search activates; search_vector generated tsvector works immediately with zero embeddings). document_chunk is deliberately NOT append-only (embedding is UPDATEd post-insert); re-indexing SOFT-deletes the prior chunk generation, never a hard DELETE. files.file carries UNIQUE(id, tenant_id) from this build's own day one — a first for this codebase, built in rather than added via a later reopen, the prerequisite the still-deferred forward-ref FK-wiring bundle (8 columns across admin/crm/pos/receiving/inventory/ai) will need. Foundation-layer discipline: file.consumer_id and file_access_grant.grantee_customer_id are deliberately LOOSE, unenforced uuid (no FK) — files must not FK into the consumer or business layers (SCHEMA_CONVENTIONS.md §1) — independently confirmed live via pg_constraint (zero FKs from files.* into consumer.*). The dual-principal (merchant/consumer) boundary closes at the GRANT layer: consumer_authenticated gets ZERO grant on the files schema at all; the sole consumer read path is consumer.get_files_for_consumer(p_consumer_id uuid), a consumer-schema-owned (not files-owned — the permitted calling direction), parameter-scoped SECURITY DEFINER function mirroring consumer.get_cross_tenant_activity()'s own established shape exactly. This design went through a full 3-lens independent adversarial verification BEFORE the build (a genuine layer-boundary violation in the pre-verification draft — the wrong FK direction, the wrong function placement, a non-executable join — caught and fixed pre-build, not post-build), then a further mandatory evidenced independent lock-gate verification after the build itself. Consumes identity.agent_duty_grant for attachment's own AI bulk-photo-matching review seam — no new authority mechanism introduced. Reopened same day (2026-07-11) for a Returns-module companion fix (PROJECT_DECISIONS #61): attachment.entity_type CHECK widened to accept 'return_authorization', the new returns schema's own attachment seam — constraint-only, zero column/table impact, still 6 tables/89 cols. Schema-only this pass — no FilesService yet. See PROJECT_DECISIONS #58/#59/#61. platform, identity
AI / Intelligence ai 49 227 SCHEMA LOCKED 2026-07-06 — the agent-runtime schema layer. AI onboarding import pipeline (zero-mapping ingestion: import_job/import_file/import_record) + Bedrock-call log (ai_request), plus 3 new tables: agent_execution (append-only action ledger, propose/execute linkage via resolves_execution_id self-FK, idempotency-key dedup), agent_usage_period (atomic-upsert cost/token usage meter), agent_memory (durable agent memory store, partial-unique-on-active-status). AI features are service-layer over existing data. Human-in-the-loop DB-enforced. Closes platform.tenant_setup_task onboarding-import dependency. Consumes identity.agent_duty_grant/agent_identity for all agent authority (no new authority mechanism introduced). Reopened 2026-07-08, Remediation Phase 4 Item 18 (PROJECT_DECISIONS #40): agent_memory +3 cols (subject_type/subject_ref/expires_at, chk_agent_memory_subject_consistency requiring both-null-or-both-set — a GDPR/erasure-scoping seam, not yet consumed by any erasure job). 118→121 cols. Reopened a 2nd time 2026-07-13 — Phase 2 of the 6-phase agents-v2/v3 build authorization (Phase 1 reopened platform, PROJECT_DECISIONS #62; this is Phase 2, PROJECT_DECISIONS #63): +14 new tables implementing the C1 model registry/deployment/prompt/routing layer (provider_registrymodel_familymodel_version; model_deployment + _limit/_region/_policy/_override satellites + model_deployment_status_observation — 3 genuinely separate state classes, durable/override/transient, live-confirmed distinct; prompt_definition_versionprompt_model_compatibility; routing_policy, schema-only, no resolver built yet) plus C4's agent_memory_source (a join table, since one memory may derive from multiple sources). agent_execution/agent_memory — v1 tables, reopened for the first time — are now PARTITIONED BY RANGE(created_at), monthly, the first partitioned tables in this codebase's history; the at-most-one-resolver, idempotency-dedup, and at-most-one-active-memory invariants that used to live on native partial-unique indexes are now enforced by new advisory-lock BEFORE INSERT triggers instead (a native unique index on a partitioned table must include the partition key, which would silently narrow each invariant to per-partition-month scope). routing_policy.workload_class_id is a disclosed forward-ref (bare uuid, no FK) to the not-yet-built agents.workload_class (Phase 5). 121→227 cols, 7→49 tables (14 new table types +102 cols, 28 new partition-child tables +0 cols since a partition inherits its parent's columns, agent_execution +1 col, agent_memory +3 cols). PROJECT_DECISIONS #63's own summary sentence says "13 new tables" against an actual 14-item enumeration — a pre-existing off-by-one, disclosed in docs/database/schema_docs/ai.md rather than corrected here. Schema-only this phase too — no AIService extensions exist yet for any of the 49 tables. platform, identity, files, agents (deferred)
Semantics semantics 14 114 SCHEMA LOCKED 2026-07-14 (PROJECT_DECISIONS #64) — Phase 3 of 6 of the agents-v2/v3 build authorization (Phase 1 reopened platform for the module registry + AI-capacity layer, PROJECT_DECISIONS #62; Phase 2 reopened ai for the model-registry/deployment/prompt/routing layer, PROJECT_DECISIONS #63; this is Phase 3). Brand-new foundation-layer schema, zero v1 antecedent — the shared business ontology the not-yet-built agents (Phase 5) and signals (Phase 4) modules will read from rather than each inventing their own metric/goal/constraint vocabulary. Design of record: vrida-agents-v2-design-amendment-2026-07-11.md (B1) as amended by vrida-agents-v3-correction-pass-2026-07-12.md (I2/I3/I4, v3 wins on conflict). 14 tables: approved_function_registry (11 cols — I2's supply-chain-hole-closing allowlist: a metric/attribution-model version's computation is a POINTER to a reviewed function, never raw SQL in a data row; verify_function_still_matches_approval() re-resolves and re-hashes the live function fresh on every call, closing the "approve a function, quietly redefine it later" hole), metric_definition/metric_version (6/8 cols, standard A2b definition/version pattern), tenant_metric_binding (12 cols, which version is authoritative for a tenant/date-range — RLS-enabled), metric_dependency (4 cols, real cycle-detection via a recursive-CTE trigger, not dead code), entity_definition/entity_alias (6/5 cols, names cross-schema entity aliases as queryable facts, e.g. the still-deferred crm.customer.consumer_id ambiguity — does not retrofit that FK itself), dimension_definition (5 cols), goal_definition/constraint_definition (8/7 cols, global catalogs) + tenant_goal_binding/tenant_constraint_binding (15/15 cols, RLS-enabled, same overlap-prevention pattern as tenant_metric_binding), attribution_model_definition/attribution_model_version (4/8 cols, same registry-pointer pattern as the metric family). EXCLUDE USING gist hard-reject overlap prevention on all 3 tenant-scoped binding tables (reusing platform.accounting_period's own btree_gist precedent, but HARD-REJECT not flag-not-reject — no offline-sync reason applies to a synchronous admin action like setting an authoritative binding, unlike POS's offline-first sale posting). goal_definition.applies_to_domain → platform.module_catalog.id is the first real consumer of module_catalog as an FK target outside platform itself. tenant_goal_binding.module_id/.site_id and tenant_constraint_binding.module_id/.site_id are deliberately BARE, no FK — a disclosed judgment call (v3's own literal SQL specifies them bare; platform.module_catalog already exists but the design of record doesn't specify wiring module_id to it), logged to OPEN_ITEMS, not silently fixed or ignored. Section 4 self-audit found and fixed 2 real gaps before lock: 3 lifecycle_status columns (dimension_definition/goal_definition/attribution_model_definition) had a default but no CHECK at all (fixed); tenant_goal_binding/tenant_constraint_binding were missing created_at/updated_at + the maintaining trigger their sibling tenant_metric_binding already had (fixed — this is why the schema landed at 114 cols, not the original 110). A schema-usage GRANT gap (missing GRANT USAGE ON SCHEMA) was found only by the regression suite, not the read-only audit pass — fixed live and in the migration. 19/19 new regression tests (semantics-schema.spec.ts); full apps/api suite green at 1150/1150. Consumes identity.agent_duty_grant for no agent-authored write path in this phase (see module spec §8) — no new authority mechanism introduced. Schema-only — no SemanticsService yet. platform, identity
Signals signals 11 111 SCHEMA LOCKED 2026-07-15 (PROJECT_DECISIONS #65) — Phase 4 of 6 of the agents-v2/v3 build authorization, this build's own explicitly-designated HIGHEST-RISK phase (Phase 1 reopened platform, PROJECT_DECISIONS #62; Phase 2 reopened ai, PROJECT_DECISIONS #63; Phase 3 built semantics, PROJECT_DECISIONS #64; this is Phase 4). Brand-new foundation-layer schema, zero v1 antecedent — the feature/forecast/outcome store the not-yet-built agents module (Phase 5) will read from and write to. Opposite semantics's own operational profile: observational, high-volume, bitemporal, append-only, and PARTITIONEDfeature_value/forecast/anomaly_score weekly by recorded_at (12 weekly partitions + 1 DEFAULT = 13 each), outcome_observation monthly by created_at (13 monthly partitions + 1 DEFAULT = 14); outcome_authority and the 7 catalog/experiment tables stay unpartitioned. Design of record: vrida-agents-v2-design-amendment-2026-07-11.md (B2/B3/B4/B5/B6/A6) as amended by vrida-agents-v3-correction-pass-2026-07-12.md (BLOCKER 1/2/3/8, v3 wins on conflict). 11 tables: feature_definition/feature_version and experiment/experiment_version (6/7 and 6/9 cols, standard A2b catalog pattern), feature_value/forecast/anomaly_score (10/11/10 cols, REVOKE ALL from authenticated, readable only via 3 SECURITY DEFINER as-of functions), experiment_assignment/experiment_exposure_event (19/5 cols, RLS-enabled), outcome_observation/outcome_authority (19/9 cols, the split-authority pattern). 4 named critical guards, all live-reproduced: GUARD 1 (BLOCKER 1) — the 3 as-of functions derive tenant scope from a new platform.current_tenant_id() session-GUC helper instead of a spoofable p_tenant_id parameter, owned by a new minimal signals_function_owner role with its own narrow SELECT grant on exactly the 3 tables it reads; GUARD 2 (BLOCKER 2) — a native partitioned unique index cannot express "exactly one authoritative observation per scope," so a genuinely separate, unpartitioned outcome_authority table (PK = the scope tuple) is the real enforcement, maintained by a native race-free INSERT ... ON CONFLICT upsert; GUARD 3 (BLOCKER 3) — assigned_at forgery is closed by an unconditional trigger overwriting any caller-supplied value with clock_timestamp(), and a live-reproduced retroactive-exposure-contamination race (a genuinely-earlier exposure arriving after the causal basis is locked) is closed via matching row-locks in 2 cooperating trigger functions, flagging contamination rather than silently moving history; GUARD 4 (B3 mandatory test 4a) — a fact recorded after a decision's own knowledge_cutoff is structurally invisible to that decision, DB-enforced not service-layer-conventional. A genuine gap in the design doc's own literal SQL (anomaly_score's and outcome_observation's own unique constraints omitting the required partition key — the identical error class BLOCKER 2 itself documents, just not caught by that section's own adversarial pass for these 2 sibling constraints) was found and fixed during this phase's own migration-apply pass, backstopped by a genuine cross-partition duplicate-check trigger on outcome_observation mirroring Phase 2's own ai.agent_execution.idempotency_key precedent. 2 privilege-mechanics gaps also found live (role-membership + schema-CREATE requirements for the ALTER FUNCTION ... OWNER TO transfer; a USAGE grant signals_function_owner needed on schema platform for the function body's own platform.current_tenant_id() call) — neither the v2 nor v3 design anticipated either. 2 disclosed forward-refs: outcome_observation/outcome_authority.agent_action_id (bare uuid, agents.agent_action doesn't exist until Phase 5) and the 3 as-of functions' own REVOKE EXECUTE FROM PUBLIC with no GRANT EXECUTE yet (agent_reader doesn't exist until Phase 6) — both logged to OPEN_ITEMS. 20/20 new regression tests (signals-schema.spec.ts); full apps/api suite green at 1172/1172. Consumes identity.agent_duty_grant for no agent-authored write path in this phase (see module spec §8) — no new authority mechanism introduced. Schema-only — no SignalsService yet. platform, semantics
Agents agents 47 475 SCHEMA LOCKED 2026-07-16 (module #28, PROJECT_DECISIONS #66) — Phase 5 of 6 of the agents-v2/v3 build authorization, the module the entire authorization exists to build (Phase 1 reopened platform, PROJECT_DECISIONS #62; Phase 2 reopened ai, PROJECT_DECISIONS #63; Phase 3 built semantics, PROJECT_DECISIONS #64; Phase 4 built signals, PROJECT_DECISIONS #65; this is Phase 5). Merges the full v1 agent-orchestration baseline (21 v1 tables — agent_task, agent_thread, agent_event_log, agent_schedule, agent_trigger, agent_eval_suite, agent_eval_run, agent_eval_case, agent_performance_profile, agent_shadow_run, agent_shadow_decision, agent_autonomy_profile, agent_action, rollback_recipe, rollback_execution, tool_catalog, agent_tool_grant, agent_catalog_entry, agent_catalog_entry_required_tool, tenant_agent_deployment, agent_incident) with v2's own A1–A8 amendments and v3's own BLOCKER1–8/I1–I8 corrections (v3 wins on conflict), across 12 Drizzle files: _schema.ts, tool.ts (tool_definition/tool_version/tool_version_module — SUPERSEDES v1's tool_catalog, zero live v1 rows), skill.ts (skill_definition/skill_version/skill_version_module/skill_version_required_duty/skill_version_toolset/skill_certification/skill_execution_policy/agent_skill_assignment), marketplace.ts (agent_catalog_entry/_required_tool/tenant_agent_deployment/agent_incident), task.ts (agent_task/agent_thread/agent_schedule/agent_trigger/agent_event_log), eval.ts (agent_eval_suite+_version/agent_eval_run/agent_eval_case/agent_performance_profile), shadow.ts (agent_shadow_run/agent_shadow_decision), certification.ts (workload_class/toolset_definition/toolset_version/toolset_version_member), decision_context.ts (decision_context_manifest/decision_context_snapshot [both PARTITIONED monthly] + _feature/_forecast/_metric/_policy/_knowledge_source, evidence_retention_policy), action.ts (agent_action/agent_decision [PARTITIONED monthly]/agent_autonomy_profile), kill_switch.ts (kill_switch_event/kill_switch_scope_state), policy.ts (rollback_recipe/rollback_execution). 8 distinct trigger-backed critical guards, all live-reproduced (the design named 7; a 6th, T1, was added and proven during the Section 4 audit itself — disclosed honestly as an 8th rather than force-fit): E3 (atomic task claim + fencing, lease renewal never bumps the fencing token), BLOCKER7 (kill-switch history/state split + resume-safe propagation, plus a Section-4-audit-found RLS gap — see below), BLOCKER4+I1 (the saga gate enforced against real writes, not a static toolset-shape proxy, plus agent_action/agent_decision consistency), the runaway-ceiling guard (BLOCK not flag, closing v1's own Gap 1), A5 corrected (the kill-switch check fires on UPDATE OF status — task CLAIM — not just INSERT), A3+BLOCKER5+A8R4 (skill activation: certification + duty-grant authority + a spend-ceiling policy, folded into one function, validate_skill_activation()), and the A2b retirement-version-check trigger (reject_retired_decision_context_versions). 2 genuine PL/pgSQL bugs found and fixed during this build's own live guard-reproduction (not the read-only Section 4 audit): (a) a record-typed PL/pgSQL variable's IS NOT NULL check is UNRELIABLE used directly as an IF condition — silently swallowed the entire A8 Rule 4 spend-ceiling check inside validate_skill_activation(), fixed by rewriting to scalar variables; (b) SELECT MIN(...) ... FOR UPDATE is syntactically REJECTED by Postgres, fixed by splitting into a lock-only PERFORM ... FOR UPDATE followed by a separate SELECT MIN(...) INTO .... 1 genuine security gap found and fixed during the Section 4 self-audit: agents.kill_switch_event carried tenant_id + a GRANT but zero RLS policies, letting any tenant session read every other tenant's kill-switch history and forge a kill/suspend/resume event against another tenant's agents — fixed with the same 2-policy mixed-scope RLS pattern already established by agent_eval_suite/rollback_recipe, live-reproduced and covered by a new regression test (I5); the same audit fixed item J (15 JSONB columns lacked documented example shapes) and disclosed, not fixed, item K (12 tables lack a tenant_id-leading index — logged to OPEN_ITEMS, low severity since RLS correctness is unaffected). 3 pre-existing forward-ref OPEN_ITEMS rows CLOSE with this build: ai.routing_policy.workload_class_id → agents.workload_class(id) (plain FK, workload_class is global/no tenant_id) and signals.outcome_observation/outcome_authority.agent_action_id → agents.agent_action(id, tenant_id) (composite FKs) — 0 orphans found for all 3; signals-schema.spec.ts (Phase 4's own file) needed a retrofitted real fixture chain (identity.actor → agent_identity → decision_context_snapshot → ai.agent_execution → agents.agent_decision → agents.agent_action) to keep passing, replacing 5 hardcoded placeholder UUIDs. ai.agent_execution gained 8 columns this same migration (agent_task_id, sequence_index, tool_version_id, approval_request_id, retry_count, blocked_by_policy, confidence_threshold, escalation_reason), ai.routing_policy.workload_class_id is now wired, and approvals.approval_request's chk_approval_request_source_module CHECK was widened to add 'agents' (11 values total) — all disclosed companion touches inside this one migration, not separate reopens. platform also gained 1 table this same migration (polymorphic_target_registry, I6, 4 cols) — platform is now 35 tables/518 cols (up from 34/514). Disclosed design judgment calls (in code comments in the Drizzle files): A1's domain→module_id retype scoped to EXACTLY 3 columns (agent_task.domain, tool_definition.domain, skill_definition.domain) per A1's own explicit scope-limiting text — NOT extended to agent_performance_profile.domain/agent_autonomy_profile.domain (both stay free-text); agent_eval_run.eval_suite_version_id retargets to agent_eval_suite_version.id (A2b version-pinning), not the identity row; agent_action has NO agent_task_id column — BLOCKER4's trigger derives it via a join through agent_execution_id instead (a genuine gap in v3's own literal SQL, fixed structurally not patched); A3's "trigger on tenant_agent_deployment" resolved instead as a trigger on agents.agent_skill_assignment (the only table with both agent_identity_id + skill_version_id), folding A3 cert-check + BLOCKER5 duty-check + A8 Rule4 policy-bound check into ONE function; A2b retirement-rule-1 checks split by actual column location (ai.agent_execution checks only its own tool_version_id; agents.decision_context_snapshot checks its own model_version_id/prompt_version_id/skill_version_id/toolset_version_id). A transitional Drizzle barrel collision — identity.agentSkillAssignment (v1, still used by production IdentityService) and agents.agentSkillAssignment (I5's new destination table) — coexists until Phase 6 drops the old one, resolved via explicit named re-exports in packages/db/src/schema/index.ts. Migration packages/db/migrations/20260716000000_agents_module_new_schema.sql (1650 lines, incl. a same-day SECTION 10 appended after the Section 4 self-audit's own 2 fixes). Consumes identity.agent_duty_grant/agent_identity for all agent authority — this module IS the primary consumer/executor of that authority, not a new grant mechanism. Schema-only — no AgentsService yet (every phase of this build stays schema-only until Phase 6, which builds the agent_reader role + connection helper + an ESLint ban on adminDb/tenantDB/set_config from agent read paths). Tests: apps/api/src/platform/__tests__/agents-schema.spec.ts, new file, 24/24 passing. Full apps/api suite green at 1198/1198 (serially — a known pre-existing connection-pool-exhaustion flake in database/__tests__/rls-cross-tenant.spec.ts intermittently fails only under parallel workers, unrelated to this build). platform, identity, ai, approvals, semantics, signals, files
Search search 0 0 Zero-table schema — delivered as 6 additive FTS touches (+6 search_vector cols) to locked source tables. Postgres-native FTS: tsvector generated cols + dual GIN indexes (tsvector + pg_trgm) on inventory.item, inventory.item_variant, crm.customer, purchasing.vendor, orders.order_header, pos.sale. SearchService abstraction; callers never query search_vector directly; every query must include WHERE tenant_id = ?. inventory, crm, purchasing, orders, pos
Approvals approvals 8 97 SCHEMA LOCKED 2026-07-09 — a cross-cutting approval-workflow engine, split out of Admin (reverses PROJECT_DECISIONS #34 Section 5's Option B "stays Admin-internal" decision — see Admin's row below). 3 tables MOVED from admin via ALTER TABLE ... SET SCHEMA, one extended: approval_workflow (10→12 cols — +step_mode sequential/parallel/conditional, +blocks_agent_approver the C8 financial-autonomy boundary anchor, write-locked via a table-level REVOKE + per-column re-GRANT so no tenant-scoped write path can flip it), approval_routing_rule (unchanged, 11 cols), approval_request (18→20 cols — requested_by_actor_id RENAMED to initiator_actor_id and made NOT NULL, closing a null-approver bypass; step_history DROPPED, superseded by the new approval_step). Both approval_workflow.workflow_type and approval_routing_rule.workflow_type had their old CHECK-enumerated value list (po_approval/discount_approval/refund_approval/other) REMOVED — free text now, since baking business-process names into the engine's own schema was a domain-knowledge leak the design-phase review caught (the same reasoning kept approval_request.source_type free text too). Plus 5 NET-NEW tables: approval_policy (10 cols — self-approval/SoD config, min_distinct_approvers/allow_self_approval/require_role_separation), approval_step (14 cols — per-request step instances, step_mode/resolution_mode distinguishing AND-parallel from OR-fanout groups sharing a step_index), approval_delivery (12 cols — outbound approver notifications; a disclosed, temporary overlap with the not-yet-built notifications module's own planned "delivery attempts" scope, logged to OPEN_ITEMS), approval_token (10 cols — hash-only one-click approve/reject tokens, atomic single-use redemption folding used_at/superseded_at/expires_at into one UPDATE), approval_event (8 cols — append-only audit log, reuses platform.reject_append_only_mutation()). 6 critical guards, all live-reproduced against the local DB with real before/after proof: approved-by-nobody (chk_approval_request_resolved_requires_actor_at), self-approval on every path incl. non-final parallel steps and the null-approver path (chk_approval_request_resolver_not_initiator + trg_approval_step_no_self_approval), parallel-step quorum-spoofing (trg_approval_step_distinct_approvers), the C8 agent-money boundary (trg_approval_step_blocks_agent_approver — a genuine 3rd bug found live during this build: a column-level REVOKE alone does not subtract from a broader table-level GRANT in Postgres, since ACLs are additive across granularities; fixed by revoking the table-level GRANT entirely and re-granting column-by-column), token replay/expiry (the folded atomic predicate + 2 sibling-supersession triggers), and append-only audit enforcement (rejects UPDATE/DELETE even as superuser). Every CHECK-guarded enum/threshold column across the module is NOT NULL — a full NULL-in-CHECK sweep found zero bypass surface. Zero domain-module dependencies — pure infrastructure, consumed read-only by identity (actor, agent_duty_grant), platform (tenant, outbox), and multi_loc (site); 14 domain-module convergence points (identity's own access_request/sod_violation, crm/inventory dedup and tax-cert review, pos refund approval, purchasing's PO/invoice/return gates, ai's execution-approval attribution, and more) are logged to OPEN_ITEMS with concrete triggers — none decided to migrate onto this engine yet. 22 regression tests, all passing. Reopened 2026-07-11 for a Returns-module companion fix (PROJECT_DECISIONS #61): approval_request.source_module CHECK widened to accept 'returns' — constraint-only, zero column/table impact, still 8 tables/97 cols. Schema-only this pass — no ApprovalsService yet. platform, identity, multi_loc

Business Modules

Module Schema Tables Cols Owns Depends on
Inventory inventory 29 425 SCHEMA LOCKED 2026-07-06, reopened 2026-07-07 for 1 erosion fix (PROJECT_DECISIONS #28), 2026-07-08 for Remediation Phase 1 (PROJECT_DECISIONS #37), again 2026-07-08 for Remediation Phase 4 Item 20b (PROJECT_DECISIONS #40), and again 2026-07-10 for Header/Line Remediation fixes #5, #9 (PROJECT_DECISIONS #49) — biggest module built so far, first real shared.plant consumer. 2026-07-10 reopen: new table stock_adjustment_batch (+1 table/+10 cols) — a header grouping multiple stock_adjustment_request rows for joint review, reusing stock_count's status-as-review-seam convention (+partially_approved); stock_adjustment_request gains a nullable batch_id composite FK into it (+1 col); stock_count_line gains reconciled_at/reconciled_by_actor_id (+2 cols) plus a NULL-safe CHECK and a bespoke conditional-immutability trigger (trg_stock_count_line_lock_after_reconciled, NOT the shared blanket append-only function, since a count line must stay editable pre-reconciliation). +13 cols/+1 table total (338→351, 24→25), independently verified live. Disclosed, not silently fixed: stock_movement_line still lacks UNIQUE(id, tenant_id) (the design doc named this as belonging to this reopen — prerequisite for Purchasing's still-deferred fix #7) and stock_adjustment_batch.updated_at has no maintaining trigger (found during doc-writing, logged to OPEN_ITEMS). Remediation Phase 4 added stock.last_movement_id (+1 col, a nullable reconciliation watermark FK → stock_movement) plus the codebase's FIRST CREATE VIEW, stock_reconciliation_shell — deliberately a minimal, aggregation-free shell (plain LEFT JOIN) per an explicit pre-build correction; the real sign-aware drift-detection join/aggregation logic is a named, deferred follow-up (see OPEN_ITEMS). The view does not count toward this row's table total. Vertical-neutral item catalog + variants + stock + lots + kits + counts, plus stock_adjustment_request (propose/execute stock-adjustment gate) and item_merge_candidate/item_merge (dedup propose/execute split, mirrors crm.customer_merge_candidate/customer_merge but closes crm's missing-dedup gap with a canonical-pair-order CHECK + partial-unique). Owns stock_reservation, weighted-avg cost, item_image (display), item.plant_id FK → shared.plant.id. +2 cols 2026-06-11: search_vector on item + item_variant (Search FTS touch). +1 col 2026-07-07: stock_reservation.created_by_actor_id (agent-initiated reservations are now traceable — the erosion audit found this table had no actor attribution at all despite being agent-writable). +1 col 2026-07-08: stock_count.reconciled_at, alongside 6 new fail-open CHECKs across item/item_image/item_merge_candidate/item_variant/stock/stock_adjustment_request (review_status='approved' now requires a recorded reviewer) and stock_count's own reconciled-requires-actor CHECK (PROJECT_DECISIONS #37). 2026-07-08, Remediation Phase 3 Item 11 (PROJECT_DECISIONS #39): stock_movement.movement_type widened to add 'produced'source_module already permitted 'production'; a nursery propagating its own stock now has a real movement_type to pair with it. Pure enum-widen, zero column change. Reopened again 2026-07-11 for a Returns-module companion fix (PROJECT_DECISIONS #61): stock_movement.source_module CHECK widened to accept 'returns', the new returns schema's own inventory-posting seam — constraint-only, zero column/table impact, still 25 tables/351 cols. Reopened again 2026-07-18 for Phase 2 (Nursery Vertical Extraction, PROJECT_DECISIONS #70): item.plant_id (FK → shared.plant) DROPPED, superseded by the new nursery.item_profile tenant-scoped extension table. Live column count is now 358 (this phase's own net effect is −1 col; this row's prior 351 baseline was already stale by +8 cols for reasons unrelated to this phase — see OPEN_ITEMS.md). Table count unchanged, still 25 tables. Reopened again 2026-07-20 as a side effect of the pricing Gap-Fill A1 reopen (PROJECT_DECISIONS #74): category gained a prerequisite UNIQUE(id,tenant_id) (constraint-only, 0 col change) and a brand-new, minimal brand table (7 cols: id/tenant_id/name/created_by_actor_id/created_at/updated_at/deleted_at — no brand catalog existed anywhere in the codebase, architect-authorized to build one now) — the prerequisite for pricing.price_rule.brand_id's own new composite FK. A re-measurement live via information_schema while reconciling this reopen's own counts found this row's stated 358 does NOT reconcile — the true pre-batch live count was 350 cols/25 tables, an 8-column pre-existing drift unrelated to either this reopen or the Phase 2 pass above; disclosed via a new OPEN_ITEMS row, not chased to a root cause. True count was 357 cols / 26 tables at the Gap-Fill reopen; Inventory Core Write Protection (PROJECT_DECISIONS #75) adds five columns and recertifies the current live/Drizzle shape at 362 cols / 26 tables / 1 view; the final fresh-context verifier returned CLEAN and Inventory is re-locked with 137 indexes. (350 + brand's 7 cols, 25 + 1 table). Consumes identity.agent_duty_grant for all agent authority (no new authority mechanism introduced). Schema-only this pass — no InventoryService yet. platform, shared, multi_loc, identity
Nursery nursery 2 18 SCHEMA LOCKED 2026-07-18 (PROJECT_DECISIONS #70) — Phase 2 of the Nursery Vertical Extraction + shared Lockdown effort. Tenant-scoped nursery-vertical extension: item_profile (9 cols, replacing the dropped inventory.item.plant_id FK → shared.plant) and site_profile (9 cols, replacing the dropped multi_loc.site.climate_zone_code plus 3 nursery-only site_type values previously living on multi_loc.site). Both are brand-new tables — no v1 antecedent, no moved columns — built to codify the governing rule this phase establishes (docs/database/SCHEMA_CONVENTIONS.md §21): a generic core table must never gain a vertical-only column/FK when a tenant-scoped extension table can represent the same relationship instead. Consumes identity.agent_duty_grant for no agent-authored write path yet — introduces no new authority mechanism. Schema-only — no NurseryService yet. See PROJECT_DECISIONS #70. platform, inventory, multi_loc, nursery_ref
CRM crm 13 193 SCHEMA LOCKED 2026-07-06, reopened 2026-07-08 for Remediation Phase 3 Item 8 (PROJECT_DECISIONS #39) and again for Remediation Phase 4 Items 17b/18 (PROJECT_DECISIONS #40) — the first actually-built v2 product module (supersedes the stale v1-carryover placeholder count this row previously held, 8 tables/108 cols, never a real v2 build). 13 tables: customer (core entity, individual/business via discriminator, full autonomy treatment), contact, address (global shape — flat lines + natural-key FKs into shared.country/administrative_region, mirrors multi_loc.site), customer_group (human-only catalog, shared with Pricing), customer_note (append-only observation log, no review seam — additive/zero-risk), customer_task (follow-up tasks, open/completed/dismissed IS the review mechanism), customer_merge_candidate (pending dedup proposals, the review gateway), customer_merge (append-only post-execution merge audit), customer_consent (append-only consent event log, never AI-initiated, no review seam), customer_tax_certificate (tax exemption certs, global jurisdiction fields, pending_review default), customer_segment_definition (NEW — segment catalog, mirrors identity.role's mixed-scope built-in/tenant-custom pattern), customer_segment_membership (customer×segment, full autonomy treatment, agent-computed VIP/at-risk/seasonal/wholesale-like classification), customer_tag_assignment (NEW — human free-form labels, deliberately separate from segments). search_vector (Search FTS touch) and consumer_id FK both deferred — search/consumer schemas don't exist yet in v2. +2 cols 2026-07-08 (Remediation Phase 3 Item 8): credit_limit_cents/credit_terms — fixes a live crm↔billing orphan (billing.ar_account's own comment already said it reads these from crm.customer; crm.customer never actually had them — an incomplete v1→v2 migration). crm.customer OWNS the credit policy; billing.ar_account reads it, never duplicates it. +2 cols 2026-07-08 (Remediation Phase 4 Items 17b/18): payment_terms_id (nullable FK → shared.payment_terms_catalog, additive-interim alongside the unchanged credit_terms CHECK-enum — NOT yet kept in sync, and NOT a clean vocabulary subset either, see OPEN_ITEMS) + pii_vault_ref (nullable text, mirrors ein_ref's vault-reference pattern for future crypto-shred erasure, shares rather than duplicates the existing vault-service OPEN_ITEMS dependency). 191→193 cols. Consumes identity.agent_duty_grant for all agent authority (no new authority mechanism introduced). Schema-only this pass — no CrmService yet. platform, shared, multi_loc, identity
POS pos 12 183 SCHEMA LOCKED 2026-07-07, reopened 2026-07-08 for Remediation Phase 3 Items 9–11 (PROJECT_DECISIONS #39), again for Remediation Phase 4 (PROJECT_DECISIONS #40), and again 2026-07-10 for Header/Line Remediation fix #8 (PROJECT_DECISIONS #46) — the only offline-first module. 2026-07-10 reopen: sale_refund_line's documented-but-unenforced write-once immutability is now real DB enforcement (REVOKE UPDATE, DELETE + trg_sale_refund_line_append_only, reusing platform.reject_append_only_mutation() verbatim — a genuine gap Remediation Phase 1's own append-only sweep missed), plus 2 bundled additions: sale_line gained UNIQUE(id, tenant_id) (a prerequisite for a future, still-deferred Orders-side composite FK) and sale_refund_line.sale_line_id was upgraded from a bare to a composite FK. Table/column counts UNCHANGED (still 10/160) — constraints and 1 trigger only. First of 3 reopens (POS → Purchasing → Platform) in a new, coordinated "Header/Line Remediation" effort. 9 tables: register, register_session, register_cash_entry (till lifecycle), sale, sale_line, sale_payment, sale_refund, sale_refund_line (immutable-gross sale.total_minor_units forever — net refunded position is sum(sale_refund.refunded_amount_minor_units) at query time, sale.status deliberately has no 'refunded' value), pos_sync_conflict (genuine two-different-device oversell case only — duplicate-sale replay is solved entirely by the client_uuid UNIQUE dedup index, no conflict row needed for that case). +5 cols 2026-07-08 (Remediation Phase 3): sale.sale_number (+1, restores an unlogged v1 erosion — register-session-prefixed, NOT a global gapless sequence, honoring offline-first); sale_refund_line.item_variant_id/.tax_amount_cents/.tax_rate (+3, the no-receipt-refund identifier + refund tax capture — sale_line_id relaxed to nullable, sale_refund.sale_id itself UNCHANGED/still NOT NULL, the anonymous-walk-in-return question flagged for the architect, not decided); sale_refund.tax_refunded_amount_cents (+1). chk_sale_payment_no_unvalidated_stored_value_tender renamed/widened to chk_sale_payment_no_unbacked_tender_type, now also blocking 'reward' (same bug class as the gift_card/store_credit fix). sale/sale_payment/sale_refund each carry the offline-first quintet (client_uuid, origin, sync_status, idempotency_key, synced_at) with UNIQUE (tenant_id, client_uuid) WHERE deleted_at IS NULL and an origin/sync_status consistency CHECK. Honors Pricing's Hard Contract 1 verbatim on sale_line (6 required snapshot fields — resolved/charged amounts, currency, tax_treatment, resolving_price_rule_id, resolved_quantity — live-tested to stay unchanged after the resolving price rule is superseded). No POS-side reservation table — nets out via inventory.stock.available_qty. search_vector dropped this pass (no free-text field on sale to index, logged to OPEN_ITEMS) and 10 v1 tables deferred with concrete triggers (gift_card/gift_card_transaction/store_credit/store_credit_transaction, layaway_payment, sale_template/sale_template_line, guarantee, sale_line_tax, receipt). +1 table/+17 cols 2026-07-08 (Remediation Phase 4): tender_type_catalog (7 cols, 7 seeded rows incl. 'reward', additive-interim alongside sale_payment's unchanged tender-type enum) + sale_payment.tender_type_id; the anonymous-walk-in-return decision, DECIDED = ALLOWsale_refund.sale_id relaxed to nullable, chk_sale_refund_identification requires a linked sale OR a documented reason, never neither (audit controls deferred to service layer, see OPEN_ITEMS); sale/sale_refund/register_cash_entry.business_date + the flag-not-reject fiscal-period trigger (closes a platform.accounting_period without blocking a late-arriving offline sale) — register_cash_entry additionally gained its first full review-seam (5 cols: review_status/review_reason/reviewed_by_actor_id/reviewed_at/decision_provenance), a necessary mid-build discovery, not planned upfront; sale.entity_id (nullable FK → platform.legal_entity). 143→160 cols, 9→10 tables. Reopened again 2026-07-11 for a Returns-module companion fix (PROJECT_DECISIONS #61): sale_refund/.sale_refund_line each gained UNIQUE(id, tenant_id) — the prerequisite for the new returns schema's own composite FKs into POS — constraint-only, zero column/table impact, still 10 tables/160 cols. Reopened again 2026-07-20 for Gap-Fill A2 + A4 (PROJECT_DECISIONS #74): 2 new tables, parked_cart (13 cols) + parked_cart_line (9 cols) — held/parked carts, guarded by a terminal-state trigger (parked/resumed/discarded, mirroring notifications.delivery_attempt's own no-op-plus-reject shape); parked carts NEVER reserve stock (v1 decision, documented in a table comment). register gained a prerequisite UNIQUE(id,tenant_id). sale_line gained is_gift boolean NOT NULL DEFAULT false (+1 col) — line-level gift-receipt intent; rendering is the receipt/notifications build's own future work. 160→183 cols, 10→12 tables. Consumes identity.agent_duty_grant for all agent authority (no new authority mechanism introduced). Schema-only this pass — no PosService yet. inventory, crm, pricing, admin, payments, multi_loc
Orders orders 7 175 SCHEMA LOCKED 2026-07-07 (module #15) — completes the sell path (supersedes the stale v1-carryover placeholder count this row previously held, 7 tables/133 cols, never a real v2 build). First module designed AND built under the Design-Phase Integrity rules (Section 0.5/2.3.6) — zero tables consolidated or dropped, pure addition (+40 cols across the same 7 v1 tables). 7 tables: order_header (root commercial-order record — quote/order/special_order/preorder, full autonomy treatment, accepted_by text preserved AND joined by a new accepted_by_actor_id), order_line (Hard Contract 1 6-field price snapshot, stock_reservation_id real FK, full autonomy pack), order_payment (deposit/milestone/balance schedule, pos_sale_payment_id real FK, full autonomy pack), order_fulfillment (fulfillment batch header, ship_to_address_id real FK, full autonomy pack), order_fulfillment_line (byte-for-byte unchanged from v1, zero autonomy cols), order_template (full autonomy pack), order_template_line (byte-for-byte unchanged, zero autonomy cols). 3 real (not deferred) cross-module seams at lock: order_line.stock_reservation_id → inventory.stock_reservation, order_header.fulfilled_sale_id → pos.sale (ON DELETE RESTRICT, live-tested), order_payment.pos_sale_payment_id → pos.sale_payment — Pricing's Hard Contract 1 now SATISFIED by both pos and orders. +1 col 2026-07-08 (Remediation Phase 4 Item 15): order_header.entity_id (nullable FK → platform.legal_entity, one of 10 header tables across the codebase carrying this column). 173→174 cols. Reopened again 2026-07-10 for Header/Line Remediation fix #6 (PROJECT_DECISIONS #50) — a 4th real seam: order_line.sale_line_id (nullable, composite FK → pos.sale_line(id, tenant_id), order_line_sale_line_id_tenant_fkey) — line-level fulfillment linkage, link-don't-convert, mirroring order_header.fulfilled_sale_id's own established asymmetry (no reciprocal column on POS's side either); the prerequisite UNIQUE(id, tenant_id) on pos.sale_line was laid in the prior batch's POS reopen (fix #8, PROJECT_DECISIONS #46) anticipating exactly this. Independent verification found the fix fully correct and disclosed 2 findings: a test-cleanup bug (since fixed via a tryDelete() helper) and a new, previously undisclosed gap — 5 bare (non-composite) FKs remain inside orders' own internals (order_line.order_id/order_payment.order_id/order_fulfillment.order_idorder_header, order_fulfillment_line.order_fulfillment_idorder_fulfillment, order_template_line.order_template_idorder_template), none of whose parents yet carry UNIQUE(id, tenant_id) — logged to OPEN_ITEMS, not fixed this pass. 174→175 cols. Reopened again 2026-07-11 for a Returns-module companion fix (PROJECT_DECISIONS #61): order_header/.order_line each gained UNIQUE(id, tenant_id) — the prerequisite for the new returns schema's own composite FKs into Orders — constraint-only, zero column/table impact, still 7 tables/175 cols. Reopened again 2026-07-20 for Gap-Fill B10 (PROJECT_DECISIONS #74): order_payment.status CHECK widened to add 'forfeited', guarded by a new terminal-state trigger (trg_order_payment_guard_status) — a customer-forfeited deposit is now distinguishable from cancelled/refunded. CHECK + trigger only, zero column/table impact, still 7 tables/175 cols. The deeper balance/billing semantics remain OrderService's own future decision. Consumes identity.agent_duty_grant for all agent authority (no new authority mechanism introduced). Schema-only this pass — no OrderService yet. See PROJECT_DECISIONS #29/#40/#50/#61/#74. inventory, crm, pricing, pos, multi_loc, identity, shared
Purchasing purchasing 15 353 SCHEMA LOCKED 2026-07-07 (module #16), reopened 2026-07-08 for Remediation Phase 4 (Items 15/17b), 2026-07-10 for Header/Line Remediation fixes #10/#12/#4 (PROJECT_DECISIONS #47), and again 2026-07-10 for the Receiving extraction (PROJECT_DECISIONS #55) — the BUY path, completing the supply loop opposite orders/pos (supersedes the stale v1-carryover 16/341; +57 cols at lock, zero tables consolidated or dropped). Second module built under the Design-Phase Integrity rules. Vendor master (vendor+linked_customer_id → crm.customer, vendor_contact, vendor_address global-reshaped, vendor_item), POs (purchase_order, purchase_order_line dual-UOM cost snapshot, templates), receiving (purchase_receipt+idempotency_key, purchase_receipt_line), 3-way match (vendor_invoice no-'paid'/Billing-owns-payment, vendor_invoice_line, vendor_invoice_match N:M), credit/return (vendor_credit, vendor_credit_lineNEW 2026-07-10 —, vendor_return, vendor_return_line; all LIGHT except the new line table, ZERO). The receiving seam writes inventory.stock_movement (movement_type='received', REAL FK, +avg-cost update); vendor_return_line writes returned movements; purchase_order.source_order_id → orders.order_header AND the reciprocal orders.order_header.draft_po_id → purchasing.purchase_order CLOSED this build. 7 FULL / 8 LIGHT / 2 ZERO autonomy tiers; the reorder→PO draft loop is the flagship buy-side agent path (PO-send stays human-gated — the C8 financial boundary). +4 cols 2026-07-08 (Remediation Phase 4): vendor.payment_terms_id + purchase_order.payment_terms_id (nullable FKs → shared.payment_terms_catalog, Item 17b) + purchase_order.entity_id + vendor_invoice.entity_id (nullable FKs → platform.legal_entity, Item 15). 398→402 cols. 2026-07-10 reopen (Header/Line Remediation, 2nd of 3 — POS → Purchasing → Platform): chk_purchase_order_line_quantity_rollup on purchase_order_line (fix #10, mandatory pre-migration audit found zero violating rows live); line_number partial-unique indexes on purchase_receipt_line/vendor_invoice_line/vendor_return_line/purchase_order_template_line (fix #12, mandatory dedup audit found zero duplicates on all 4); new table vendor_credit_line (fix #4 — 8 cols, write-once, 3 composite FKs into vendor_credit/vendor_invoice_line/vendor_return_line, each of which needed its own new prerequisite UNIQUE(id, tenant_id), plus a header-is-truth reconciliation trigger, trg_vendor_credit_line_validate_against_credit, capping SUM(lines.amount_cents) at the parent's credit_amount_cents). 402→410 cols, 16→17 tables. Reopened again 2026-07-10 for the Receiving extraction (PROJECT_DECISIONS #55)purchase_receipt (30 cols) and purchase_receipt_line (27 cols) MOVED out entirely to a new receiving schema as goods_receipt (33 cols, +3 new: voided_at/voided_by_actor_id/void_reason) and goods_receipt_line (29 cols, +2 new: stock_movement_line_id/reversal_of_goods_receipt_line_id), closing the fix #7 (movement-line linkage) and fix #11 (over-receipt tolerance) gaps this row's own prior entries named as pending the Receiving extraction. vendor_invoice_match.purchase_receipt_line_id and vendor_return_line.purchase_receipt_line_id are both RENAMED goods_receipt_line_id and upgraded from bare to composite FK → receiving.goods_receipt_line(id, tenant_id); vendor/vendor_address/purchase_order/purchase_order_line each gained UNIQUE(id, tenant_id), the prerequisite for receiving's own new composite FKs pointing back into this module. Every remaining table's own column count is unchanged — the entire delta is the 2 moved tables' own combined column count. 410→353 cols, 17→15 tables. Consumes identity.agent_duty_grant, no new authority mechanism. Schema-only — no PurchasingService yet. See PROJECT_DECISIONS #30/#40/#47/#55. inventory, orders, crm, shared, multi_loc, identity, ai, receiving
Receiving receiving 2 63 SCHEMA LOCKED 2026-07-10 (PROJECT_DECISIONS #55) — extracted out of purchasing, the physical receiving-against-PO step between PO issuance and Purchasing's own 3-way-match/credit-return tables. 2 tables: goods_receipt (33 cols, moved+renamed from purchasing.purchase_receipt's 30 cols, +3 new: voided_at/voided_by_actor_id/void_reason — closes a real pre-existing gap where 'void' was already a valid status value with zero attribution, via new CHECK chk_goods_receipt_voided_requires_actor_at) and goods_receipt_line (29 cols, moved+renamed from purchasing.purchase_receipt_line's 27 cols, +2 new: stock_movement_line_id + the self-referencing reversal_of_goods_receipt_line_id; inspection_status CHECK widened to add 'quarantine'). Closes 2 fixes Purchasing's own prior reopens had named as pending this extraction: fix #11 (over-receipt tolerance) — a new trigger, trg_goods_receipt_line_check_over_receipt_tolerance (scoped to BEFORE INSERT OR UPDATE OF accepted_qty only (corrected post-independent-verification from an initial OF over_short_qty scoping, once over_short_qty became a derived rather than caller-supplied value) — a deliberate narrowing from the design doc's own literal wording, so a later unrelated edit to an already-reviewed line can't silently re-flip a human review decision), reads admin.tenant_setting/setting_definition with site-scoped > tenant-wide > catalog-default precedence (2 new setting_definition rows, category receiving) and either flags (default) or blocks the line depending on tenant config; and fix #7 (movement-line linkage)goods_receipt_line.stock_movement_line_id, a composite FK → inventory.stock_movement_line(id, tenant_id), superseding the older header-grain stock_movement_id (renamed from inventory_movement_id, deprecated in place per convention). Capped write-back (BLOCKER 1 from design-phase verification): PO-line received_qty advances by LEAST(accepted_qty, ordered_qty − received_qty − invoiced_qty − cancelled_qty) only, never raw accepted_qty, keeping purchasing.chk_purchase_order_line_quantity_rollup satisfied even on an over-tolerance receipt; over_short_qty = accepted_qty − absorbed_qty captures the excess. Reversal is a compensating goods_receipt_line (via reversal_of_goods_receipt_line_id) plus a compensating inventory.stock_movement_line carrying a NEGATIVE quantity_delta under the same stock_movement.correlation_id — the original append-only movement rows are never edited or deleted, and the compensating line's own received_qty/accepted_qty stay a positive magnitude (the sign lives entirely in the linked movement, not in goods_receipt_line itself). 13 composite (col, tenant_id) → parent(id, tenant_id) FKs, all individually live-reproduced for cross-tenant rejection. 3 real bugs found and fixed during this build's own live-reproduction pass (beyond the 2 BLOCKERs design-phase independent verification already caught pre-build): a missing UNIQUE(id, tenant_id) on goods_receipt itself (needed for goods_receipt_line's own composite FK into it, fixed in both migration and Drizzle source); an admin.custom_field_definition CHECK-widen/backfill ordering bug (11 test-fixture rows on the old 'purchasing.purchase_receipt' entity_type value — DROP CHECK, backfill, then re-ADD, not backfill-then-widen); and a defensive-cast gap in the tolerance trigger (a corrupted, non-numeric catalog value would have crashed the trigger instead of failing safe — fixed via a BEGIN/EXCEPTION WHEN invalid_text_representation fallback to 0% tolerance). 32 live-reproduction guard assertions across 6 sections, all pass, run inside one transaction and rolled back, zero residue. Reopened 2026-07-20 for Gap-Fill B3 (PROJECT_DECISIONS #74): purchase_order_id relaxed nullable + new receipt_source (purchase_order/direct) CHECK; goods_receipt_line.purchase_order_line_id ALSO relaxed nullable (a scope correction found mid-build) with a new cross-table trigger enforcing source/line-ref coherence at the line level — enabling walk-in/off-PO vendor deliveries. vendor_id was already NOT NULL. 62→63 cols, still 2 tables. Entitlement-bundled to Purchasing's own toggle (no independent toggle exists — no per-module entitlement mechanism exists anywhere in this codebase yet). Consumes identity.agent_duty_grant, no new authority mechanism. Schema-only — no ReceivingService yet. purchasing, inventory, multi_loc, identity, admin
Tax tax 3 47 SCHEMA LOCKED 2026-07-07 (module #17), reopened 2026-07-08 for Remediation Phase 3 Item 9 (PROJECT_DECISIONS #39) and again for Remediation Phase 4 Items 15/17c (PROJECT_DECISIONS #40) — NEW module, zero v1 precedent (v1 assumed tax was 100% outsourced to Stripe Tax, no local schema at all — confirmed via find docs/old -iname "*tax*" returning zero results). Closes pos's own PRE-CUSTOMER OPEN_ITEMS gap: sale_line's collapsed flat tax_amount_cents/tax_rate had no per-jurisdiction breakdown. tax_calculation (28 cols, FULL — the nexus/rate-anomaly-flag surface, never a rate-computing one; automation_source defaults 'system', the codebase's first justified deviation from 'human') + tax_calculation_jurisdiction (9 cols, ZERO, APPEND-ONLY — no updated_at/deleted_at at all, restores v1's own state/county/city/district/special granularity). source_ref polymorphic → pos.sale_line/orders.order_line (link-don't-require-reciprocal, no reopen). No local rate/jurisdiction master tables — Stripe Tax owns rates, Admin owns nexus config. supersedes_calculation_id scoped to same-source corrections only, DB-enforced by trg_tax_calculation_validate_supersession (mirroring pricing.trg_price_rule_validate_supersession — added same-day after post-lock independent verification found the scoping was initially documented-only); an orders-time estimate and a pos-time final deliberately coexist, reconciled at report time via the header-level fulfilled_sale_id join (not auto-superseded — an adversarial design-phase pass caught an earlier draft over-claiming this). +2 cols 2026-07-08 (Remediation Phase 3 Item 9): calculation_type ('original'/'reversal') + reversed_calculation_id (self-FK) — a refund's tax reversal is now representable with sign-aware CHECKs so SUM(original+reversal) nets to zero for remittance reporting; source_pair widened to accept (pos, sale_refund_line); trg_tax_calculation_jurisdiction_validate_sign closes the per-jurisdiction sign-consistency gap (this table is INSERT-only, so a BEFORE INSERT trigger suffices — no CHECK could reference the parent's calculation_type). +1 table/+8 cols 2026-07-08 (Remediation Phase 4): jurisdiction_level_catalog (6 cols, 6 seeded rows incl. country) + tax_calculation_jurisdiction.jurisdiction_level_id, plus a REAL widen of chk_tax_calculation_jurisdiction_level to add 'country' (closing the VAT/GST gap — this same-named CHECK's pre-Phase-4 definition genuinely lacked it) + tax_calculation.entity_id (nullable FK → platform.legal_entity). 39→47 cols, 2→3 tables. Consumes identity.agent_duty_grant. Schema-only — no TaxService yet. See PROJECT_DECISIONS #31, #39, #40. pos, orders, crm, shared, multi_loc, identity
Billing billing 16 260 SCHEMA LOCKED 2026-07-07 (module #18), reopened 2026-07-08 for Remediation Phase 4 Items 15/16 (PROJECT_DECISIONS #40), again 2026-07-10 for Header/Line Remediation fix #2 (PROJECT_DECISIONS #51), and again 2026-07-18 for the Phase 3 stored-value build (PROJECT_DECISIONS #71) — the SETTLE half of the financial layer (tax calculates, billing settles), now also the home of the tenant's stored-value liability. Up from the stale placeholder's 8/113 — all 8 v1 tables preserved 1:1 (ar_account 20 FULL, ar_charge 26 FULL, ar_payment 23 FULL, ar_payment_application 9 LIGHT append-only, ar_statement 21 LIGHT, vendor_payable 17 LIGHT, ap_payment 17 LIGHT, ap_payment_application 9 LIGHT append-only) + ar_adjustment (22 cols, FULL, NEW — the write-off/dispute-resolution table v1 deferred, resolved decision 1). ar_charge.tax_calculation_id → tax.tax_calculation.id is the NEW tax seam. ar_charge.source_ref/.source_payment_ref are polymorphic → pos.sale/orders.order_header and pos.sale_payment/orders.order_payment — plain uuid, NO single-table FK (structurally impossible for a two-target polymorphic column), validated by the source_module/source_type pair CHECK instead. A live NULL-distinctness bug was caught and fixed during this build's own test-writing pass (not deferred): the idempotency dedup on ar_charge is a two-partial-unique split (ar_charge_idempotency_full_unique + ar_charge_idempotency_no_payment_ref_unique), mirroring orders.order_header's own precedent — a naive single index would have let two charges for the same source_ref both land with a NULL payment_ref without colliding. Asymmetric autonomy tiering (A/R FULL, A/P LIGHT), disclosed deliberately — the real judgment for A/P already happened upstream at purchasing.vendor_invoice's own FULL gate. +2 cols 2026-07-08 (Remediation Phase 4): ar_account.entity_id + vendor_payable.entity_id (nullable FKs → platform.legal_entity, Item 15). A cross-table currency-agreement trigger (Item 16) also landed on ar_payment_application (trg_ar_payment_application_validate_currency, BEFORE INSERT only), enforcing ar_payment/ar_charge/(same-account) ar_account all share one currency — no column added, enforcement-only. 162→164 cols. 2026-07-10 reopen (Header/Line Remediation, fix #2 — 3rd of the batch's 3 reopens: Inventory #49 → Orders #50 → Billing #51): new table ar_charge_line (12 cols, write-once per-line decomposition of ar_charge.charge_amount_cents, PK platform.uuid_generate_v7()), reconciled HEADER-IS-TRUTH via trg_ar_charge_line_validate_against_charge (mirrors purchasing.vendor_credit_line's own precedent) — 2 composite FKs (→ billing.ar_charge, → tax.tax_calculation), each needing a new prerequisite UNIQUE(id, tenant_id); the tax.tax_calculation one is a cross-module prerequisite this fix needed and added (no column/table impact on tax itself). Backfill was the RECONSTRUCTED kind (no JSONB blob existed) — a mandatory dry run found zero live pos-sourced ar_charge rows share a tenant with any seeded pos.sale row (disconnected seed datasets), and the full reconstructive query was actually run, confirmed INSERT 0 0. Independent verification found the fix fully correct and disclosed 2 findings, neither invalidating it: ar_charge.tax_calculation_id was ALSO found to be a bare FK (closed in a LATER, separate migration/docs pass, not credited here) and a minor rounding-methodology note (unreachable in today's data). 164→176 cols, 9→10 tables. Reopened again 2026-07-18 for the Phase 3 stored-value build (PROJECT_DECISIONS #71): +6 tables/+84 cols — 176→260 cols, 10→16 tables — the gift-card + store-credit subsystem, placed in billing (a stored-value instrument is a LIABILITY on the tenant's books, billing's charter), reversing v1's own pos placement. TWO instruments, never merged (v1's own named never-merge GUARD — bearer instrument vs customer liability), identical ledger PATTERN: gift_card (25 cols, mutable, FULL autonomy — code_hash only, SHA-256 of a CSPRNG value, plaintext never stored — a disclosed security upgrade over v1's plaintext codes) + gift_card_transaction (14 cols, append-only money ledger, uuid_generate_v7() PK, balance_after_cents trigger-derived — cached balance maintained EXCLUSIVELY by billing.sync_gift_card_balance(), the rewards-style atomic row-locking guard) + gift_card_reversal_tracker (7 cols — the cumulative clawback cap, 4th consumer of the rewards/offers/returns tracker idiom); store_credit_account (17 cols, mutable, FULL — customer-tied, one per customer per tenant, customer_id NOT NULL as the never-merge GUARD's structural half) + store_credit_transaction (14 cols, append-only, v7 PK, billing.sync_store_credit_balance()) + store_credit_reversal_tracker (7 cols). Plus stored_value_liability — a security_invoker VIEW summing the trigger-guaranteed cached balances per (tenant, instrument kind, currency), not counted in the table total. Seams: composite FKs into crm.customer, pos.sale/sale_payment/sale_refund, a composite self-FK for clawbacks; pos.sale_payment.gift_card_id/.store_credit_id became REAL composite FKs the same migration (pos's fail-closed tender gate narrowed to 'reward'-only) and returns.return_resolution.store_credit_transaction_id points at 'issue' entries here (link-don't-reimplement). Tenant policy stays in admin.setting_definition (stored_value/cash_out_allowed, stored_value/gift_card_default_expiry_days), never baked into schema. Consumes identity.agent_duty_grant. Schema-only — no BillingService yet. See PROJECT_DECISIONS #32/#40/#51/#71. crm, purchasing, pos, orders, tax, shared, identity
Admin admin 10 122 SCHEMA LOCKED 2026-07-07 (module #20), reopened 2026-07-08 for Remediation Phase 3 Item 13 (PROJECT_DECISIONS #39), again for Remediation Phase 4 Items 15/17d/19 (PROJECT_DECISIONS #40), and again 2026-07-09 to split the tenant-side approval engine out into its own approvals schema — Admin's FIRST real v2 pass (v1 was 11 tables/145 cols, locked 2026-06-10, never rebuilt for v2 until now — supersedes the stale v1-carryover placeholder this row previously held). tenant_business_profile (17 cols) DROPPED entirely: 16 of 17 columns MOVED to platform.tenant_profile (Platform's 4th reopen); the 1 remaining column, attributes, dropped outright with no successor (zero known consumers, no defined shape) — full 17-column fate mapping in PROJECT_DECISIONS #34 Block 3. The 10 surviving tables (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 — sums to 128, matching 145−17 exactly) are a FAITHFUL PORT of v1 with exactly ONE schema-level change: 5 columns across 3 tables retargeted from identity.identity_user to identity.actor (tenant_setting.updated_by_actor_id, api_key.created_by_actor_id/.revoked_by_actor_id, approval_request.requested_by_actor_id/.resolved_by_actor_id) — 3 of these 10 tables (the approval-engine trio) later moved out entirely; see the 2026-07-09 note below. Admin does NOT own tenant identity — platform.tenant_profile is the single source of truth (PROJECT_DECISIONS #34); every table carries tenant_id → platform.tenant.id, zero identity-shaped columns anywhere. tenant_branding owns the logo (logo_ref, Files forward-ref) — platform.tenant_profile.logo_url deprecated in its favor. REVERSED 2026-07-09: DECIDED Option B (PROJECT_DECISIONS #34 §5 — the tenant-side approval engine "stays Admin-internal") is superseded. Low-autonomy module: narrow LIGHT flagging-only surfaces on hardware_device/integration_config/webhook_config/api_key; tenant_branding explicitly human-only, disclosed. (Admin's one FULL-autonomy table, approval_request, moved out in this reopen — see the approvals row above.) +1 table 2026-07-08 (Remediation Phase 3 Item 13): setting_definition (12 cols) — the config-key catalog covering tenant_setting (global reference data, no tenant_id, no RLS, mirrors identity.permission's catalog precedent); NOT enforced against tenant_setting via FK/trigger (disclosed gap, see OPEN_ITEMS); covers tenant_setting only, 4 other free-form config surfaces deliberately out of scope. +2 tables/+21 cols 2026-07-08 (Remediation Phase 4): integration_provider_catalog (7 cols, 6 seeded rows, global/no-RLS, Item 17d) + integration_config.provider_id; custom_field_definition (12 cols, tenant-scoped WITH RLS, governs the 7 confirmed ungoverned attributes JSONB columns codebase-wide, Item 19); compliance_document.entity_id (nullable FK → platform.legal_entity, Item 15). 11→13 tables, 140→161 cols as of Remediation Phase 4. Reopened a 3rd time 2026-07-09 to split the tenant-side approval engine into its own approvals schema: approval_workflow/approval_routing_rule/approval_request MOVED out via ALTER TABLE ... SET SCHEMA (−3 tables/−39 cols: approval_workflow −10, approval_routing_rule −11, approval_request −18) and extended there with 5 new tables plus hard autonomy/security guards — see the approvals row above. Admin's remaining 10 tables (tenant_branding, compliance_document, tenant_setting, setting_definition, hardware_device, integration_config, integration_provider_catalog, webhook_config, api_key, custom_field_definition) are UNCHANGED by this reopen. 13→10 tables, 161→122 cols. Schema-only — no AdminService yet. See PROJECT_DECISIONS #34/#36/#39/#40. platform, multi_loc, identity
Audit audit 7 118 Cross-cutting integrity-chained audit log + compliance workflow (GDPR/CCPA DSR, breach incidents, DPA, subprocessors, exports). platform
Notifications notifications 18 252 SCHEMA LOCKED 2026-07-19 (module #29) — the first STALE-BUT-UNBUILT module revival (SCHEMA_DESIGN_RUNBOOK 2.1a): v1 was schema-locked 2026-06-10 (11/166) and never migrated. Full v1→review→v2→16-architect-rulings→11-finding-adversarial-verification→build lineage — see PROJECT_DECISIONS (Notifications entry). 18 tables/252 cols (up from the stale 11/166 placeholder — net +7 tables/+86 cols): the RULED design's 17 tables/246 cols, plus a same-pass 18th table, platform_suppression (6 cols — a NEW platform-owned, cross-tenant hard-bounce/spam-complaint/provider-block suppression list, service-role-only, RLS-enabled-with-zero-policies + explicit REVOKE ALL FROM authenticated, closing Part B Finding #10's disclosed shared-domain-suppression-poisoning risk). notification_template (16, autonomy pack), notification (33, the convergence table — outbox_event_id/is_test/printed_at/reprint_of_notification_id ADD, status CHECK widened incl. 'suppressed', UNIQUE(id,tenant_id), autonomy pack), delivery_attempt (21, no autonomy — deterministic execution; status CHECK widened to 7 values with a monotonic BEFORE UPDATE OF status guard, no-op-on-duplicate not error), notification_preference (13, unchanged), in_app_notification (20, autonomy pack), notification_quota_usage (9, unchanged), inbound_message (15, +vendor_id), notification_journey/journey_step/journey_enrollment (11/14/15, KEPT verbatim, dormant — out of v1 capability scope, no guard added), campaign (21, +customer_group_id/customer_segment_definition_id (bare FK + validation trigger, mirroring offers.offer_targeting_rule's precedent — customer_segment_definition.tenant_id is nullable)/offer_id, autonomy pack, draft_only gate tying status/review_status), campaign_recipient (8, NEW — the pre-send audience snapshot + campaign-grain dedup), notification_frequency_tracker (8, NEW — atomic per-customer marketing-cap enforcement reading admin.tenant_setting), provider_event_log/provider_event_dead_letter (9/10, NEW — mirror payments.stripe_event_log/stripe_event_dead_letter exactly, confirmed live to carry ZERO triggers, nullable tenant_id), suppression (10, NEW — per-tenant, UNIQUE(tenant_id,channel,address_normalized) WHERE deleted_at IS NULL), sender_identity (13, NEW — shared-domain-for-v1, BYO a structurally-ready v1.1 path). 5 companion reopens (one migration each, all additive-only): platform (outbox +UNIQUE(id,tenant_id); processor_catalog.kind widened +'notification_provider', seeded resend/twilio), orders (order_fulfillment +UNIQUE(id,tenant_id)), billing (ar_statement +UNIQUE(id,tenant_id)), purchasing (purchase_order +vendor_acknowledged_at/vendor_acknowledgement_note), and an emergency 6th, crm (customer_group +UNIQUE(id,tenant_id), discovered mid-build as a missing composite-FK prerequisite). pg_cron extension installed codebase-wide (no jobs — job definitions are service-build work). The frequency-cap trigger's original demonstration-only shape was self-caught and fixed mid-build into a genuine atomic lock-then-check-then-increment (offers precedent); a missing schema-wide GRANT/ALTER DEFAULT PRIVILEGES to authenticated was also self-caught and fixed (a real, live-reproduced permission denied for schema notifications error). Live-reproduced: suppression (per-tenant + platform-wide + unconditional-regardless-of-transactional + is_test-still-suppressed), recipient_contact post-insert immutability, monotonic status guard (regression rejected, duplicate no-op, first-write-wins opened_at/clicked_at), frequency cap (genuine 3-way concurrent race, exactly 2 commit), campaign-recipient presence guard, dedup_key NULL-safe partial unique, provider_event_log global idempotency + NULL-tenant RLS invisibility, platform_suppression hard permission-denied (REVOKE, not just RLS). Consumes identity.agent_duty_grant. Schema-only — no NotificationsService yet. platform, crm, identity, offers, purchasing, orders, billing
Reporting & Analytics reporting 3 43 Operational BI — 2 frozen financial snapshots + run header; matviews + service-layer for the rest. Terminal — reads ~15 schemas (service_role); nothing depends on it; zero forward-refs out. platform, multi_loc, inventory, identity
Returns returns 9 153 SCHEMA LOCKED 2026-07-11 (module #27, PROJECT_DECISIONS #61) — the customer RMA (return merchandise authorization) module, built as Pass 2 of a 2-pass session (Pass 1 reopened rewards/offers to fix their reversal triggers, PROJECT_DECISIONS #60 — a hard prerequisite for this build's own Architect Decision 2, proportional clawback, below). 9 tables: return_authorization (29 cols — the RMA header, FULL autonomy pack; FKs to multi_loc.site, pos.sale, orders.order_header, a bare crm.customer FK matching the universal convention, returns.warranty, returns.return_reason; chk_return_authorization_warranty_requires_type gates warranty_id to return_type='warranty_claim' only), return_authorization_line (28 cols — freezes discount/tax allocation at RA-creation time, the module's central design decision: pre_discount_extended_price_cents/allocated_discount_cents/effective_unit_price_cents/allocated_tax_cents/eligible_refund_cents/restocking_fee_cents, allocation_source CHECK line_scoped_offer/basket_allocated_offer/loyalty_allocated/none, authorized_qty/received_qty/credited_qty as INDEPENDENT peer counters — not a chain — enabling the no-physical-receipt warranty path), return_source_line_tracker (10 cols, NEW — closes a real design-verification finding: since pos.sale_line/orders.order_line are append-only, an aggregate-cap cache mirroring offers.offer_redemption_reversal_tracker's own shape caps total_authorized_qty/total_eligible_refund_cents atomically via the same UPDATE...WHERE...RETURNING pattern used elsewhere in this codebase, preventing cumulative over-return across MULTIPLE separate RMAs against the same source line), return_resolution (21 cols — the outcome header: refund/store_credit/replacement/repair/warranty_credit/reject; loyalty_reversal_ledger_id → rewards.loyalty_point_ledger / offer_reversal_redemption_id → offers.offer_redemption link to 'reverse'-typed rows written using rewards/offers' OWN now-proportional mechanism — returns never re-implements loyalty/offer math, crediting Pass 1/#60; pos_sale_refund_id → pos.sale_refund required when resolution_type='refund' via CHECK), return_resolution_line (8 cols, APPEND-ONLY — write-once financial fact mirroring billing.ar_charge_line/purchasing.vendor_credit_line, capped by a trigger summing resolved_amount_cents across ALL siblings sharing the same return_authorization_line_id against that line's own eligible_refund_cents; sale_refund_line_id reconciles against pos.sale_refund_line's own authoritative amount when populated), return_receipt (14 cols, mirrors receiving.goods_receipt), return_receipt_line (16 cols, mirrors receiving.goods_receipt_line, mutable — the posting trigger derives the absorbable qty via SELECT...FOR UPDATE and posts inventory.stock_movement/.stock_movement_line atomically; TOLERANCE POLICY IS BLOCK, NOT FLAG, a deliberate deviation from receiving's own "flag" default since a customer returning goods twice is more dangerous to leave un-gated; reversal_of_return_receipt_line_id self-referencing FK mirrors goods_receipt_line's own precedent), return_reason (10 cols, tenant-scoped catalog mirroring inventory.stock_adjustment_reason exactly), warranty (16 cols, REVIVED v1 pos.guarantee — 13 cols verbatim, 2 renames variant_id→item_variant_id/guarantee_type→warranty_type, 1 retarget claimed_refund_id→claimed_via_resolution_id since a plant-guarantee claim resolves as a REPLACEMENT not a refund, correcting v1's own refund-only assumption). 3 new BEFORE INSERT trigger functions: check_and_reserve_source_line() (the aggregate cap), validate_and_apply_resolution_line() (the resolution-line cap + sale_refund_line reconciliation + credited_qty maintenance), post_and_cap_return_receipt_line() (derives absorbed qty + posts the stock movement atomically + BLOCK-not-flag tolerance). 5 companion reopens bundled into this same migration, all constraint/CHECK-shape only, ZERO column/table count impact on any of the 5 (see their own rows above): pos.sale_refund/.sale_refund_line and orders.order_header/.order_line each gained UNIQUE(id, tenant_id); inventory.stock_movement.source_module, approvals.approval_request.source_module, and files.attachment.entity_type CHECKs each widened to accept a returns-related value. The design's own originally-planned rewards/offers reopens (2 new partial-unique indexes) are OBSOLETE and were NOT applied — Pass 1/#60 already built the correct proportional mechanism. 14 sub-guards plus a genuine 3-way concurrency race, all live-reproduced against the real local DB: discount allocation (a $150 basket/20%-off return correctly refunds $80, not $100), aggregate over-return cap (sequential + a genuine 3-way concurrent race, exactly 2 of 3 succeed when only capacity for 2 exists), proportional clawback (5 shrubs/100 points/$10 offer, return 2 of 5 → 40 points + $4.00 clawed back, additive on a 2nd partial return, over-clawback rejected — the capability this whole 2-pass effort exists for), idempotent inventory posting + BLOCK-not-flag over-receipt rejection, warranty no-physical-receipt flow (credited_qty reaches authorized_qty while received_qty stays 0), restocking fee arithmetic ($40 effective − $5 fee = $35, never separately taxed), unreferenced/blind return (anonymous walk-in, full risk-tiering), sale_refund stays referenced not absorbed, cross-tenant RLS rejection (42501). 31/31 new tests (returns-schema.spec.ts); full apps/api suite 1083/1083 passing (one pre-existing, already-disclosed, unrelated cross-file concurrency flake in admin-tenants.spec.ts, confirmed not caused by this build). Inspected 2026-07-18 as part of Phase 2 (Nursery Vertical Extraction, PROJECT_DECISIONS #70): returns.warranty was reviewed and confirmed already fully generic — nothing extracted, zero schema change, still 9 tables/152 cols. Reopened 2026-07-18 for the Phase 3 stored-value build (PROJECT_DECISIONS #71): +1 col — 152→153return_resolution.store_credit_transaction_id (nullable composite FK → billing.store_credit_transaction(id, tenant_id)), the real ledger pointer to the 'issue'-typed store-credit entry a resolution created (link-don't-reimplement, same direction as loyalty_reversal_ledger_id); a new CHECK (chk_return_resolution_store_credit_requires_transaction) REQUIRES it for store_credit/warranty_credit resolutions, mirroring pos_sale_refund_id's own enforced refund pattern. The loose free-text store_credit_reference is DEPRECATED IN PLACE (0 non-NULL rows at deprecation; drop-trigger: after ReturnsService ships reading only the new column) — this closes the module's own §9 deferred store-credit item now that billing.store_credit_account/store_credit_transaction exist. Consumes identity.agent_duty_grant for all agent authority (no new authority mechanism introduced). Schema-only — no ReturnsService yet. platform, multi_loc, pos, orders, crm, inventory, rewards, offers, identity

Consumer Layer

Module Schema Tables Cols Owns Depends on
Consumer consumer 10 104 SCHEMA LOCKED 2026-07-11 (PROJECT_DECISIONS #56) — the Consumer Layer's first real v2 build (supersedes the stale v1-carryover placeholder count this row previously held, 4 tables/47 cols, never a real v2 build). Platform-level person account (Model B): consumer (17 cols — identity root, supabase_auth_user_id the auth seam, NEW this build per Finding 8), consumer_identifier (9 cols — multi-identifier model: multiple emails/phones/social IDs per consumer, partial-unique on (identifier_type, identifier_value) WHERE superseded_at IS NULL), consumer_address (13 cols), consumer_interest (8 cols), consumer_consent (9 cols, append-only, platform-level consent — distinct from crm.customer_consent's merchant-level record), identity_merge_event (9 cols, append-only, audits but does not prevent mis-merges), identity_map (6 cols, anonymous_id → consumer_id resolution), consumer_feature (11 cols, RFM/recommendation cache) — these 8 tables are consumer-scoped RLS, REVOKEd from the merchant authenticated role entirely, reachable only via the new consumer_authenticated Postgres role (see below). Plus 2 deliberate exceptions, tenant-scoped and merchant-accessible: consumer_merchant_link (12 cols, renamed from v1's consumer_tenant_link — the consumer's own record of which nurseries they're linked to, complementary to crm.customer.consumer_id) and event (10 cols, append-only — the engagement spine: every purchase/cart/view/email/loyalty/offer/search touchpoint, site_id composite FK → multi_loc.site). NON-TENANT-SCOPED overall (third non-tenant schema after platform and shared) — consumer_merchant_link/event are the only 2 tenant-scoped exceptions. The consumer_authenticated role mechanism: NOT a Supabase Custom Access Token Auth Hook (investigated, found structurally viable in Supabase generally but this application's apps/api never routes through PostgREST's auto-role-switching) — a parallel consumerDB() helper applies the same SET LOCAL ROLE/set_config() pattern tenantDB() already uses, to a new NOLOGIN NOINHERIT consumer_authenticated role. Cross-tenant reads (a consumer's loyalty balances/offers across every linked merchant) go through consumer.get_cross_tenant_activity(p_consumer_id uuid), a SECURITY DEFINER function taking the id as a parameter, never an ambient session GUC. 47→104 cols, 4→10 tables. Reopened 2026-07-18 for Phase 2 (Nursery Vertical Extraction, PROJECT_DECISIONS #70): consumer_interest.interest_type CHECK widened from category/item to also accept topic, generalizing a nursery-specific vocabulary the vertical-neutral core had accidentally absorbed — constraint-only, zero column/table impact, still 10 tables/104 cols. Consumes identity.agent_duty_grant for actor attribution on event only — the 8 consumer-scoped tables carry no actor attribution (a consumer acts for themselves). Schema-only — no ConsumerService yet. platform, shared, multi_loc, crm, pos
Rewards rewards 7 109 SCHEMA LOCKED 2026-07-11 (PROJECT_DECISIONS #56), reopened same day for a proportional/partial-reversal capability (PROJECT_DECISIONS #60) — per-business loyalty (supersedes the stale v1-carryover placeholder count this row previously held, 6 tables/89 cols, never a real v2 build). loyalty_program (14 cols), loyalty_accrual_rule (22 cols — multiplier/flat-bonus earning rules), loyalty_reward_tier (12 cols), reward_option (27 cols — the points-cost redemption catalog), loyalty_account (14 cols, one per consumer×program, balance_points/lifetime_points both CHECK-guarded non-negative), loyalty_point_ledger (22 cols, append-only, sale_id composite FK → pos.sale(id, tenant_id), the real seam this build closes — riding a new pos.sale UNIQUE(id, tenant_id) prerequisite). The concurrency-race fix, this build's central guard: rewards.sync_loyalty_account_balance() is a single atomic BEFORE INSERT trigger whose own UPDATE ... RETURNING balance_after_points takes a row lock, serializing concurrent redemptions — live-reproduced against a naive two-trigger (BEFORE-check/AFTER-sync) shape that let a 100-point balance overshoot to -60 under 2 concurrent -80-point redemptions; the fixed shape correctly resolved 3-of-5 concurrent -30-point redemptions against the same 100-point balance to exactly 10, rejecting the other 2. TENANT-SCOPED, standard authenticated RLS, consumer_id a dimension (not the RLS scope) — inverse of consumer.consumer_merchant_link. Fully tenant-scoped and merchant-authenticated-accessible — consumer_authenticated gets zero direct grant on any table in this schema. Reopened same day (2026-07-11, PROJECT_DECISIONS #60): sync_loyalty_account_balance() originally hard-enforced EXACT FULL NEGATION ONLY on a 'reverse' entry, rejecting a genuine partial reversal (e.g. return 2 of 5 units) outright; proportional/partial reversal is now a supported first-class capability via a new mutable tracker table, loyalty_point_ledger_reversal_tracker (7 cols — id/tenant_id/ledger_id/original_amount_points/total_reversed_points/created_at/updated_at, tenant-scoped RLS, composite FK to loyalty_point_ledger, UNIQUE(tenant_id, ledger_id)), lazily created and capped via the same atomic row-locking UPDATE ... WHERE total + :amt <= original RETURNING ... pattern this codebase already uses elsewhere (offers' own budget-cap UPDATE, receiving's capped write-back trigger) — a separate table rather than a maintained column, since loyalty_point_ledger is append-only via platform.reject_append_only_mutation() (an unconditional RAISE EXCEPTION, confirmed via pg_get_functiondef) and would reject any in-place UPDATE; live-reproduced incl. a 3-concurrent-reversal race (exactly 2 of 3 succeeded, cumulative capped correctly). 102→109 cols, 6→7 tables. Consumes identity.agent_duty_grant for all *_actor_id autonomy attribution. Schema-only — no RewardsService yet. platform, consumer, pos, identity
Offers offers 7 127 SCHEMA LOCKED 2026-07-11 (PROJECT_DECISIONS #56), reopened same day for a real bug fix plus a proportional-reversal capability (PROJECT_DECISIONS #60) — business-issued discount/promo offers, this build's own AI-authored-offer surface (supersedes the stale v1-carryover placeholder count this row previously held, 4 tables/68 cols, never a real v2 build). offer (43 cols, live-confirmed — not the 42 this row previously stated; see the disclosed count correction below — funding_source business/vrida flag, provenance human_defined/ai_suggested/ai_auto_created, chk_offer_ai_requires_guardrail structurally forbids a non-human-defined offer with zero discount guardrails), offer_code (13 cols), offer_assignment (15 cols — issued/viewed/claimed/redeemed/expired/cancelled lifecycle), offer_redemption (15 cols, append-only, sale_id composite FK → pos.sale(id, tenant_id) NOT NULL — v1's own "THE REDEMPTION SEAM" — sale_line_id composite FK → pos.sale_line, nullable, the margin-floor verification target), offer_targeting_rule (21 cols — NEW, segment_definition_id a plain FK → crm.customer_segment_definition with cross-tenant integrity DB-enforced by a dedicated trigger, offers.validate_offer_targeting_rule_segment(), mirroring pricing.trg_price_rule_validate_supersession's precedent, rather than a composite FK), customer_discount_exposure (9 cols — per-consumer discount-exposure tracking). offers.check_and_sync_offer_budget() merges 3 concerns into one atomic BEFORE INSERT trigger: margin-floor check (fails CLOSED against a zero/NULL avg_cost_cents, never silently skips), the max_discount_percent/max_discount_amount_per_order guardrail check, and the budget-cap check + budget_used_cents/offer_code.redeemed_count sync — the same atomic-UPDATE-takes-a-row-lock pattern as Rewards' own fix, closing the identical concurrency-race class. Refund clawback: redemption_type='reverse' requires a sign-flipped negative amount, a non-NULL note, and a reversed_redemption_id back-reference — live-reproduced correctly walking budget_used_cents/redeemed_count back down through the same trigger. TENANT-SCOPEDfunding_source flag; Vrida-funded settlement to platform deferred ('vrida' guarded, not built); consumer_authenticated gets zero direct grant on any table here either. 68→115 cols, 4→6 tables at original lock. 2 disclosed Finding-6-class arithmetic corrections at build time: offer_targeting_rule built as 21 cols not the design doc's stated 17 (its own itemized list undercounted itself by the standard created_at/updated_at/deleted_at/created_by_actor_id set every sibling autonomy-pack table carries). Reopened same day (2026-07-11, PROJECT_DECISIONS #60) for a REAL BUG: check_and_sync_offer_budget() had NO magnitude check on a reversal — only a sign check — so a $10 redemption could be "reversed" by a fabricated row claiming a $1000 discount, silently zeroing budget_used_cents by consuming an unrelated redemption's own budget (live-reproduced against the exact pre-fix function body: a padded $10 redemption plus a legit $990 one, "reversed" by a fabricated -$1000 row, succeeded pre-fix). A new mutable tracker table, offer_redemption_reversal_tracker (7 cols, composite FK to offer_redemption, UNIQUE(tenant_id, redemption_id)), now caps the cumulative reversed amount atomically (the same row-locking UPDATE ... WHERE total + :amt <= original RETURNING ... pattern as Rewards' own fix), so proportional/partial reversal now works correctly with a real cumulative cap — redemption_count/offer_code.redeemed_count only decrement by 1 once a reversal's cumulative total exactly equals the original discount amount, preventing fractional count changes on a partial reversal; live-reproduced incl. a 3-concurrent-reversal race (exactly 2 of 3 succeeded, cumulative capped correctly). Also discloses a pre-existing, this-fix-unrelated column-count drift, found while reconciling counts for this reopen: of the resulting +8 cols, +7 is the new tracker table and +1 is NOT caused by this fix — offers.offer is live-confirmed at 43 cols, not the 42 this row had stated since the original 2026-07-11 build; the extra column, redemption_count, was actually added by that same build's own same-day lock-gate verification fix pass (PROJECT_DECISIONS #57) and simply never got reflected in this row's column count at the time — disclosed plainly, not silently folded into the math, matching this doc's own established disclosure convention (e.g. shared's 12-column Phase 4 drift, above). 115→123 cols (+8: +7 new table, +1 the disclosed pre-existing drift), 6→7 tables. Reopened 2026-07-18 for Phase 2 (Nursery Vertical Extraction, PROJECT_DECISIONS #70): offer_targeting_rule.growing_zone_code RENAMED attribute_ref (value 'growing_zone' renamed 'attribute_match'), and its FK to shared.climate_zone DROPPED — the generic offer engine no longer structurally depends on any vertical's reference data. Column-rename/constraint-only, zero column-count/table impact, still 7 tables/123 cols. Reopened 2026-07-20 for Gap-Fill B6 (PROJECT_DECISIONS #74): a new, additive discount_type='buy_x_get_y' (bxgy_qualifying_qty/bxgy_reward_qty/bxgy_reward_variant_id/bxgy_reward_discount_pct, own coherence CHECK) — quantity-gated BOGO ("buy 3 get 1 free/50% off"); the pre-existing 1-for-1 bogo value is untouched. Multi-item bundle pricing stays deferred, not built. A real, previously-undiscovered bug found only via this reopen's own live-reproduction step: the pre-existing chk_offer_discount_type_coherence had no branch for the new value at all, so every buy_x_get_y insert would have unconditionally failed it — fixed same-day in a follow-up migration. 123→127 cols, still 7 tables. Consumes identity.agent_duty_grant. Schema-only — no OffersService yet. platform, consumer, pos, crm, multi_loc, identity

Unbuilt / Future Modules

Later phases

Module Schema Notes
Production / Propagation production Pure nursery-vertical — non-nursery deployments omit this module entirely. Depends on inventory, admin.
Delivery & Logistics delivery Depends on orders, admin.
Services & Jobs services Depends on crm, admin.

Consumer Layer (remaining 1 module — direction only, no spec yet)

Module Schema Notes
Consumer App consumer_app Consumer app experience. Supersedes nursery-only 08_customer_app.md direction.

Dependency Build Order

Recommended order. Foundation first; nursery-facing modules layer on top.

Ordering Rationale

Five principles shape the sequence:

  1. Foundation firstplatform, identity, and shared migrate first because every other module depends on tenant identity, user/authZ, and global reference data. No downstream schema can migrate before its upstream dependencies exist.
  2. Close seams while fresh — schedule each module soon after the modules whose seams it closes. billing follows pos/orders (charge-account seams); payments follows the Stripe-seam modules; integrations follows notifications (provider seams); files follows the R2-seam modules. Deferred FKs accumulate context debt — closing them late means reconstructing decisions.
  3. AI mid-service-layerai sits alongside the cross-cutting services (not last) because the onboarding import pipeline (import_job/import_file/import_record) is a hard dependency of platform.tenant_setup_task. The pipeline must be functional before the core build proceeds.
  4. Reporting near-lastreporting reads from every operational module, AI, and Files. Building it early would mean constant revision as upstream tables lock. It is terminal (nothing in the core build depends on it); build it after all upstream schemas are stable.
  5. Consumer layer last, separate phaseconsumer, rewards, offers, consumer_app are a distinct product surface (Model B) layered on the locked merchant core. The core does not depend on them; they depend on the core (especially POS for rewards earn). A later phase. (consumer/rewards/offers all held stale v1-carryover placeholder schema counts from 2026-06-11/06-12 until their real v2 build landed 2026-07-11 (PROJECT_DECISIONS #56); consumer_app remaining.)

1. Foundation & Platform:

  1. platform — Vrida's control plane (tenant, subscriptions, entitlements)
  2. identity — authorization layer (users, roles, permissions, sessions)
  3. shared — global reference data (seed-managed, no tenant dependency) 3a. nursery_ref — global, Vrida/AI-curated botanical reference data (locked 2026-07-18, relocated out of shared; depends only on shared for a locale FK)
  4. paymentsPaymentsService over Stripe Connect; required for trial-to-paid
  5. integrations — external-connector runtime (QuickBooks first); depends on admin config

2. Cross-cutting services (build alongside the modules that use them):

  • aiAIService over Bedrock; AI import pipeline + Bedrock-call log (locked 2026-06-11)
  • files — R2 metadata + access control (locked 2026-06-11)
  • search — Postgres FTS + pg_trgm dual-GIN; SearchService abstraction (locked 2026-06-11 — zero tables, 6 FTS touches)

3. Tenant configuration: 6. admin — tenant settings, branding, tax config, integration/webhook config (depends on identity; HR/labor out of scope)

4. Nursery-facing: 7. multi_loc — site concept (foundation for all site_id FKs) 8. crm — customer table (tenant-scoped, in crm schema) 9. inventory — item catalog, variants, stock 9a. nursery — tenant-scoped nursery-vertical extension, item_profile/site_profile (locked 2026-07-18, PROJECT_DECISIONS #70; depends on platform, inventory, multi_loc, nursery_ref) 10. pricing — price lists, rules, assignments (depends on inventory + crm) 11. pos — in-store transactions (depends on inventory, crm, pricing, payments) 12. orders — quotes/orders (depends on inventory, crm, pricing) 13. purchasing — vendor POs, receiving, bills (depends on inventory, integrations) 14. billing — A/R + A/P control (depends on crm, payments) 15. audit — integrity-chained log + compliance workflow 16. notifications — delivery orchestrator (locked 2026-07-19 — 18 tables, 252 cols; depends on platform, crm, identity, offers, purchasing, orders, billing — integrations/audit dependency-blocked, not built) 17. reporting — terminal BI module (locked 2026-06-11 — 3 tables, 43 cols; reads ~15 schemas)

Later / deferred modules: 18. production — propagation (nursery-vertical only) 19. multi_loc — transfers, cross-site fulfillment (deferred) 20. delivery — logistics 21. services — services & jobs 22. consumer — consumer identity (real v2 schema locked 2026-07-11, PROJECT_DECISIONS #56; ConsumerService build when Consumer Layer service-layer phase begins) 23. rewards — per-nursery loyalty (real v2 schema locked 2026-07-11, PROJECT_DECISIONS #56; RewardsService build when Consumer Layer service-layer phase begins) 24. offers — discount/promo offers (real v2 schema locked 2026-07-11, PROJECT_DECISIONS #56; OffersService build when Consumer Layer service-layer phase begins) 25. returns — customer RMA / returns module (schema locked 2026-07-11, PROJECT_DECISIONS #61; depends on rewards/offers for proportional loyalty/offer clawback; ReturnsService build when the returns service-layer phase begins) 26. consumer_app — consumer mobile experience (direction only)

Last modified: Jul 16, 2026, 9:20 AM PT
On this page
Esc