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_locremains 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
crmbuild — first product module;crmwas 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 theagent_duty_grantbuild — 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 theinventorybuild — biggest module, first realshared.plantconsumer;inventorywas 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 theaibuild — the agent-runtime schema layer;aiwas 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 thepricingbuild — the first module of the sell path;pricingwas 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 theposbuild — the only offline-first module;poswas 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 theordersbuild (module #15, PROJECT_DECISIONS #29) — completes the sell path;orderswas 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 thepurchasingbuild (module #16, PROJECT_DECISIONS #30) — the BUY path;purchasingwas 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 thetaxbuild (module #17, PROJECT_DECISIONS #31):taxwas NOT one of the original 23 rows — v1 never had a Tax module at all (confirmed viafind 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 thebillingbuild (module #18, PROJECT_DECISIONS #32) — the SETTLE half of the financial layer, built same day immediately after tax;billingwas 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 NEWar_adjustmenttable + 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): onlyinventorygained 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): onlypaymentsgained 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):admingained 1 table (setting_definition, +12 cols — the config-key catalog),identitygained +4 cols (agent_identity.status/suspended_at/suspended_by_actor_id/suspension_reason— the agent kill-switch),crmgained +2 cols (customer.credit_limit_cents/.credit_terms— the crm↔billing orphan fix),taxgained +2 cols (tax_calculation.calculation_type/.reversed_calculation_id— refund tax reversal),posgained +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_typewidened 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):platformgained 3 tables (accounting_period/legal_entity/outbox, +32 cols total incl. 2entity_idspot-cols),admingained 2 tables (integration_provider_catalog/custom_field_definition, +21 cols),posgained 1 table (tender_type_catalog, +17 cols incl. the anonymous-return CHECK relax + fiscal-period flag-trigger cols),taxgained 1 table (jurisdiction_level_catalog, +8 cols),sharedgained 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/inventorygained 0 tables each (entity_id/payment_terms_idspot-columns, theagent_memoryGDPR seam, and inventory'sstock.last_movement_id+ its first-everCREATE 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):identitygained 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 nowidentity+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/billingeach had exactly one FK upgraded from bare to composite (payment.invoice_id,vendor_invoice_match.vendor_invoice_line_id,ar_charge.tax_calculation_idrespectively) — 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 newreceivingschema (+2 tables/+62 cols —goods_receipt33 cols +goods_receipt_line29 cols, moved+renamed frompurchasing.purchase_receipt/.purchase_receipt_line's own 30+27 cols) split out ofpurchasing(−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.inventoryis unaffected at the row level (still 25 tables/351 cols —item_variant/lot/stock_movement/stock_movement_lineeach gainedUNIQUE(id, tenant_id)only, the prerequisite forreceiving'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 thetax/crm/orders/purchasingprecedents above —consumer/rewards/offerswere 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.salealso gainedUNIQUE(id, tenant_id)as this build's own prerequisite (constraint-only, no column/table impact onpos). 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 thetax/crm/orders/purchasing/Consumer-Layer precedents above —fileswas 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.filealso gainedUNIQUE(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 (newloyalty_point_ledger_reversal_tracker, closing the exact-negation-only limitation with a genuine proportional/partial-reversal capability, the cumulative reversed amount capped atomically) andoffers+1 table/+8 cols (newoffer_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.offeris live-confirmed at 43 cols, not the 42 this document's ownoffersrow 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 asshared'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-11returnsmodule build (PROJECT_DECISIONS #61, module #27) is the same shape of event astax's own #31 entry above, not thecrm/orders/purchasing/Consumer-Layer/Files-shaped delta-to-an-existing-row event —returnswas 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 v1pos.guarantee). The same migration bundled 5 companion reopens —pos(sale_refund/.sale_refund_linegainUNIQUE(id, tenant_id)),orders(order_header/.order_linegainUNIQUE(id, tenant_id)),inventory(stock_movement.source_moduleCHECK widened for'returns'),approvals(approval_request.source_moduleCHECK widened for'returns'),files(attachment.entity_typeCHECK 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 thebilling/Header-Line-Remediation precedents above — a delta toplatform'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-13aireopen (Phase 2 of the same 6-phase agents-v2/v3 build, PROJECT_DECISIONS #63) is the same shape of event as thebilling/Header-Line-Remediation/Platform-Phase-1 precedents above — a delta toai'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 partitioningagent_execution/agent_memoryBY RANGE(created_at) contributing 0 additional cols, since a partition inherits its parent's columns, plusagent_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 ownCREATE TABLEstatements) — a pre-existing off-by-one in that entry's own summary, not corrected here (out of this docs-only pass's scope); seedocs/database/schema_docs/ai.md's own disclosure note for the full accounting. Summing now gives 312 tables / 4,237 cols. The 2026-07-14semanticsbuild (Phase 3 of the same 6-phase agents-v2/v3 build authorization, PROJECT_DECISIONS #64) is the same shape of event astax's own entry above, not a delta to an existing row —semanticswas 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-15signalsbuild (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 astax's/returns'/semantics's own entries above, not a delta to an existing row —signalswas 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 byrecorded_at),forecast(11, PARTITIONED weekly),anomaly_score(10, PARTITIONED weekly),experiment_assignment(19),experiment_exposure_event(5),outcome_observation(19, PARTITIONED monthly bycreated_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-16agentsbuild (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 astax's/returns'/semantics's/signals's own entries above, not a delta to an existing row —agentswas 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 newtool_definition/tool_versionversion-row pattern (zero live v1 rows), not carried forward as a 22nd v1 table. See theAgentsrow below for the full concern-group breakdown. This same migration also growsplatform'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;platformis 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 (theagentsnew-schema addition), plus theplatformdelta'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 ofshared's own existing row viaALTER TABLE ... SET SCHEMA) andnursery(+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 tonursery_ref(net 0 across that pair, since the same physical tables simply changed schema), so the only genuinely new contribution to this running total isnursery's own +2 tables/+18 cols, alongsideinventory(−1 col,item.plant_iddropped) andmulti_loc(−1 col,site.climate_zone_codedropped); 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 1platformgain (+1 table/+6 cols, PROJECT_DECISIONS #68) — both post-date this paragraph's own last update (theagentsbuild, #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 thebilling/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_card25,gift_card_transaction14,gift_card_reversal_tracker7,store_credit_account17,store_credit_transaction14,store_credit_reversal_tracker7; the accompanyingstored_value_liabilityVIEW doesn't count toward the table total, matchinginventory.stock_reconciliation_shell's own precedent) andreturns+0 tables/+1 col (return_resolution.store_credit_transaction_id, 152→153); the same migration'sposandcrmcompanion 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 prerequisiteUNIQUE(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 tables — accounting_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 altered — module_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_bucket→storage_key/storage_bucket renamed, uploaded_by_user_id→uploaded_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_usage→tenant_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_registry→model_family→model_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→_version→prompt_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 PARTITIONED — feature_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 (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 (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 = ALLOW — sale_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_id → order_header, order_fulfillment_line.order_fulfillment_id → order_fulfillment, order_template_line.order_template_id → order_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_line — NEW 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→153 — return_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-SCOPED — funding_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:
- Foundation first —
platform,identity, andsharedmigrate 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. - Close seams while fresh — schedule each module soon after the modules whose seams it closes.
billingfollowspos/orders(charge-account seams);paymentsfollows the Stripe-seam modules;integrationsfollowsnotifications(provider seams);filesfollows the R2-seam modules. Deferred FKs accumulate context debt — closing them late means reconstructing decisions. - AI mid-service-layer —
aisits alongside the cross-cutting services (not last) because the onboarding import pipeline (import_job/import_file/import_record) is a hard dependency ofplatform.tenant_setup_task. The pipeline must be functional before the core build proceeds. - Reporting near-last —
reportingreads 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. - Consumer layer last, separate phase —
consumer,rewards,offers,consumer_appare 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/offersall 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_appremaining.)
1. Foundation & Platform:
platform— Vrida's control plane (tenant, subscriptions, entitlements)identity— authorization layer (users, roles, permissions, sessions)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 ofshared; depends only onsharedfor a locale FK)payments—PaymentsServiceover Stripe Connect; required for trial-to-paidintegrations— external-connector runtime (QuickBooks first); depends onadminconfig
2. Cross-cutting services (build alongside the modules that use them):
ai—AIServiceover Bedrock; AI import pipeline + Bedrock-call log (locked 2026-06-11)files— R2 metadata + access control (locked 2026-06-11)search— Postgres FTS +pg_trgmdual-GIN;SearchServiceabstraction (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)