Module Build Status
Build-pipeline status per module. Updated by the module runbook lock gate after each module pass. The Issues column is a count only — detail lives in docs/open-items/OPEN_ITEMS.md.
Legend: ✅ done · 🚧 started (partial, see footnote) · ⬜ not started · N/A not applicable · deferred (see OPEN_ITEMS)
'Schema locked' = schema has completed the AI Capability Plane pass (AI-needed fields/flags/tables added) AND passed the Section 4 audit. The original pre-plane schemas are NOT locked under this definition.
² PLATFORMSERVICE COMPLETE 2026-06-29 (both phases, 56/56 tests green). Phase 1: tenant management cluster — tenant core (provisionTenant, updateTenant, updateTenantProfile), lifecycle state machine (transitionTenantState — validates §8 legal transitions, rejects illegal, writes lifecycle_event, sets timestamps), data lifecycle/GDPR, onboarding (createSetupTasks 19 codes, completeSetupTask → identity.actor FK, getOnboardingStatus), legal (recordAgreementAcceptance → identity.identity_user FK, getAcceptanceStatus, listAgreementVersions, getContract), tenant contacts (addTenantContact → identity.identity_user FK, updateTenantContact). Phase 2: billing/entitlements/audit — createBillingAccount/update, createSubscription (one-active partial unique honored), dunning (recordPaymentFailure→past_due→writes-lifecycle-event, processDunning, recoverDunning→recovered), resolveEntitlements (real: active-window + override>contract>beta>addon+tier stacking per spec §9), consumeAiCredit (atomic balance + spend_limit + ledger), createPromoCode/validatePromoCode/applyPromoCode, invoices/payments/usage, writeOperatorAudit/writeInternalActivity (both → identity.identity_user FK). Past_due schema gap resolved: 'past_due'+'recovered' added to event_type CHECK (migration 20260629030000). HTTP controllers remain (step 11 / pages build).
¹ IDENTITY SCHEMA DESIGN COMPLETE — all batches A–D locked 2026-06-28. Batch A LOCKED 2026-06-28 — Pass 1 (actor model) + Pass 2 (groups, role inheritance, permission_group bundles) — 18 tables, 202 cols. 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) — 22 tables, 233 cols. 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) — 27 tables, 295 cols. 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) — 33 tables, 357 cols. 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) — 34 tables, 365 cols. IDENTITY SCHEMA DESIGN COMPLETE (A–D). MIGRATION COMPLETE 2026-06-29 — Drizzle schema authored (packages/db/src/schema/identity/), migration 0001_true_loners.sql applied (34 tables / 365 cols verified in Supabase local), trigger trg_tenant_user_actor_type_check deployed, all 8 platform→identity FK constraints wired (.references() in platform Drizzle files + FK constraints in DB). POST-BUILD RECONCILIATION 2026-06-29 — full doc-vs-DB diff; migration 20260629020000_identity_drift_reconcile.sql applied (+4 cols, +6 indexes/constraints); schema + docs realigned at 34 tables / 369 cols. IDENTITYSERVICE COMPLETE 2026-06-29 — all 5 build phases done: Phase 1 Permission Resolution Engine, Phase 2 Core RBAC CRUD, Phase 3 Sessions + Auth, Phase 4 Machine + Agent Identity, Phase 5 Governance + SoD + Support Access. 50/50 integration tests green. On-approval seam (DR-22), detect-and-flag (DR-19), time-window double-check (DR-10), ALLOW-ONLY SoD (S1) all live. Pages (Phase 6) remain.
Backend foundation milestone 2026-06-29: IdentityService (50/50 tests) + PlatformService (56/56 tests) complete. Admin HTTP layer started: AdminAuthGuard (Supabase JWT + operator gate) + AdminTenantsController (7 read endpoints, 18 HTTP integration tests). Both services fully integration-tested against local Supabase; HTTP layer pattern established.
³ ADMIN CONSOLE (admin.vrida.app) SHELL BUILT 2026-06-30, RECONCILED + VERIFIED END-TO-END 2026-06-30. Repurposed the untouched create-next-app scaffold at
apps/webinto a multi-app structure —apps/web/admin(this build), withapps/web/tenantandapps/web/storefrontplanned but not yet created; workspace globs andarchitecture/README.mdupdated accordingly. Full nav shell (5 groups / 18 pages, matching the Vrida Admin Console mockup minus a redundant Feature Flags page — see PROJECT_DECISIONS #14, removed as duplicating Plans & Entitlements) + design system (General Sans, brand palette, sharedTable/Badge/Chart/Drawer/Toastprimitives) + one isolatedmockData.tsmodule, all verified with a cleannext build+tsc+eslintpass. Tier taxonomy reconciled: Seed/Grow/Bloom are now the official display names for the real starter/pro/enterprise keys (PROJECT_DECISIONS #14) — one tier palette used everywhere, no separate marketing ladder. Tenants list + Tenant detail Overview tab verified against a real, runningapps/api+ local Supabase — minted a real operator JWT, confirmed real tenant rows render correctly (no mock banner, no error boundary) — the first true end-to-end validation of the real path; the other 6 detail tabs (Lifecycle/Contacts/Onboarding/Usage/Data & Retention/Internal Notes) show as disabled with a "future slice" toast, matching the source mockup's own scope. Update 2026-06-30: Announcements and Settings later moved from 🔴 PROPOSED to real skeleton tables + read endpoints (see PROJECT_DECISIONS #15) — the current split is 3 real page groups (Tenants, Announcements, Settings) and 15 fully 🟡 sample-flagged pages with no endpoint at all (dashboard,work-queue,reports,audit,onboarding,subscriptions,invoices,payments,entitlements,plans,contracts-promotions,legal-agreements,usage,operators,support-access— see OPEN_ITEMS row 67).apps/apiboot gap fixed:packages/dbnow has a real build step (tsc→ CJSdist/) instead of shipping raw ESM TypeScript asmain—apps/apinow boots and serves real traffic outside Jest for the first time (125/125 tests still green). No write endpoints and no operator login flow were built this pass (see OPEN_ITEMS). Update 2026-07-08 (PROJECT_DECISIONS #41) — real-data cutover, most of the console is now real. 18 newGETroutes (16 onPlatformService/7 pre-existing reused, 2 onIdentityServicevia its first-ever controller — see footnote ⁵, now stale). All 7 remaining tenant-detail tabs (Contracts, Lifecycle, Contacts, Usage, Data & Retention, Internal Notes, Support Access) and 8 more cross-tenant pages (Onboarding, Subscriptions, Invoices, Payments, Promotions, Agreement Versions, Audit log, Support Access) cut over frommockData.tsto real reads. Of the original 15 fully-sample page groups listed above, only 4 remain unwired by explicit in-session decision — Plans & pricing, Entitlements catalog, Operators (role field — a real schema gap, platform-level roles aren't representable today), Reports — plus the top-level Usage page's daily-chart portion (its per-tenant Usage tab is real);work-queuefolded into the Dashboard tile,contracts-promotions/legal-agreementsfolded into the Contracts tab + Promotions/Agreement Versions pages. Two real bugs found and fixed during this pass (a bigint-returned-as-string MRR sum bug; a DATE-column UTC-timezone display bug) — see PROJECT_DECISIONS #41 for both. Dashboard aggregates scoped to 5 flagship tenants (not the full, largely schema-test-fixture-pollutedplatform.tenanttable) — also #41. Seed data for the 10 previously-empty tables these routes read from added viapackages/db/seeds/admin-console-seed.ts(additive-only). See OPEN_ITEMS rows 55/66/67 (updated) for the reconciled remaining-gap count.
⁴ TENANT ADMIN CONSOLE (tenant.vrida.app) MOCKUP BUILT 2026-06-30.
apps/web/tenantcreated as its own Next.js app (mirrorsapps/web/admin's scaffold — package.json/tsconfig/eslint/next.config,globals.css/design tokens copied verbatim). Covers all 9 tenant admin UI page areas fromidentity.md§17 (Members, Groups, Roles & Permissions, Access Requests, SoD Compliance, Service Accounts, AI Agents, Active Sessions, Security Policy), grouped into 4 nav sections (People/Access/Identities/Security). Every page reads from one isolated, fully-typedlib/mockData.tsmodule (~25 identity tables, field-for-field matchingdocs/database/schema_docs/identity.md) — no invented fields, verified by an independent per-page review pass. Shared primitives copied/adapted fromapps/web/admin(Table,Badge,DataFlagBanner,palette,Drawer,Toast,ToggleSwitch) plus two NEW extracted-for-reuse components (DetailCard— Card/Grid/Pill, previously duplicated inline in admin'sTenantSkeletonPanels.tsx;TabSwitcher— generic tab strip). All 9 pages are 🟡 SAMPLE DATA — zero API calls, zero auth, zero tenant-context wiring, since IdentityService (86 methods, tested) has no HTTP endpoints at all yet (see OPEN_ITEMS). Verified with a cleannext build+eslintpass and a full browser click-through of all 9 pages + the/members/[id]detail route. One real bug caught and fixed during the build: a Server Component passing a live event handler into theToggleSwitchClient Component on/security-policy(Next.js RSC boundary violation) — fixed via a smallReadOnlyToggleRowclient wrapper. Enterprise-heaviness scoping flag for a small single-location nursery (human judgment call, not yet decided): Roles & Permissions' 6 sub-tabs, Service Accounts, AI Agents, Access Requests, and SoD Compliance all read as heavier than day-to-day small-nursery needs — Members, Groups, and Active Sessions read as appropriately scoped.
⁵ IDENTITY HAD ZERO HTTP CONTROLLERS (confirmed 2026-06-30; STALE as of 2026-07-08 — see update below). IdentityService is fully built (86 methods, 50/50 integration tests across its own 5 phases, 2026-06-29) but had no REST surface at all —
find apps/api/src -iname "*.controller.ts"returned onlyapp.controller.tsand 3 platform controllers (AdminTenantsController,AdminAnnouncementsController,AdminSettingsController); none referencedIdentityServiceexceptAdminAuthGuard, which calls one method (resolveActorFromAuthUser) purely to resolve the caller's identity, not to expose IdentityService's own capabilities. Marked ⬜ (not started) rather than a caveated ✅ — unlike Platform, which had a genuine partial HTTP layer (3 controllers, 9 real endpoints), Identity had none. See OPEN_ITEMS row 63 (now closed). Update 2026-07-08 (PROJECT_DECISIONS #41): no longer true.IdentityControllernow exists — Identity's first-ever HTTP-reachable route, 2 endpoints (GET /admin/tenants/:id/support-access,GET /admin/support-access), both wrappinglistSupportAccessGrants(tenantId?). Every other one ofIdentityService's 86 methods remains service-layer-only until its own controller is built — this closes the "zero controllers" gap specifically, not the broader HTTP-surface gap.
⁶ SHARED MODULE LOCKED 2026-07-05 (module #3) — 10 tables, 101 cols, fully seeded. Natural-key PKs on 9 of 10 tables (currency/country/language/locale/administrative_region/unit_of_measure/climate_zone use their ISO/short code as PK;
plantuses UUID — the one deliberate exception, since taxonomic names aren't permanently stable). A Section 4 audit FAIL onadministrative_region.region_type's CHECK list (originally 6 values, insufficient for real-world ISO 3166-2 type diversity) was fixed before lock by expanding to 21 values. Seed scope was honestly scaled down from originally-discussed targets in two places:administrative_region(186 rows — comprehensive for US/Canada/Mexico/UK/Australia, representative for 5 more countries, not the full ~3-4K global ISO 3166-2 set) andplant(114 rows, not "a few hundred") — both to avoid generating volume that couldn't be verified accurately from memory. 26 new schema/seed-integrity tests added (151/151 total suite still green, no regressions). NoSharedServicebuilt this pass — schema + seed data only; downstream modules queryshared.*directly via Drizzle until a consuming module needs an abstraction. See PROJECT_DECISIONS #17.
⁷ MULTI-LOCATION SCHEMA LOCKED 2026-07-05 (module #4) — 1 table (site), 29 cols (up from v1's 21, net +8 from the global-address redesign). Global from day 1: natural-key FKs into shared (country/administrative_region/climate_zone) replace v1's US-shaped address JSONB; currency/locale resolve via country_code join rather than being stored; measurement_system and climate_zone_code's system-appropriateness are service/UI-enforced (no shared lookup exists for either) — see PROJECT_DECISIONS #18. New DB-enforceable chk_site_region_country_match CHECK. 4 pre-existing deferred FKs (platform.tenant.primary_site_id, identity.tenant_user.default_site_id, identity.user_site_assignment.site_id, identity.user_permission_override.scope_id) are now unblocked but deliberately not wired this pass — see OPEN_ITEMS. Tests: 1 test suite, 6 tests, all passed (0 failed). Sections: A(1) table exists, B(1) RLS enabled, C(1) valid insert incl. DE/DE-BW/rhs:h4, D(1) chk_site_region_country_match rejects DE+US-CA, E(1) is_primary partial-unique rejects second primary for same tenant, F(1) climate_zone_code FK resolves via LEFT JOIN with zero unresolved rows. All inserted rows cleaned up by slug (verified 0 stray test-multi-loc-% rows remain in DB post-run). Full suite: 11 test suites passed, 157 tests passed, 0 failed (run via
npx jest --forceExitin apps/api). No regressions introduced by the new file. NoMultiLocServiceyet.
⁸ AUTONOMY-FIRST BACKFILL applied 2026-07-06 across platform (+6 cols), identity (+2 cols), shared (+7 cols), multi_loc (+8 cols) — see PROJECT_DECISIONS #19 for the canonical pattern and full rationale. 33 new tests added (platform+10, shared+19, multi_loc+5, identity+4), full suite 197/197 passing.
⁹ AGENT-DUTY-GRANT BUILT 2026-07-06 — the A5 agent-authority passport (
AI_CAPABILITY_PLANE.mdA5). +identity.agent_duty_grant(+1 table, +25 cols) — per (agent, permission) authority grant recording draft/execute/needs-approval authority and spend/quantity ceilings; complements (does not conflict with)role.requires_approval_for_agents's grant-time gate (PROJECT_DECISIONS #21).identity_access_event.event_typeCHECK expanded 39→42 (additive). 34→35 tables, 371→396 cols. Two bugs caught by independent adversarial verification during design, before any migration was written, and fixed pre-build: (1) the unique index originally includedscope_id/scope_code, which — since Postgres treats NULL as distinct in unique indexes — would have silently let two active tenant-scoped grants coexist for the same (agent, permission); fixed to(agent_identity_id, permission_id, scope_type) WHERE status='active', matchinguser_permission_override_unique's real shape. (2)scope_id's FK tomulti_loc.sitewas originally proposed as deferred, incorrectly citing a precedent (user_permission_override.scope_id) whose justification (multi_loc unbuilt) no longer applies now thatmulti_locis locked; fixed to a real, enforced FK. See PROJECT_DECISIONS #22,schema_docs/identity.mdDR-31/DR-32. 10 new schema/seed-integrity tests added (identity-agent-duty.spec.ts) — including the one-active-grant regression test (D1) proving the bug fix actually holds. Full suite: 15 test suites, 210 tests, all green (2 consecutive runs). NoIdentityServicemethods built this pass (schema-only) —grantAgentDuty()/revokeAgentDuty()/checkAgentDutyAuthority()remain open (see OPEN_ITEMS).
¹⁰ CRM SCHEMA LOCKED 2026-07-06 (module #5) — the first actually-built v2 PRODUCT module (modules #1-4 — platform/identity/shared/multi_loc — were all foundation/service-layer). 13 tables, 189 cols:
customer(core entity, individual/business discriminator, full autonomy treatment),contact,address(global shape, mirrorsmulti_loc.site),customer_group,customer_note(append-only, no review seam — additive/zero-risk),customer_task,customer_merge_candidate(the review gateway for dedup),customer_merge(append-only post-execution audit),customer_consent(append-only, never AI-initiated),customer_tax_certificate,customer_segment_definition(NEW — mirrorsidentity.role's mixed-scope built-in/tenant-custom pattern),customer_segment_membership(NEW — agent-computed VIP/at-risk/seasonal/wholesale-like classification, the primary home for D6/D7 autonomy this pass),customer_tag_assignment(NEW — human free-form labels, deliberately kept separate from segments). Pure consumer ofidentity.agent_duty_grant— introduces no new authority mechanism (illustrativecrm:*permission codes,module_code='crm'requires zero identity-module changes). Two issues caught and fixed before/during the migration, not after: (1)customer_tax_certificate's two jurisdiction-scoped partial unique indexes were designed up front to split onissuing_region_code IS NULLvsIS NOT NULL, avoiding the same NULL-in-multi-column-unique trapagent_duty_granthad originally hit (PROJECT_DECISIONS #22) — caught by applying that precedent proactively during design, not by a live failure this time. (2)customer_segment_membership's active/expired state was originally drafted as a partial index predicate onWHERE expires_at > now(); confirmed empirically pre-migration (ERROR: functions in index predicate must be marked IMMUTABLE—now()is STABLE, not IMMUTABLE) that Postgres rejects this, so it was fixed to a storedstatuscolumn before the migration was ever run, matching the on-read/sweep conventionidentity.role_assignment.ends_at/agent_duty_grant.ends_atalready use. See PROJECT_DECISIONS #23,schema_docs/crm.mdDR-33 through DR-47. Migration20260706060000_crm_module.sqlapplied to the live local Supabase DB (schemacrm, 13 tables verified viainformation_schema.columns). NoCrmServiceexists yet (schema-only this pass) — see OPEN_ITEMS.
¹¹ INVENTORY SCHEMA LOCKED 2026-07-06 (module #12) — the biggest module built so far, first real
shared.plantconsumer. 24 tables, 335 cols (up from v1's locked 21 tables/238 cols baseline — the same vertical-neutral, post-pivot design, just pre-shared.plantand pre-autonomy; delta is +3 tables/+97 cols, not a rebuild from scratch). Catalog:item,item_variant,category,item_category,tag,item_tag,barcode,item_image. Variant structure:option_type,variant_option. Locations:inventory_location. Stock:stock,stock_movement,stock_movement_line,stock_reservation,stock_adjustment_reason,stock_adjustment_request(NEW — propose/execute stock-adjustment gate,needs_approvalalways, never may-act-alone; ONE partial unique(tenant_id, site_id, variant_id) WHERE review_status='pending'prevents duplicate pending proposals even across located/unlocated variants — live-tested). Counts:stock_count,stock_count_line. Lots:lot,stock_lot(addscondition_gradeCHECKA/B/C/cull, closing v1's never-built plant condition grading). Kits:kit_component. Merge:item_merge_candidate/item_merge(NEW — mirrorscrm.customer_merge_candidate/customer_merge's propose/execute split, but closes a real gap crm's own audit found and left open:chk_item_merge_candidate_pair_order CHECK (item_a_id < item_b_id)+ aWHERE review_status='pending'partial unique together reject both mirrored-pair and duplicate-pending proposals — live-tested).item.plant_id— nullable FK →shared.plant.id(ON DELETE SET NULL), CHECK restricts it toitem_type='plant'rows. UOM columns (sell_uom_code/stock_uom_code/purchase_uom_code/weight_uom_code/capacity_uom_code) retyped totextFKs →shared.unit_of_measure.code, matching the actual locked shape (v1 had assumeduuid). All actor columns retargeted toidentity.actor(notidentity.identity_user).stock_movement.photo_ref— plain nullableuuid, no FK — deferred forward-ref to an unbuiltfilesschema (see OPEN_ITEMS). Pure consumer ofidentity.agent_duty_grant— introduces no new authority mechanism. Migration20260706070000_inventory_module.sqlapplied to the live local Supabase DB (schemainventory, 24 tables / 335 cols verified viainformation_schema, all 24 RLS-enabled). See PROJECT_DECISIONS #24,schema_docs/inventory.md. NoInventoryServiceexists yet (schema-only this pass) — see OPEN_ITEMS. 2026-07-11 companion reopen (see footnote ²³):stock_movement.source_moduleCHECK widened to accept'returns'— constraint-shape only, table/column counts unchanged, see PROJECT_DECISIONS #61.
¹² AI SCHEMA LOCKED 2026-07-06 (module #6, cross-cutting) — the agent-runtime schema layer. 7 tables, 118 cols (up from v1's locked 4 tables/71 cols baseline — reconciled with light retargeting only:
import_job,import_file,import_record,ai_request; delta is +3 tables/+47 cols, not a rebuild from scratch). New this pass:agent_execution(append-only action ledger —resolves_execution_idnullable self-FK →ai.agent_execution.idlinks a later "executed" row back to its earlier "proposed" row via direct JOIN, replacing a shared polymorphictarget_table/target_row_idkey that independent verification proved doesn't match the real dedicated-FK precedentinventory.stock_movement.adjustment_request_id/crm.customer_merge.candidate_idestablished;idempotency_key+UNIQUE (tenant_id, agent_identity_id, idempotency_key) WHERE idempotency_key IS NOT NULLdedup protection, matchinginventory.stock_movement.idempotency_key's precedent — live-tested duplicate rejection).agent_usage_period(atomic-upsert cost/token usage meter — reconciliation formula sumsai_request.total_token_countdirectly offai_request.agent_identity_id, not indirectly throughagent_execution.ai_request_id, closing a silent-undercount bug caught pre-migration; every increment MUST be a single atomicINSERT ... ON CONFLICT ... DO UPDATEstatement, never an app-level read-then-write — closes both a lost-update race and a duplicate-insert race).agent_memory(durable agent memory store —UNIQUE (tenant_id, category, key) WHERE status='active'partial unique, matchingidentity.agent_duty_grant's own precedent exactly; live-tested that disabling a memory row and inserting a fresh active row for the same key succeeds and preserves both rows as history, not a destructive overwrite).ai_request.agent_identity_id— new nullable FK →identity.agent_identity(v1 predatedagent_identityentirely). Pure consumer ofidentity.agent_duty_grant/agent_identityfor all agent authority — introduces no new authority mechanism.import_file.file_idstays a deferred forward-ref (plain uuid, no FK —filesschema still unbuilt). Migration20260706080000_ai_module.sqlapplied to the live local Supabase DB (schemaai, 7 tables / 118 cols verified viainformation_schema, all 7 RLS-enabled). See PROJECT_DECISIONS #25,schema_docs/ai.md. NoAIServiceextensions exist yet for the 3 new tables (schema-only this pass) — see OPEN_ITEMS.
¹³ PRICING SCHEMA LOCKED 2026-07-06 (module #13) — the first module of the sell path, outside-critique-resolved design. 4 tables, 75 cols (up from v1's locked 4 tables/56 cols baseline — table count unchanged, +19 cols).
price_level(11 cols),price_rule(33 cols),price_list_assignment(19 cols),price_change_log(12 cols, append-only). An outside-expert gap analysis (3 independent cold-read critiques against Odoo/Shopify/Magento/NetSuite/Stripe pricing-engine patterns) found real gaps, all 6 schema-fixable ones resolved directly in schema rather than deferred: supersede-don't-edit price history (chk_price_rule_no_self_supersede,chk_price_rule_superseded_by_consistency, cross-tenant supersession blocked by the newtrg_price_rule_validate_supersessiontrigger — live-tested), explicit site-scoping for customer-scoped rules (chk_price_rule_applies_all_sites_xor_site_id,chk_price_rule_customer_scope_site_explicit),tax_treatment,cost_plus_percentmarkup pricing (chk_price_rule_price_valuebounds enforced,-50clearance markdown succeeds,1500above ceiling rejected — both live-tested),campaign_label, and a pending-scope dedup unique index (price_rule_pending_scope_dedup_unique) that correctly rejects a genuine duplicate pending proposal while letting two different pending price_types for the same scope coexist — both live-tested. 22 CHECK constraints total; RLS enabled on all 4 tables; 4 triggers (set_updated_aton 3 tables + the 1 new trigger function). 3 additional non-schema gaps recorded as binding hard contracts (not vague deferrals):pos.sale_linemust snapshot the resolved price at sale time, a single versionedresolvePrice()spec with cross-language golden test vectors, and display rounding as aPricingService-only concern — seemodule_spec/pricing.md. Migrationpackages/db/migrations/20260706090000_pricing_module.sqlapplied to the live local Supabase DB (schemapricing, 4 tables / 75 cols verified viainformation_schema, all 4 RLS-enabled). Design-phase verification ran twice (original 4-table proposal, then again on the round-2 expanded design after the 6 gaps were folded in), each via an independent 3-agent Workflow, finding and fixing real bugs both times before this build. See PROJECT_DECISIONS #26,schema_docs/pricing.md. NoPricingServiceexists yet (schema-only this pass) — see OPEN_ITEMS.
¹⁴ POS SCHEMA LOCKED 2026-07-07 (module #14) — the offline-first sell-path module. 9 tables, 138 cols (down from v1's locked 19 tables/280 cols baseline — the 10-table cut is deliberate deferral, not a design regression; see OPEN_ITEMS for each deferred table's concrete trigger).
register(8 cols),register_session(18 cols),register_cash_entry(9 cols),sale(24 cols),sale_line(14 cols),sale_payment(26 cols),sale_refund(22 cols),sale_refund_line(7 cols),pos_sync_conflict(10 cols). Offline-first is the defining constraint:sale/sale_payment/sale_refundeach carryclient_uuid+origin+sync_status+idempotency_key+synced_at, withUNIQUE (tenant_id, client_uuid) WHERE deleted_at IS NULLgiving duplicate-sale replay a free, conflict-row-free dedup viaINSERT ... ON CONFLICT DO NOTHING; the distinct genuine-oversell case (two different offline devices, two differentclient_uuids, same last unit) is NOT caught by that dedup and instead requires a not-yet-builtInventoryService.completeSale()to detect the would-go-negative decrement and write apos.pos_sync_conflictrow (conflict_type='stock_oversell') — only the schema's ability to represent that conflict row is built and tested this pass (test B3). Newchk_<table>_origin_sync_status_consistencyCHECK on all 3 tables enforces online-created rows staysync_status='synced'. Pricing's Hard Contract 1 honored verbatim onsale_line(6 snapshot fields:resolved_amount_minor_units,charged_amount_minor_units,currency_code,tax_treatment,resolving_price_rule_id,resolved_quantity) — live-tested (test C1) that superseding the resolvingprice_ruleafterward does not change the already-recorded sale line. No POS-side reservation table: a POS sale callsInventoryService.completeSale()directly, netting out via the existinginventory.stock.available_qtygenerated column (inventory.stock_reservation.source_typeCHECK deliberately excludes'pos'— reservations stay Orders' own mechanism).sale.total_minor_unitsis immutable-gross forever (live-tested, test D1: asale_refundinsert never changes it);sale.statusCHECK is('open','completed','voided')with deliberately no'refunded'value — net refunded position is a query-time sum oversale_refund. Asale.search_vectorcolumn from the earlier design draft was dropped during build (no free-text field onsaleto index) — logged to OPEN_ITEMS as deferred, not silently dropped. 6 triggers total (5xset_updated_at+ 1xtrg_pos_sale_requires_open_session), 41 CHECK constraints total; RLS enabled on all 9 tables. New cross-module seam:orders.order_header.fulfilled_sale_id -> pos.sale(FK lives on Orders' side, not built yet; direction Orders → pos, "link-don't-convert; tax finalizes at POS"); Payments seam (sale_payment.stripe_payment_intent_id,sale_payment.charge_account_ref) recorded as forward-refs, no FK yet. Migrationpackages/db/migrations/20260707000000_pos_module.sqlapplied to the live local Supabase DB (schemapos, 9 tables / 138 cols verified viainformation_schema, all 9 RLS-enabled). See PROJECT_DECISIONS #27,schema_docs/pos.md. NoPOSServiceexists yet (schema-only this pass) — see OPEN_ITEMS. Tests: 16/16 pos-specific tests passing (apps/api/src/pos/__tests__/pos-schema.spec.ts), fullapps/apisuite 314/314 passing, no regressions. 2026-07-11 companion reopen (see footnote ²³):sale_refund/sale_refund_lineeach gainedUNIQUE(id, tenant_id), a prerequisite forreturns' own composite FKs — constraint-shape only, table/column counts unchanged, see PROJECT_DECISIONS #61.
¹⁶ PURCHASING SCHEMA LOCKED 2026-07-07 (module #16) — the BUY path, completing the supply loop opposite orders/pos. 16 tables, 398 cols (up from v1's locked 16 tables/341 cols baseline — +57 cols, ZERO tables consolidated or dropped; the stale MODULE_INDEX placeholder held 16/341). Second module built under the Design-Phase Integrity rules — the retained 4-block v1→v2 delta record lives in PROJECT_DECISIONS #30, grep-verified in-file at lock (Section 6 items 12+15). Groups: Vendor Master (
vendor37 — master,+linked_customer_id → crm.customerfor the grower-who-also-buys-retail overlap,−search_vector;vendor_contact16;vendor_address17 — global-reshaped to shared.administrative_region/country, no 'US'/'USD' defaults, +region/country CHECK mirroring crm.address;vendor_item29 — the cost-change-flag surface), Purchase Orders (purchase_order44 — the reorder-agent draft target;purchase_order_line30 — dual-UOM cost snapshot frozen at PO time, preserved verbatim; templates 15/10), Receiving (purchase_receipt30 —+idempotency_keyNULL-safe unique for the InventoryService.receive() dedup;purchase_receipt_line27), Invoice+3-Way-Match (vendor_invoice39 — status excludes 'paid', Billing owns payment via write-backs;vendor_invoice_line20;vendor_invoice_match18 — N:M line-to-line), Credit+Return (vendor_credit24,vendor_return23,vendor_return_line19 — all LIGHT, the resolved tier). Autonomy: 7 FULL / 8 LIGHT / 1 ZERO; purchasing's LIGHT carriesautomation_sourceuniformly (a disclosed deviation from crm's created_by-only light tier). 3 previously-DEFERRED v1 forward-ref FKs are REAL now (targets exist live):purchase_receipt_line.inventory_movement_id/vendor_return_line.inventory_movement_id → inventory.stock_movement(movement_type='received'/'returned' — the receiving/return seam, +avg-cost update on receipt);purchase_order.source_order_id → orders.order_header. The reciprocalorders.order_header.draft_po_id → purchasing.purchase_order— the forward-ref orders locked WITHOUT — is CLOSED this build via an ALTER (constraintorder_header_draft_po_id_fkey, not an orders reopen); documented on both sides in CROSS_MODULE_CONTRACTS.+vendor.linked_customer_id → crm.customer. An independent adversarial design-phase verification pass (before build) found and fixed 3 BLOCKER + 2 disclosure issues (a vendor off-by-one, conflicting totals, a crm LIGHT-tier mis-citation now disclosed as a deliberate deviation, a DR-E tier justification, an undisclosed column rename reverted). 6 v1 service-only rules promoted to real CHECKs (PO approved-requires-actor/at, invoice disputed/void/approved-requires-timestamp, invoice-line item-requires-variant, return rma-when-authorized + authorized-when-not-draft, match resolved-requires-actor/at). The reorder→PO draft loop is the flagship buy-side agent path — PO-send staysneeds_approvalALWAYS (the C8 financial-autonomy boundary); the cumulative agent spend-ceiling is deferred (cross-ref existing identity/ai/pricing rows, not reopened). Consumesidentity.agent_duty_grant— no new authority mechanism. Migrationpackages/db/migrations/20260707070000_purchasing_module.sqlapplied live (schemapurchasing, 16 tables / 398 cols verified viainformation_schema, all 16 RLS-enabled, 61 CHECK + 111 FK constraints,set_updated_aton all 16). Two independent non-self-grading verifiers confirmed ZERO FAILs: Section 4 (all A-P+T+U PASS) and the seams+FK pass (receiving seam, orders seam both directions with a live 23503 draft_po_id-rejection reproduction, linked_customer_id, idempotency dedup + NULL-distinctness, FK resolution across all targets, dual-UOM snapshot). See PROJECT_DECISIONS #30,schema_docs/purchasing.md,module_spec/purchasing.md. NoPurchasingServiceexists yet (schema-only). Tests: 35/35 purchasing-specific tests passing (apps/api/src/purchasing/__tests__/purchasing-schema.spec.ts); fullapps/apisuite 412/412 green run serially (--runInBand) — under default parallel execution 3 identity tests intermittently fail on a pre-existing shared-DB race on global identity tables (pass 25/25 in isolation), unrelated to purchasing.
¹⁵ ORDERS SCHEMA LOCKED 2026-07-07 (module #15) — completes the sell path (crm → pricing → inventory → orders → pos). 7 tables, 173 cols (up from v1's locked 7 tables/133 cols baseline — zero tables consolidated or dropped, pure addition of +40 cols; the stale MODULE_INDEX placeholder this row previously held, 7/133, was never a real v2 build). First module designed AND built under the Design-Phase Integrity rules (
SCHEMA_DESIGN_RUNBOOK.mdSection 0.5/2.3.6, written earlier the same day) — the full retained 4-block delta-accounting record lives in PROJECT_DECISIONS #29, not just a chat proposal.order_header(44 cols — root commercial-order record,accepted_bytext preserved AND joined by a newaccepted_by_actor_idper an explicit dual-column decision,confirmed_at-required CHECK deliberately widened beyond v1's literal single-state text),order_line(38 cols — Pricing's Hard Contract 1 6-field snapshot honored verbatim,stock_reservation_idreal FK,price_override/price_override_reasonrestored — a capabilitypos.sale_lineitself dropped),order_payment(27 cols — deposit/milestone/balance schedule,pos_sale_payment_idreal FK),order_fulfillment(26 cols — fulfillment batch header,ship_to_address_idreal FK),order_fulfillment_line(13 cols, byte-for-byte unchanged from v1, zero autonomy cols),order_template(16 cols),order_template_line(9 cols, byte-for-byte unchanged, zero autonomy cols). 3 real (not deferred) cross-module seams, all live-tested:order_line.stock_reservation_id → inventory.stock_reservation;order_header.fulfilled_sale_id → pos.sale(ON DELETE RESTRICT— deleting a referenced sale is rejected, proven);order_payment.pos_sale_payment_id → pos.sale_payment— Pricing's Hard Contract 1 is now SATISFIED by both pos and orders. An independent adversarial design-phase verification pass (before any table was built) found and fixed 5 real issues: an under-specified cancel-CHECK (only 2 of v1's actual 3 required fields), a fully unaddressed index-disclosure gap, 2 missing non-negativity CHECKs, an unjustified partial autonomy-tier assignment onorder_line/order_fulfillment(both upgraded to the full 7-column pack), and an undisclosed CHECK-scope widening (confirmed_at) — all fixed before the design was ever presented for approval. 6 v1 service-enforced-only rules promoted to real DB CHECKs, incl. the fixed cancel-CHECK requiring all 3 fields (cancelled_at/cancelled_by_actor_id/cancellation_reason) — live-tested as a regression (all 3 missing-combinations rejected, all-3-present succeeds). Idempotency: a single lightidempotency_key(not pos's offline-sync quintet — Part D D12 explicitly ruled that out, orders is online-first), two-partial-unique NULL-actor-safe split matchingpricing.price_rule's precedent — NULL-distinctness explicitly checked (order_numberis NOT NULL, no trap possible; both idempotency indexes exclude NULL-key rows, live-verified).pg_trgmfuzzy-search index onorder_numberconfirmed absent live — correctly deferred, not silently dropped, matching inventory's identical gap. Consumesidentity.agent_duty_grantfor all agent authority — no new authority mechanism introduced. Migrationpackages/db/migrations/20260707060000_orders_module.sqlapplied to the live local Supabase DB (schemaorders, 7 tables / 173 cols verified viainformation_schema, all 7 RLS-enabled, 46 CHECK + 43 FK constraints). See PROJECT_DECISIONS #29,schema_docs/orders.md,module_spec/orders.md. NoOrderServiceexists yet (schema-only this pass) — see OPEN_ITEMS. Tests: 41/41 orders-specific tests passing (apps/api/src/orders/__tests__/orders-schema.spec.ts), fullapps/apisuite 418/418 passing, no regressions. 2026-07-11 companion reopen (see footnote ²³):order_header/order_lineeach gainedUNIQUE(id, tenant_id), a prerequisite forreturns' own composite FKs — constraint-shape only, table/column counts unchanged, see PROJECT_DECISIONS #61.
¹⁷ TAX SCHEMA LOCKED 2026-07-07 (module #17) — the first module in this build with ZERO v1 precedent. 2 tables, 37 cols (up from v1's 0 tables — v1 fully outsourced tax to Stripe Tax with no local schema at all: confirmed via
find docs/old -iname "*tax*"returning nothing, cross-referenced against Payments' MODULE_INDEX row "tax → Stripe Tax" and Admin's "tax config (Stripe Tax)"). This is the FIRST module in the entire discrepancy-tracking history that is a genuine +1-schema/+2-table addition, not a delta against a stale pre-existing placeholder row. Exists becausepos's own 19-to-9 audit foundsale_line.tax_amount_cents/tax_ratecollapse to a flat aggregate with no per-jurisdiction breakdown — an unrecoverable loss for stacked-tax remittance, logged as a PRE-CUSTOMER OPEN_ITEMS blocker; this build closes that gap.tax_calculation(28 cols — header, one row per calculation event, the nexus/rate-anomaly-flag surface;automation_sourceDEFAULTS'system', the first justified deviation from the universal'human'default anywhere in this codebase — verified live, zero exceptions before this table) andtax_calculation_jurisdiction(9 cols — append-only per-jurisdiction breakdown restoring the lost decomposition; structurally append-only by shape, noupdated_at/updated_at/deleted_atcolumns exist at all, confirmed live viainformation_schema). Polymorphictax_calculation.source_ref→pos.sale_line.idororders.order_line.id, validated bysource_module/source_typeCHECK pairs — no reciprocal column on either locked table, matchingbilling.ar_charge.source_ref's established pattern.supersedes_calculation_id(self-FK) is scoped to same-source_refcorrections only, mirroringpricing.price_rule's supersede-don't-edit pattern. An independent adversarial design-phase verification pass (before build, run jointly with billing's proposal) found and fixed 2 real BLOCKERs: a missingpurchase_order.amount_paid_centswrite-back target in billing's design (not a tax-side fix, logged against billing), and an over-claimed reconciliation mechanism — the original draft assertedsupersedes_calculation_idbridges an orders-time estimate to its later pos-time final calculation, which is structurally impossible since no line-level link exists betweenorder_lineandsale_line(only header-levelorder_header.fulfilled_sale_id → pos.sale); resolved via explicit decision (option b): the two rows deliberately COEXIST as independent records, reconciled only at report time via the header-level join — a documented design decision, not a schema gap.total_tax_amount_cents = SUM(tax_calculation_jurisdiction.tax_amount_cents)is a build requirement, live-tested against a 3-jurisdiction stacked fixture (state 6.00% + county 0.25% + district 1.00%, summing correctly). Migrationpackages/db/migrations/20260707080000_tax_module.sqlapplied to the live local Supabase DB (schematax, 2 tables / 37 cols verified viainformation_schema, both RLS-enabled, 14 CHECK + 10 FK constraints, 2 triggers ontax_calculationonly —set_updated_at+trg_tax_calculation_validate_supersession— the jurisdiction child has neither, having noupdated_atcolumn at all). A post-lock independent verification pass (same day) found the same-source_refsupersession scoping (resolved decision 1) was documented but not DB-enforced — closed viatrg_tax_calculation_validate_supersession, mirroringpricing.trg_price_rule_validate_supersession, live-reproduced and covered by a new regression test. See PROJECT_DECISIONS #31 (incl. addendum),schema_docs/tax.md,module_spec/tax.md. NoTaxServiceexists yet (schema-only). Tests: 24/24 tax-specific tests passing (apps/api/src/tax/__tests__/tax-schema.spec.ts).
¹⁸ BILLING SCHEMA LOCKED 2026-07-07 (module #18) — the SETTLE half of the financial layer, built same day immediately after tax. 9 tables, 162 cols (up from v1's locked 8 tables/113 cols baseline — all 8 v1 tables preserved 1:1, +1 NEW table
ar_adjustment, +49 cols total).ar_account(19, FULL),ar_charge(26, FULL — the tax seam),ar_payment(23, FULL),ar_payment_application(9, LIGHT append-only),ar_statement(21, LIGHT),ar_adjustment(22, FULL, NEW — the write-off/dispute-resolution table v1 deferred, resolved decision 1),vendor_payable(16, LIGHT),ap_payment(17, LIGHT),ap_payment_application(9, LIGHT append-only). Asymmetric autonomy tiering disclosed deliberately: A/R gets FULL (collections/dunning-flag, payment-matching-ambiguity, write-off/dispute-resolution are real judgment surfaces); A/P stays LIGHT (the real judgment already happened upstream atpurchasing.vendor_invoice's own FULL gate).ar_charge.tax_calculation_id → tax.tax_calculation.idis the NEW tax seam (nullable, real FK).ar_charge.source_ref/.source_payment_refare polymorphic →pos.sale/orders.order_headerandpos.sale_payment/orders.order_payment— plain uuid, NO single-table FK (structurally impossible for a two-target polymorphic column; the design proposal's "now REAL FKs" language meant the target schemas now exist to validate against, not that a single-column FK is possible), validated by thesource_module/source_typepair CHECK instead — identical pattern totax.tax_calculation.source_ref, established the same day. A live NULL-distinctness bug was caught and fixed during this build's own test-writing pass (not deferred to post-lock): a naive single 5-column idempotency index onar_chargewould let two charges for the samesource_refboth land with a NULLsource_payment_refwithout colliding, since Postgres treats every NULL as distinct — fixed via a two-partial-unique split (ar_charge_idempotency_full_unique+ar_charge_idempotency_no_payment_ref_unique), mirroringorders.order_header's own idempotency_key precedent exactly. An adversarial design-phase pass (run jointly with tax's proposal) found and fixed 1 real blocker specific to billing: a missingpurchase_order.amount_paid_centswrite-back target, now documented as a SUM-rollup requirement (one PO can have multiple vendor_payables), not a naive 1:1 copy. Migrationpackages/db/migrations/20260707090000_billing_module.sqlapplied live (schemabilling, 9 tables / 162 cols verified viainformation_schema, all 9 RLS-enabled, 47 CHECK + 47 FK constraints,set_updated_aton 7 of 9 tables — the 2 append-only application junction tables correctly have none). See PROJECT_DECISIONS #32,schema_docs/billing.md,module_spec/billing.md. NoBillingServiceexists yet (schema-only). Tests: 47/47 billing-specific tests passing (apps/api/src/billing/__tests__/billing-schema.spec.ts); fullapps/apisuite 483/483 green serially (--runInBand). Reopened 2026-07-18 for the Phase 3 Gift Card + Store Credit build (PROJECT_DECISIONS #71) — +6 tables/+84 cols (10→16 tables, 176→260 cols):gift_card/gift_card_transaction/gift_card_reversal_tracker,store_credit_account/store_credit_transaction/store_credit_reversal_tracker, plus thestored_value_liabilityVIEW — the first phase in this build sequence to add real business capability, unblockingpos.sale_payment's fail-closed gift_card/store_credit tender gate (real composite FKs replace the old forward-refs; the gate narrows topayment_method != 'reward'). All 8 named guards (negative-balance impossibility, concurrency race, double-redemption, append-only both layers, cross-tenant, code-hash security, doubly-capped clawback, the CHECK relaxation) live-reproduced.billing-stored-value.spec.ts(42 tests, new). NoBillingServicestored-value methods yet.
¹⁹ PAYMENTS SCHEMA LOCKED 2026-07-07 (module #19) — the money-MOVEMENT layer, executes what billing (#18) records. 9 tables, 158 cols (up from v1's locked 8 tables/112 cols baseline — all 8 v1 tables preserved, +1 NEW table
terminal_reader, +46 cols total).stripe_connect_account(16, LIGHT),payment_intent(32, FULL — the central execution ledger),payment_refund(21, FULL — the clearest C8 boundary in this module),payout(20, FULL),dispute(21, FULL),payment_method(15, LIGHT),stripe_event_log(8, ZERO — insert-once/status-updated, global non-tenant-scoped unique onstripe_event_id, nullabletenant_idpreserved verbatim from v1),stripe_event_dead_letter(10, ZERO),terminal_reader(15, LIGHT, NEW — closes OPEN_ITEMS row 185'spos.registerhardware-pairing gap).payment_intent.source_refpolymorphic →pos.sale_payment/orders.order_payment/billing.ar_payment(plain uuid, no FK — 3-way widened from v1's pos/billing-only pair,'sale'renamed'sale_payment').chk_payment_intent_source_pairis a genuine fix caught by independent adversarial verification DURING THE DESIGN-PHASE PASS (not v1-preserved — v1 never had this CHECK either): rejects invalidsource_module/source_typecombos, mirroringtax.tax_calculation/billing.ar_charge's own source-pair CHECK. The same adversarial pass also caughtterminal_reader.stripe_reader_idmissing its expectedUNIQUE(fixed) and an arithmetic error in theautomation_source='system'table count (corrected to 3 more/4 total). The idempotency/double-charge guard was checked specifically against the same-daybilling.ar_chargeNULL-distinctness bug class and confirmed clean pre-build. 5 hardcoded'USD'currency defaults removed (global from day 1). The money boundary (billing records, payments executes, platform is Vrida's own unrelated SaaS revenue) required a corrective column-comment fix onplatform.subscription.status, folded into this build's migration. First module built under the new evidenced independent-verification gate (SCHEMA_DESIGN_RUNBOOK §2.3.7/2.7/6.1a, added the same day after this module's own design proposal was initially self-graded "Zero FAILs" and found wrong by adversarial review). A SEPARATE agent independently re-audited the built schema post-build and found zero FAILs — full pasted findings in PROJECT_DECISIONS #33. Migrationpackages/db/migrations/20260707100000_payments_module.sqlapplied live (schemapayments, 9 tables / 158 cols verified viainformation_schema, all 9 RLS-enabled with real tenant-isolation policies, 35 CHECK + 32 FK constraints,set_updated_aton 7 of 9 tables — the 2 webhook tables correctly have none). See PROJECT_DECISIONS #33,schema_docs/payments.md,module_spec/payments.md. NoPaymentsServiceexists yet (schema-only). Tests: 46/46 payments-specific tests passing (apps/api/src/payments/__tests__/payments-schema.spec.ts); fullapps/apisuite 529/529 green serially (--runInBand).
²⁰ ADMIN SCHEMA LOCKED 2026-07-07 (module #20) — the FIRST v2 pass of this module (v1 was 11 tables/145 cols, locked 2026-06-10, never rebuilt for v2 until now). 10 tables, 128 cols (down from v1's 145 —
tenant_business_profile, 17 cols, DROPPED entirely: 16 of its 17 columns MOVED toplatform.tenant_profilein a 4th Platform reopen, the 1 remaining column,attributes, dropped outright with no successor — zero known consumers, no defined shape; full 17-column fate mapping lives in PROJECT_DECISIONS #34, not re-derived here). The 10 surviving tables are a FAITHFUL PORT of v1 — same columns, types, CHECK constraints, indexes — with exactly ONE schema-level change, the module-wide actor-attribution retarget already applied to every module built since 2026-06-28: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 minus 17 exactly. 5 columns across 3 tables retargeted fromidentity.identity_usertoidentity.actor:tenant_setting.updated_by_actor_id,api_key.created_by_actor_id,api_key.revoked_by_actor_id,approval_request.requested_by_actor_id,approval_request.resolved_by_actor_id. Admin does NOT own tenant identity —platform.tenant_profileis the single source of truth (PROJECT_DECISIONS #34); every Admin table carriestenant_id → platform.tenant.idand duplicates no identity facts.tenant_brandingnow owns the logo (logo_ref, a Files-module forward-ref) —platform.tenant_profile.logo_urlwas deprecated in its favor (PROJECT_DECISIONS #35). The approval engine (approval_workflow/approval_routing_rule/approval_request) is tenant-scoped, tenant-RLS throughout, and stays Admin-internal per DECIDED Option B (PROJECT_DECISIONS #34 Section 5) — it does not converge withplatform.contract's separate Vrida-operator review gate (different plane, untouched), but is confirmed purely forward-compatible as the shared mechanism any future tenant-side approval need can route through, with zero changes required to any already-locked module. [SUPERSEDED 2026-07-09] This Option-B decision was reversed 2 days later: all 3 approval-engine tables moved OUT of Admin into a new dedicatedapprovalsschema, reducing Admin to 10 tables/122 cols and 60/60 admin-specific tests (the approval-engine coverage moved toapprovals's own 22-test suite) — see footnote ²¹ and PROJECT_DECISIONS #44. A config-surface decision tree (7 branches, PROJECT_DECISIONS #34 Section 6) governs where new tenant-config-shaped settings go:tenant_settingis the branch-7 default/fallback,integration_config.settingsis branch 5,hardware_device.configis branch 6. Dependency-blocked register (Block 4, already established, not new discoveries):tenant_branding.logo_ref/compliance_document.document_refremain unchanged forward-refs to the not-yet-built Files module;integration_config.credentials_ref/webhook_config.secret_refare vault references depending on the same not-yet-built vault-encryption serviceplatform.tenant_profile.ein_refalready depends on (confirmed via grep — zero Vault/Encryption service classes anywhere inapps/api/src);integration_config/webhook_configare the real, already-documented FK targets for the not-yet-built Integrations module. Part D capability discovery ran during the design phase: Admin is honestly a LOW-AUTONOMY, mostly-human-configuration module — onlyapproval_requestreaches FULL (mechanical routing + anomaly/SLA flagging; the approve/reject decision itself stays definitionally human), narrow LIGHT flagging-only surfaces exist onhardware_device/integration_config/webhook_config/api_key(detection only, never autonomous write access to credentials/endpoints/issuance), andtenant_branding/approval_workflow/approval_routing_ruleare explicitly human-only (branding is a subjective creative decision; workflow/routing-rule definitions are governance decisions over approval authority itself, not delegable to the system they exist to check) — noautomation_source/review_status/decision_provenancecolumns were added to any of the 10 tables, a deliberate disclosed decision. Migrationpackages/db/migrations/20260707120000_admin_module.sqlapplied live (schemaadmin, 10 tables / 128 cols verified viainformation_schema, RLS enabled + tenant-isolation policy on all 10, all 5 retargeted actor FKs confirmed resolving toidentity.actor). See PROJECT_DECISIONS #34/#36,schema_docs/admin.md. NoAdminServiceexists yet (schema-only). Tests: 46/46 admin-specific tests passing (apps/api/src/admin/__tests__/admin-schema.spec.ts) covering table/RLS/column-count existence, RLS policy correctness,platform.tenant_idFK resolution, the 5 retargeted actor columns, the full approval-engine chain,api_keyhash-only storage, NULL-distinctness across every multi-column unique in the module, and CHECK-constraint spot-checks; fullapps/apisuite 585/585 passing (up from 539 pre-Admin-build), zero regressions.
²¹ APPROVALS SCHEMA LOCKED 2026-07-09 (module #21) — NEW module, consolidating the tenant-side approval engine OUT of Admin, reversing PROJECT_DECISIONS #34 Section 5's Option B (see footnote ²⁰ above). Preceded by a read-only, Ultracode-scale sweep of every approval/review/sign-off/authorization-gate/workflow mechanism across all 15 then-locked modules (81 mechanisms found: 17 real business-process approvals, 2 operator-only, 41 autonomy-review seams deliberately left distributed/untouched, 21 other), then a design proposal for a dedicated module, then TWO independent adversarial verification passes (a 3-lens pass, superseded by a more precise 7-lens pass covering domain-agnosticism, the C8 agent-money boundary, self-approval defeatability — explicitly including the null-approver path prior code reviews had found as a hole — token security, polymorphic integrity/outbox degradation, autonomy-seam exclusion, and admin-reopen scoping) before any schema was touched. 8 tables, 97 cols:
approval_workflow/approval_routing_rule/approval_requestMOVED fromadminviaALTER TABLE ... SET SCHEMA(RLS/triggers/grants carried over automatically, confirmed live) —approval_workflowextended 10→12 cols (+step_mode, +blocks_agent_approver— the C8 boundary anchor),approval_requestextended 18→20 cols (requested_by_actor_idrenamed toinitiator_actor_idand made NOT NULL, closing a null-approver bypass;step_historydropped, superseded by the newapproval_step); both tables'workflow_typeCHECK-enum removed (was a domain-knowledge leak baked into the engine). Plus 5 net-new tables:approval_policy,approval_step,approval_delivery,approval_token(hash-only one-click approve/reject, atomic single-use redemption),approval_event(append-only audit log, reusesplatform.reject_append_only_mutation()). 6 critical guards, every one LIVE-REPRODUCED with a real before/after proof, not asserted: approved-by-nobody, self-approval on every path (header, non-final parallel step, and the null-approver path), parallel-step quorum-spoofing, the C8 agent-money boundary, token replay/expiry/sibling-supersession, and append-only audit enforcement (UPDATE/DELETE rejected even as superuser). Guard 4's own reproduction found a 3rd, previously-unknown bug live: a column-levelREVOKEalone does not subtract from an existing broader table-levelGRANT(Postgres ACLs are additive across granularities) — fixed by revoking the table-level grant and re-granting column-by-column. A full NULL-in-CHECK sweep across all 13 CHECK constraints found zero bypass surface. A post-build independent adversarial lock-gate verification pass then found one further real bug the build's own reproductions had missed: a cross-tenant FK bypass on all 4 new child tables — Postgres FK checks bypass RLS while the new guard triggers areSECURITY INVOKER, so a crafted cross-tenant reference silently defeated both the self-approval and C8-boundary guards (live-reproduced 3 ways by the verifier) — closed via composite FKs(child_col, tenant_id) → parent(id, tenant_id), a deliberately stronger fix than this codebase's existing trigger-based cross-tenant precedent (tax.trg_tax_calculation_validate_supersession), re-verified live as closed. Zero domain-module dependencies — pure infrastructure, consumed read-only byidentity(actor,agent_duty_grant),platform(tenant,outbox),multi_loc(site); 14 domain-module convergence points logged to OPEN_ITEMS, none decided to migrate onto this engine yet. Migrationpackages/db/migrations/20260709070000_approvals_module.sql(+ a same-day follow-up20260709080000_approvals_fix_cross_tenant_fk.sqlfor the composite-FK fix) applied live (schemaapprovals, 8 tables / 97 cols verified viainformation_schema, all 8 RLS-enabled). See PROJECT_DECISIONS #44,schema_docs/approvals.md,module_spec/approvals.md. NoApprovalsServiceexists yet (schema-only). Tests: 22/22 approvals-specific tests passing (apps/api/src/approvals/__tests__/approvals-schema.spec.ts); Admin's own suite was updated in the same pass to drop the 8 tests specific to the relocated approval-engine tables (68 → 60 — the pre-move 68 already reflected Remediation Phase 3/4 test growth beyond the original 46/46 at Admin's 2026-07-07 lock, not itemized further here) — coverage moved toapprovals's own suite rather than shrinking; fullapps/apisuite green, zero regressions. 2026-07-11 companion reopen (see footnote ²³):approval_request.source_moduleCHECK widened to accept'returns'— constraint-shape only, table/column counts unchanged, see PROJECT_DECISIONS #61.
²² REWARDS + OFFERS SCHEMA LOCKED 2026-07-11 (PROJECT_DECISIONS #56/#57), REOPENED SAME DAY (PROJECT_DECISIONS #60) for a proportional-reversal fix. This row was never updated at the original lock (a missed-doc-file gap, same class as footnote ²⁰'s own Admin/Approvals precedent) — closed here rather than left stale.
rewards: 7 tables/109 cols (up from the original 6/102).offers: 7 tables/123 cols (up from the original 6/115). The same-day reopen fixedrewards.sync_loyalty_account_balance()'s exact-negation-only reversal limitation and a real, independent bug inoffers.check_and_sync_offer_budget()(reversal magnitude was completely unvalidated — live-reproduced: a $10 redemption "reversed" by a fabricated $1,000 row succeeded pre-fix). Both now support proportional/partial reversal, cumulative-capped per original entry via a new tracker table each (loyalty_point_ledger_reversal_tracker,offer_redemption_reversal_tracker), live-reproduced sequentially and under genuine concurrent load, then independently re-verified by 3 separate adversarial lenses (all PASS, zero functional defects). Neither module has a service layer yet (RewardsService/OffersService— schema-only). Tests:rewards-schema.spec.ts28/28,offers-schema.spec.ts39/39; fullapps/apisuite 1052/1052.
²³ RETURNS SCHEMA LOCKED 2026-07-11 (module #27) — the customer RMA module, PASS 2 of a 2-pass same-day session (Pass 1: the rewards/offers proportional-reversal fix, PROJECT_DECISIONS #60 — see footnote ²² above). 9 tables, 152 cols:
return_authorization(29, FULL),return_authorization_line(28 — freezes discount/tax allocation at RA-creation time, the module's central design decision),return_source_line_tracker(10, NEW — an aggregate-cap cache mirroringoffers.offer_redemption_reversal_tracker's own shape, sincepos.sale_line/orders.order_lineare append-only and off-limits for a counter column; caps cumulative over-return across multiple separate RMAs against the same source line),return_resolution(21),return_resolution_line(8, append-only — write-once financial fact mirroringbilling.ar_charge_line/purchasing.vendor_credit_line),return_receipt(14),return_receipt_line(16 — tolerance policy is BLOCK, not flag, a deliberate deviation fromreceiving.goods_receipt_line's own flag default, since a customer returning goods twice is more dangerous to leave un-gated),return_reason(10),warranty(16, revived v1pos.guarantee, retargeted so a plant-guarantee claim resolves as a REPLACEMENT not a refund). Proportional loyalty/offer clawback is a REAL, WORKING capability from day one, not deferred — directly crediting Pass 1's (PROJECT_DECISIONS #60) same-day fix torewards/offers' own reversal triggers, whichreturn_resolution.loyalty_reversal_ledger_id/.offer_reversal_redemption_idcall into rather than re-implementing any loyalty/offer math itself; live-reproduced (5 shrubs, 100 points + $10 offer, return 2 of 5 → 40 points + $4.00 clawed back, additive on a 2nd partial return, an over-clawback attempt correctly rejected). 5 small companion reopens bundled into the same migration, all constraint/CHECK-shape only — zero table/column count impact on any of the 5:pos.sale_refund/sale_refund_lineandorders.order_header/order_lineeach gainedUNIQUE(id, tenant_id);inventory.stock_movement.source_module,approvals.approval_request.source_module, andfiles.attachment.entity_typeCHECKs were each widened to accept areturns-related value ('returns','returns','return_authorization'respectively). 3 new trigger functions, allBEFORE INSERT(check_and_reserve_source_line,validate_and_apply_resolution_line,post_and_cap_return_receipt_line), each reusing this codebase's own proven atomic-cap/row-lock patterns — a real NULL-in-ON CONFLICT-predicate bug (missing theWHEREclause needed to match a partial unique index againstreturn_source_line_tracker's 2 mutually-exclusive nullable columns) was caught and fixed during this build's own live-reproduction. Migrationpackages/db/migrations/20260711040000_returns_module.sqlapplied live (schemareturns, 9 tables / 152 cols verified viainformation_schema.columns, zero drift against the Drizzle schema files). See PROJECT_DECISIONS #61. NoReturnsServiceexists yet (schema-only this pass, matching every other schema-only module's own established pattern). Tests: 31/31 returns-specific tests passing (apps/api/src/returns/__tests__/returns-schema.spec.ts); fullapps/apisuite 1083/1083 passing, confirmed serially (--runInBand) — one pre-existing, already-disclosed, unrelated cross-file concurrency flake (admin-tenants.spec.ts) intermittently reproduces under parallel Jest workers, not caused by this build.
²⁴ PLATFORM REOPENED A 6TH TIME 2026-07-12 (Phase 1 of 6 — agents-v2/v3 build). Build authorization for the full
agents/semantics/signalsbuild (design of record:vrida-agents-v2-design-amendment-2026-07-11.mdas amended byvrida-agents-v3-correction-pass-2026-07-12.md) is now underway, sequenced in 6 phases with an independent lock-gate verification required between each before the next may begin. This is Phase 1: +7 tables, 0 existing tables altered —module_catalog(A1, the global module registry replacing the closed CHECK-enum module-tag pattern, forward-onlylifecycle_statusvia a new trigger),tier_module_entitlement(which modules a tier includes by default, replacing the deadtier_definition.entitled_modulesJSONB),tenant_module_activation(the REAL per-tenant module switch, replacingis_toggleable— a mechanism found to have zero callers anywhere in the codebase),module_dependency(module prerequisite graph with real recursive-CTE cycle detection plus a reverse-dependency-on-deactivation guard),ai_capacity_policy/tenant_ai_capacity_usage(A8/E4 — the 6-level AI-capacity precedence chain's atomic enforcement half,platform.try_increment_ai_capacity_spend(), embedding the resolved ceiling as a correlated subquery inside oneUPDATE...RETURNING),tenant_regional_policy(E7 — one resolvable regional-placement policy per tenant, for later phases' routing/storage/retrieval/archival decisions to evaluate against). 4 guards live-reproduced (lifecycle transition, recursive cycle rejection, reverse-dependency-on-deactivation, atomic capacity race).platformis now 34 tables / 514 cols (up from 27/450). An independent lock-gate verification (separate agent) found and fixed 4 issues before Phase 2 was authorized to proceed: (1) BLOCKER — a pre-existing schema-wide default-privilege GRANT letauthenticatedfreely mutate 4 global reference tables including the platform-wide AI cost ceiling, closed via explicit REVOKE; (2) MAJOR — the spend-increment function's ceiling check wrongly gated refunds/negative deltas, now bypassed unconditionally fordelta <= 0; (3) MAJOR —ai_capacity_policy.prioritywas a vestigial tie-break, now honored viaORDER BY priority DESC, max_cost_cents ASC; (4) MAJOR — the seededmulti_locrow was misclassifiedlayer='foundation', corrected to'business'perSCHEMA_CONVENTIONS.md§1. A formal Section 4 self-audit (A-P+T+U) found 3 more GAPs (2 missing RLS-status doc comments, 1 missing supporting index, 1 trigger scoping tightening), all fixed same-pass. Migrationpackages/db/migrations/20260712000000_platform_module_registry_capacity.sql. See PROJECT_DECISIONS #62. Schema-only this phase — no service-layer changes (every phase of this build stays schema-only until Phase 5 lands theagentsmodule itself). Tests:apps/api/src/platform/__tests__/platform-module-registry.spec.ts, 23/23 passing; fullapps/apisuite 1111/1111 (serially). Phase 1 lock-gate verdict: CLEAN. Phase 2 (aireopen) is authorized to proceed.
²⁵
aiREOPENED FOR THE 1ST TIME 2026-07-13 (Phase 2 of 6 — agents-v2/v3 build). Continuing the same 6-phase agents-v2/v3 build authorization Phase 1 opened (footnote ²⁴). This is Phase 2: +13 new tables (PROJECT_DECISIONS #63's own count; an independent cross-check against the Drizzle schema files/migrationCREATE TABLEstatements — seedocs/database/schema_docs/ai.md's own disclosure note — finds 14 distinct new table definitions, a pre-existing off-by-one in that entry's own summary sentence, disclosed here rather than silently perpetuated or corrected out of scope) implementing the C1 model registry/deployment/prompt/routing layer —provider_registry/model_family/model_version(the 3-level provider/model registry),model_deployment+ its_limit/_region/_policy/_overridesatellites +model_deployment_status_observation(3 genuinely separate state classes: durable config, administrative override, transient runtime observation — live-confirmed distinct),prompt_definition/_version/prompt_model_compatibility(the same A2b definition/version split applied to prompts),routing_policy(schema-only — no resolution function built yet) — plus C4'sagent_memory_source(a join table, not a single polymorphic column, since one memory may derive from multiple sources).ai.agent_executionandai.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. 2 genuine gaps neither the v2 nor v3 design addressed, resolved consistently with Phase 1's own established pattern for structurally-impossible native constraints:agent_execution's at-most-one-resolver + idempotency-dedup partial-unique indexes, andagent_memory's at-most-one-active-per-key partial-unique index, all 3 cannot survive partitioning as native indexes (Postgres requires a partitioned table's unique index to include the partition key, which would silently narrow each global invariant to per-partition-month scope) — all 3 are now enforced by newBEFORE INSERT(orBEFORE INSERT OR UPDATE OF status) advisory-lock triggers instead (ai.check_agent_execution_single_resolver(),ai.check_agent_execution_idempotency(),ai.check_agent_memory_single_active()), plus a composite self-FK (resolves_execution_id/resolves_execution_created_at) replacingagent_execution's old plain self-FK. A mandatory pre-migration audit (required since these are v1 tables reopened for the first time) found 948 liveagent_executionrows (0 inagent_memory) and one genuine pre-existing data-integrity violation — a dev-seed row'spermission_idreferenced a deletedidentity.permissionrow despite a live FK — nulled out and disclosed, not silently dropped; re-verified zero orphans before re-running the migration.model_deployment_override.emergency_traffic_weightis trigger-guarded (ai.check_model_deployment_override_residency()) to only ever raise traffic toward a tenant-restricted deployment whose region already satisfies that tenant'splatform.tenant_regional_policy-resolved home region.routing_policy.workload_class_idis a disclosed forward-ref (bare uuid, no FK) toagents.workload_class(Phase 5, not yet built) — logged to OPEN_ITEMS andCROSS_MODULE_CONTRACTS.md. 6 guards + the residency guard all live-reproduced via real psql sessions.aiis now 49 tables / 227 cols (up from 7/121) — 227 counts each distinct table definition once; the 49 physical-table count (live-verified viainformation_schema.tables) additionally includes 28 partition-child tables (13 monthly + 1DEFAULTcatch-all, × 2 partitioned tables) that inherit their parent's columns rather than adding new ones. A Section 4 self-audit found 3 GAPs, all fixed same-pass (3 JSONB columns missing documented example shapes; a Drizzle comment falsely claiming a resolution function existed forrouting_policy; theworkload_class_idforward-ref documented in code but not yet logged to OPEN_ITEMS). A disclosed mistake during this phase's own live-reproduction pass (an overly broadDELETE ... WHERE action_code LIKE 'test.%'cleanup query also removed 270 pre-existing dev-seed rows sharing that generic prefix — confirmed harmless, the substantive 678-row dataset fully intact) is logged in PROJECT_DECISIONS #63, not repeated in the regression suite written afterward. Migrationpackages/db/migrations/20260713000000_ai_registry_partition_memory_governance.sql. See PROJECT_DECISIONS #63. Schema-only this phase — no service-layer changes (every phase of this build stays schema-only until Phase 5 lands theagentsmodule itself). Tests:apps/api/src/platform/__tests__/ai-registry-partition-memory.spec.ts, new file, 18/18 passing; the pre-existingapps/api/src/ai/__tests__/ai-schema.spec.ts(23 tests) required 5 updates for the new partition-aware shape (table count, 2 INSERT-statement updates, 3 assertions moved from index-23505to trigger-messageshape) — all disclosed as expected updates, not defects. Fullapps/apisuite: 1129/1129, typecheck clean onpackages/dbandapps/api.
²⁶
semanticsSCHEMA LOCKED 2026-07-14 (Phase 3 of 6 — agents-v2/v3 build). Continuing the same 6-phase agents-v2/v3 build authorization Phase 1 (footnote ²⁴) and Phase 2 (footnote ²⁵) opened. This is Phase 3: a brand-new foundation-layer schema with zero v1 antecedent — the shared business ontology the not-yet-builtagents(Phase 5) andsignals(Phase 4) modules will read from rather than each inventing their own metric/goal/constraint vocabulary. 14 tables, 114 cols:approved_function_registry(11 — I2's supply-chain-hole-closing allowlist; a metric/attribution-model version's computation is a POINTER to a reviewed function viaverify_function_still_matches_approval(), re-resolved and re-hashed fresh on every call, never a cached OID or a one-time approval),metric_definition/metric_version(6/8, standard A2b definition/version pattern),tenant_metric_binding(12, RLS-enabled, which version is authoritative for a tenant/date-range),metric_dependency(4, real cycle-detection via a recursive-CTE trigger — closing v3's own disclosed "dead code" finding that an earlier draft of this exact logic had noCREATE TRIGGERstatement anywhere),entity_definition/entity_alias(6/5),dimension_definition(5),goal_definition/constraint_definition(8/7) plustenant_goal_binding/tenant_constraint_binding(15/15, RLS-enabled),attribution_model_definition/attribution_model_version(4/8).EXCLUDE USING gisthard-reject overlap prevention on all 3 tenant-scoped binding tables (reusingplatform.accounting_period's ownbtree_gistprecedent, but HARD-REJECT not flag-not-reject — no offline-sync reason applies to a synchronous admin action, unlike POS's offline-first sale posting).tenant_goal_binding.module_id/.site_idandtenant_constraint_binding.module_id/.site_idare deliberately BARE, no FK — a disclosed judgment call, not an oversight (platform.module_catalogalready exists but the design of record doesn't specify wiringmodule_idto it) — logged to OPEN_ITEMS. Section 4 self-audit found and fixed 2 real gaps before lock: 3lifecycle_statuscolumns missing their CHECK entirely (fixed, matchingmetric_definition's own correctly-built CHECK);tenant_goal_binding/tenant_constraint_bindingmissingcreated_at/updated_at+ the maintaining trigger their siblingtenant_metric_bindingalready had (fixed — column count grew 110→114 as a direct result). A schema-usageGRANTgap was found only by the regression suite, not the read-only Section 4 pass: the migration's original draft never ranGRANT USAGE ON SCHEMA semantics TO authenticated— every prior new-schema migration in this codebase does this explicitly; fixed live and in the migration file. 3 named guards live-reproduced via real psql sessions: EXCLUDE overlap rejection (all 3 tables), the I2 supply-chain-hole guard (register → verify true → redefine → verify false → new binding write rejected), andmetric_dependencycycle detection (A→B, B→C succeed, C→A rejected by the trigger, a direct self-reference separately rejected by a CHECK). Migrationpackages/db/migrations/20260714000000_semantics_new_schema.sql. See PROJECT_DECISIONS #64. Schema-only this phase — no service-layer changes (every phase of this build stays schema-only until Phase 5 lands theagentsmodule itself). Tests:apps/api/src/platform/__tests__/semantics-schema.spec.ts, new file, 19/19 passing; a companion stale-assertion fix landed in the pre-existingplatform-module-registry.spec.ts(G3, updated to assertsemanticsis now'in_build'inmodule_catalogwhileagents/signalsstay'designed'). Fullapps/apisuite: 1150/1150, typecheck clean onpackages/dbandapps/api. Phase 3 lock-gate verdict: CLEAN. Phase 4 (signals, HIGHEST RISK) is next.
²⁷
signalsSCHEMA LOCKED 2026-07-15 (Phase 4 of 6 — agents-v2/v3 build, HIGHEST RISK). Continuing the same 6-phase agents-v2/v3 build authorization Phase 1 (footnote ²⁴), Phase 2 (footnote ²⁵), and Phase 3 (footnote ²⁶) opened. This is Phase 4 — this build's own explicitly-designated highest-risk phase, combining 3 mechanisms no prior phase combined in one schema: table partitioning, a split-authority pattern for an invariant a native partitioned index cannot express, andSECURITY DEFINERtenant-isolation functions. A brand-new foundation-layer schema with zero v1 antecedent — the feature/forecast/outcome store the not-yet-builtagentsmodule (Phase 5) will read from and write to; oppositesemantics's own operational profile (observational, high-volume, bitemporal, append-only, partitioned rather than definitional/low-volume/human-curated). 11 tables, 111 cols:feature_definition/feature_version(6/7, standard A2b catalog pattern),experiment/experiment_version(6/9, same pattern plus B4's ownallows_reentry/washout_days),feature_value/forecast/anomaly_score(10/11/10, PARTITIONED weekly byrecorded_at— 12 weekly partitions + 1DEFAULT= 13 each —REVOKE ALLfromauthenticated, readable only through 3SECURITY DEFINERas-of functions),experiment_assignment/experiment_exposure_event(19/5, RLS-enabled, NOT partitioned),outcome_observation/outcome_authority(19/9,outcome_observationPARTITIONED monthly bycreated_at— 13 monthly partitions + 1DEFAULT= 14 —outcome_authoritydeliberately unpartitioned, the split-authority pattern). 4 named critical guards, each corresponding to a v3 BLOCKER, all live-reproduced: GUARD 1 (BLOCKER 1) — ap_tenant_idparameter on aSECURITY DEFINERfunction is spoofable by construction (no validation closes it), so the 3 as-of functions (get_feature_as_of/get_forecast_as_of/get_anomaly_score_as_of) derive tenant scope from a newplatform.current_tenant_id()session-GUC helper instead, with no tenant argument in the signature at all; each is owned by a new, minimalsignals_function_ownerrole (NOLOGIN, not superuser, not the table owner) with its own narrowSELECTgrant on exactly the 3 tables it reads,search_pathpinned,REVOKE EXECUTE FROM PUBLIC. GUARD 2 (BLOCKER 2) — a native unique/partial-unique index enforcing "exactly one authoritative observation per scope" cannot survivePARTITION BY RANGE(created_at)(Postgres requires the partition key in the constraint, which would silently narrow the guarantee to per-partition-month scope); resolved via a genuinely separate, unpartitionedoutcome_authoritytable whose own PK (the scope tuple) is the real enforcement, maintained bypromote_authoritative_observation()— a native, race-freeINSERT ... ON CONFLICT ... DO UPDATEupsert, no advisory lock needed. GUARD 3 (BLOCKER 3) —assigned_atforgery is closed byforce_assignment_timestamp(), an unconditionalBEFORE INSERTtrigger overwriting any caller-supplied value withclock_timestamp()(notnow(), which is transaction-start-frozen and forgeable by holding a transaction open); a second, more subtle live-reproduced race — a genuinely-earlier exposure event arriving AFTER the causal basis is already locked — is closed via matchingSELECT ... FOR UPDATErow-locks inlock_experiment_causal_basis()(function built this phase; its ownCREATE TRIGGER ... ON agents.decision_context_snapshotis deferred to Phase 5, which builds that table) andupdate_first_eligible_exposure(), which row-lock the assignment FIRST — the actual fix, not the contamination-flagging branch logic alone — flippingcontamination_statusto'exposure_revised_post_lock'andoutcome_authority.remeasurement_requiredtotruerather than silently movingfirst_eligible_exposure_at. GUARD 4 (B3 mandatory test 4a) — enforced bitemporal read: afeature_valuerow withas_ofin the business past butrecorded_atafter a decision's ownknowledge_cutoffis structurally invisible to that decision viaget_feature_as_of(), live-reproduced exactly as specified. A genuine gap in the design doc's own literal SQL, found and fixed during THIS phase's own migration-apply pass, not a later audit: the design specifiedanomaly_score's uniqueness as(id, tenant_id)andoutcome_observation's second unique constraint withoutcreated_at— both fail live against Postgres's own structural requirement (the identical error class BLOCKER 2 itself documents, just not caught by that section's own adversarial pass for these 2 sibling constraints). Fixed:anomaly_scorenow carriesUNIQUE(id, tenant_id, recorded_at);outcome_observation's scope-version constraint now includescreated_at, backstopped by a genuine cross-partitionBEFORE INSERTtrigger (check_outcome_observation_no_duplicate_version(), usingpg_advisory_xact_lock+ a real cross-partitionSELECT) mirroring Phase 2's own established resolution for the identical bug class (ai.agent_execution.idempotency_key). 2 privilege-mechanics gaps found live during the migration-apply pass itself, neither anticipated by the v2 or v3 design:ALTER FUNCTION ... OWNER TO signals_function_ownerneeded both role-membership and schema-CREATEgrants the migration-running role didn't have by default (this local Supabase stack's ownpostgresrole is confirmed NOT a true superuser,rolsuper = false) —CREATEisREVOKEd again immediately after the 3 ownership transfers; and theSECURITY DEFINERfunction body's own call toplatform.current_tenant_id()neededsignals_function_ownerto holdUSAGEon schemaplatform, a genuinely separate requirement fromEXECUTEon the function itself (PUBLIC-granted by default) — found only when live-reproducing GUARD 1 for the first time. Section 4 self-audit found and fixed 1 real gap before lock: 4 JSONBlineage/stratification_attributescolumns had no documented example shape (item J) — fixed, inline example comments added to all 4 in the Drizzle schema files; all other A-P+T+U items PASS or N/A (Item U — Design-Phase Integrity — is N/A, brand-new schema with no v1 predecessor). 2 disclosed forward-refs, both logged todocs/open-items/OPEN_ITEMS.md(signals \| FK):outcome_observation/outcome_authority.agent_action_idare bare uuid —agents.agent_actiondoesn't exist until Phase 5 (confirmed live: zero tables in theagentsschema as of this phase); and the 3 as-of functions haveREVOKE EXECUTE FROM PUBLICbut noGRANT EXECUTEto any role —agent_readerdoesn't exist until Phase 6, matching this design's own explicit allowance to "stub the REVOKE ahead of the role's full build" (all 4 critical-guard live-reproductions worked around this by temporarily grantingEXECUTEtoauthenticatedand revoking it again afterward, both live and in the regression suite's own setup/teardown).subject_type/subject_refonexperiment_assignmentresolve a genuine internal inconsistency in the design doc itself (its own column-list prose saidassignment_unit, its own "Uniqueness" line saidsubject_type, subject_ref) — a disclosed judgment call, treated as the same concept, named per the more specific, typed, structurally-consistent form matching this codebase's established polymorphic-pointer convention. Migrationpackages/db/migrations/20260715000000_signals_new_schema.sql. See PROJECT_DECISIONS #65. Schema-only this phase — no service-layer changes (every phase of this build stays schema-only until Phase 5 lands theagentsmodule itself). Tests:apps/api/src/platform/__tests__/signals-schema.spec.ts, new file, 20/20 passing; a companion stale-assertion fix landed in the pre-existingplatform-module-registry.spec.ts(G3, updated to assertsignalsnow correctly bumps to'in_build'alongsidesemanticsinmodule_catalog, whileagentsalone stays'designed'). Fullapps/apisuite: 1172/1172, typecheck clean onpackages/dbandapps/api. Phase 4 lock-gate verdict: CLEAN. Phase 5 (agents, module #28, ~30 tables) is next.
²⁸
agentsSCHEMA LOCKED 2026-07-16 (module #28) — Phase 5 of 6 of the agents-v2/v3 build authorization, the module the entire authorization exists to build (Phase 1 reopenedplatform, PROJECT_DECISIONS #62; Phase 2 reopenedai, PROJECT_DECISIONS #63; Phase 3 builtsemantics, PROJECT_DECISIONS #64; Phase 4 builtsignals, PROJECT_DECISIONS #65; this is Phase 5). 47 tables, 475 cols — 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) merged with v2's own A1–A8 amendments and v3's own BLOCKER1–8/I1–I8 corrections (v3 wins on conflict), 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 disclosed as SUPERSEDED by the newtool_definition/tool_versionversion-row pattern (v2's own A2b definition/version split, zero live v1 rows) rather than carried forward.agent_decision,decision_context_manifest, anddecision_context_snapshotare PARTITIONED (monthly, by their own respective date columns) — live-confirmed viapg_inherits. 8 distinct trigger-backed critical guards, all live-reproduced (this build's own design named 7; a 6th guard, T1, was added and proven during the Section 4 audit pass itself — disclosed honestly as an 8th rather than force-fit into the original count): 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, plusagent_action/agent_decisionconsistency), the runaway-ceiling guard (BLOCK not flag, closing v1's own Gap 1), A5 corrected (the kill-switch check fires onUPDATE OF status— i.e. task CLAIM — not justINSERT), 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'sIS NOT NULLcheck is UNRELIABLE used directly as anIFcondition — empirically confirmed via a bareDOblock (a genuinely non-null, fully-populated record still evaluated as if NULL) — silently swallowed the entire A8 Rule 4 spend-ceiling check insidevalidate_skill_activation(), fixed by rewriting to scalar variables checked individually; (b)SELECT MIN(...) ... FOR UPDATEis syntactically REJECTED by Postgres ("FOR UPDATE is not allowed with aggregate functions"), fixed by splitting into a lock-onlyPERFORM ... FOR UPDATEfollowed by a separateSELECT MIN(...) INTO ...over the now-locked rows. 1 genuine security gap found and fixed during the SECTION 4 SELF-AUDIT (not live guard testing):agents.kill_switch_eventcarriedtenant_id+GRANT SELECT, INSERT TO authenticatedbut 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 (or a platform-wide'global'kill, thoughWITH CHECKcorrectly blocked that path for ordinary tenant sessions) — fixed with the same 2-policy mixed-scope RLS pattern already established byagent_eval_suite/rollback_recipein this same build, live-reproduced (cross-tenant SELECT returns 0 rows, cross-tenant/global forged INSERT rejected, legitimate own-tenant INSERT succeeds) and covered by a new regression test (I5). The same audit found and fixed item J (15 JSONB columns across1650 lines, incl. a same-day SECTION 10 appended after the Section 4 self-audit's own 2 fixes). Consumestask.ts/eval.ts/shadow.ts/action.ts/policy.ts/decision_context.ts/skill.ts/tool.tslacked documented example shapes) and disclosed, not fixed, item K (12 tables lack atenant_id-leading index — logged to OPEN_ITEMS, low severity since RLS correctness is unaffected and each table's natural query path is via an FK-indexed parent); all other Section 4 items (A, C–I except J, L–P) PASS. 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_classis global/notenant_id) andsignals.outcome_observation/outcome_authority.agent_action_id → agents.agent_action(id, tenant_id)(composite FKs) — pre-migration orphan audits found 0 violating rows for all 3; Phase 4's own pre-existingsignals-schema.spec.tsused 5 hardcoded placeholderagent_action_idUUIDs that had to be retrofitted with a real minimal fixture chain (identity.actor → agent_identity → decision_context_snapshot → ai.agent_execution → agents.agent_decision → agents.agent_action) to keep passing.ai.agent_executiongained 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_idis now wired, andapprovals.approval_request'schk_approval_request_source_moduleCHECK was widened to add'agents'(11 values total) — all disclosed as companion touches inside this one migration, not separate reopens.platformalso 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'sdomain→module_idretype scoped to EXACTLY 3 columns (agent_task.domain,tool_definition.domain,skill_definition.domain) per A1's own explicit scope-limiting text — NOT extended toagent_performance_profile.domain/agent_autonomy_profile.domain(both stay free-text);agent_eval_run.eval_suite_version_idretargets toagent_eval_suite_version.id(A2b version-pinning), not the identity row;agent_actionhas NOagent_task_idcolumn — BLOCKER4's trigger derives it via a join throughagent_execution_idinstead (a genuine gap in v3's own literal SQL, fixed structurally not patched); A3's "trigger ontenant_agent_deployment" resolved instead as a trigger onagents.agent_skill_assignment(the only table with bothagent_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_executionchecks only its owntool_version_id;agents.decision_context_snapshotchecks its ownmodel_version_id/prompt_version_id/skill_version_id/toolset_version_id). A transitional Drizzle barrel collision —identity.agentSkillAssignment(v1, still used by productionIdentityService) andagents.agentSkillAssignment(I5's new destination table) — coexists until Phase 6 drops the old one, resolved via explicit named re-exports inpackages/db/src/schema/index.ts. Migrationpackages/db/migrations/20260716000000_agents_module_new_schema.sql(identity.agent_duty_grant/agent_identityfor all agent authority — no new authority mechanism introduced (this module is itself the primary consumer, not a new grant surface). Schema-only — noAgentsServiceyet (every phase of this build stays schema-only until Phase 6, which builds theagent_readerrole + connection helper + an ESLint ban onadminDb/tenantDB/set_configfrom agent read paths). Tests:apps/api/src/platform/__tests__/agents-schema.spec.ts, new file, 24/24 passing;apps/api/src/platform/__tests__/signals-schema.spec.ts(Phase 4's own file) retrofitted with the realagent_actionfixture chain described above. Fullapps/apisuite: 1198/1198 (serially — a known pre-existing connection-pool-exhaustion flake indatabase/__tests__/rls-cross-tenant.spec.tsintermittently fails only under parallel workers, unrelated to this build). See PROJECT_DECISIONS #66. Phase 6 (identity's 6th reopen — DROPPING the legacyidentity.agent_skill/agent_skill_assignmenttables now fully superseded by this module's ownskill_definition/skill_version/agent_skill_assignment, plus the newagent_readerrole/connection helper and an ESLint ban onadminDb/tenantDB/set_configfrom agent read paths — see footnote ²⁹) is the final phase of this 6-phase build authorization — CLOSING IT.
²⁹ IDENTITY REOPENED A 6TH TIME 2026-07-17 (Phase 6 of 6 — agents-v2/v3 build, CLOSES THE ENTIRE BUILD). Continuing the same 6-phase agents-v2/v3 build authorization Phase 1 (footnote ²⁴) through Phase 5 (footnote ²⁸) opened. This is Phase 6, the final phase:
identity.agent_skill(9 cols) andidentity.agent_skill_assignment(10 cols) — the v1 skill-catalog tables — were DROPPED, now fully superseded byagents.skill_definition/skill_version/agent_skill_assignment(built Phase 5, footnote ²⁸). A mandatory pre-migration audit foundagent_skill_assignmenthad 0 live rows andagent_skillhad 13 live rows, all test debris (test.skill.%/ti3.skill.%patterns, no real data) — cleaned up in the migration, not preserved.IdentityService's skill methods (listSkillCatalog,assignSkillToAgent,removeSkillFromAgent,listAgentSkillAssignments, and the skill-check half ofauthorizeAgentAction) were rewritten inapps/api/src/identity/identity.service.tsto join throughagents.skill_version/skill_definitioninstead — assignment is now at SKILL VERSION granularity (agents.agent_skill_assignment.skill_version_id), not skill-identity granularity; callers now passskillVersionId(renamed fromskillId),listAgentSkillAssignments's return field is nowskillVersionId, andlistSkillCatalog's return shape changed to{id, code, name, lifecycleStatus}(droppedcategory/moduleCode— no equivalent on the flatteragents.skill_definitioncatalog). Zero controllers/DTOs referenced any of these 4 methods (grep-confirmed) — no live HTTP consumer affected. This resolves the transitional Drizzle-barrel collision footnote ²⁸ disclosed (identity.agentSkillAssignmentvsagents.agentSkillAssignmentcoexisting) — the old export is gone.identityis now 36 tables / 410 cols (down from 38/429). Also this phase: a NEWagent_readerPostgres role (NOLOGIN NOINHERIT, mirrorsconsumer_authenticated's shape exactly), granted toauthenticator— scoped to (a)files.document_chunk/document_index(GRANT SELECT + 2 new dedicated RLS policies,document_chunk_agent_reader_select/document_index_agent_reader_select, the sametenant_id = current_setting('app.current_tenant_id')::uuidpredicate the existingauthenticated-scoped policies already use) and (b) EXECUTE on signals' 3*_as_of()functions (get_feature_as_of/get_forecast_as_of/get_anomaly_score_as_of, all(uuid, uuid, timestamptz, timestamptz)) — closing the disclosed forward-ref Phase 4 (footnote ²⁷, PROJECT_DECISIONS #65) left open.filesstays 6 tables/89 cols andsignalsstays structurally unchanged — both are GRANT/policy-only touches, no table/column impact. A real gap was found and fixed during this build: the pre-existingauthenticated-scoped RLS policies ondocument_chunk/document_indexdo NOT automatically extend toagent_reader(Postgres role-scoped policies apply only to the exact role named — confirmed live viapg_policies,roles = {authenticated}) —GRANT SELECTalone would have left everyagent_readerquery silently returning zero rows; fixed with the 2 new SELECT-only policies. A newagentReaderDB()connection helper (packages/db/src/client.ts, exported frompackages/db/src/index.ts) mirrorsconsumerDB()exactly (SET LOCAL ROLE agent_reader+set_config('app.current_tenant_id', ...)). A new ESLint rule (apps/api/eslint.config.mjs, scoped tosrc/agents/**/*.tsexcluding__tests__) bansadminDb/getAdminDb/tenantDBimports from@vrida/db(no-restricted-imports) and rawset_config(...)calls (no-restricted-syntax) — verified firing via a temporary probe file (removed after verification; nosrc/agents/directory exists in the committed tree yet, since noAgentsServiceexists). Honest disclosure:agent_reader/agentReaderDB()have ZERO real call sites today (noAgentsServiceor other agent-execution read path exists yet) — logged as a new OPEN_ITEMS row, not silently treated as fully closed. 4 guards live-reproduced (tenant-scoped read, cross-tenant isolation, write rejection42501, EXECUTE on the 3 signals functions) via a new regression describe block, "agent_reader role / agentReaderDB() (Phase 6, 2026-07-17)", inapps/api/src/database/__tests__/rls-cross-tenant.spec.ts(4 tests). 4 OPEN_ITEMS rows CLOSED this phase (files/agent_reader,ai/ESLint rule,signals/GRANT EXECUTE, and a stale row aboutidentity.agent_skill_assignmentneeding a reverse-lookup index — closed-obsolete, since the table it named was dropped); 1 new open row logged (agentReaderDB()zero call sites). Migrationpackages/db/migrations/20260717000000_identity_reopen_drop_legacy_agent_skill.sql+packages/db/migrations/20260717000001_agent_reader_role.sql. See PROJECT_DECISIONS #67. Fullapps/apisuite: 1205/1205 (up from 1201 — 4 newagent_readertests). Phase 6 lock-gate verdict: CLOSES THE ENTIRE 6-PHASE agents-v2/v3 BUILD AUTHORIZATION (Phase 1platform, Phase 2ai, Phase 3semantics, Phase 4signals, Phase 5agents, Phase 6identity+agent_reader— footnotes ²⁴–²⁹, PROJECT_DECISIONS #62–#67).
A separately-authorized Phase 1: Security & Integrity Remediation (2026-07-18) then ran — a live-DB structural sweep across all 25 business schemas (GRANT/REVOKE fixes, append-only enforcement on 16 tables, consumer.event consent enforcement, a new platform.processor_catalog reference table, a cross-module bare-FK audit, a new drift CI gate, and 4 missing set_updated_at triggers), not a new module and no service-layer work. platform gained 1 table (processor_catalog) — now 36 tables / 524 cols (up from 35/518); every other touched schema is constraint/trigger-shape-only. shared's own write-permission lockdown is explicitly deferred to this same effort's Phase 2. Full apps/api suite: 1205/1205 (unchanged). See PROJECT_DECISIONS #68.
³⁰ INVENTORY CORE WRITE PROTECTION REOPEN — RE-LOCKED, 2026-07-15 task run (PROJECT_DECISIONS #75). Schema-only hardening of direct Inventory writes: 26 base tables / 362 base-table columns / 1 view. Ordinary roles retain only catalog edits excluding
avg_cost_centsand the four ruled stock-planning columns; operational stock, reservation, movement, movement-line, and Returns receipt posting paths are structurally protected. Transfer tables, Transfer wrappers, executor grants, services, APIs, UI, and runtime wiring remain unbuilt. The first independent verifier returned NOT CLEAN; all ten findings were dispositioned, and the final fresh-context verifier found and forced two more additive corrections before returning CLEAN. Final live shape includes 137 indexes, with complete reservation fulfillment source-unique and every Inventory function unreachable toinventory_command_executor.
³¹ STOCK TRANSFER SCHEMA BUILD — 2026-07-16 task run. Inventory gains
transfer/transfer_line/transfer_reconciliation_event: exactly 3 tables / 63 columns / 28 CHECKs / 20 FKs / 11 triggers. Current live Inventory is 29 tables / 425 columns / 1 view. One atomic shipment, partial receipt, one optional lot per line, exact outbound movement-line cost provenance, protected reservations/movements, lifecycle soft-delete guards, and canonical locking are built at the schema layer. Additive verifier correction20260720000025makes same-key receipt races deterministic, centralizes the exactFOR UPDATEhierarchy, freezes lifecycle evidence and pointers, and hardens exact retries;20260720000026aligns finalization retries for fully cancelled lines. Neither changes the ruled shape. Consequential wrappers remain dormant to all runtime roles. No Transfer service, API, UI, worker, module wiring, or trusted credential gateway was built.
| Module | Schema locked | Docs | API | Pages | Issues | Done |
|---|---|---|---|---|---|---|
platform |
✅⁸,²⁴ | ✅ | ✅² | 🚧³ | 21 | ⬜ |
identity |
✅¹,⁸,⁹,²⁹ | ✅ | ⬜⁵ | 🚧⁴ | — | ⬜ |
shared |
✅⁶,⁸ | ✅ | N/A | N/A | — | ⬜ |
multi_loc |
✅⁷,⁸,³¹ | ✅ | N/A | N/A | — | ⬜ |
pricing |
✅¹³ | ⬜ | ⬜ | ⬜ | — | ⬜ |
payments |
✅¹⁹ | ⬜ | ⬜ | ⬜ | — | ⬜ |
integrations |
⬜ | ⬜ | ⬜ | ⬜ | — | ⬜ |
files |
⬜ | ⬜ | ⬜ | ⬜ | — | ⬜ |
ai |
✅¹²,²⁵ | ⬜ | ⬜ | ⬜ | — | ⬜ |
semantics |
✅²⁶ | ⬜ | ⬜ | ⬜ | — | ⬜ |
signals |
✅²⁷ | ⬜ | ⬜ | ⬜ | — | ⬜ |
agents |
✅²⁸ | ⬜ | ⬜ | ⬜ | — | ⬜ |
search |
⬜ | ⬜ | ⬜ | ⬜ | — | ⬜ |
inventory |
✅¹¹,³⁰,³¹ | ✅³⁰,³¹ | ⬜ | ⬜ | — | ⬜ |
crm |
✅¹⁰ | ⬜ | ⬜ | ⬜ | — | ⬜ |
pos |
✅¹⁴ | ⬜ | ⬜ | ⬜ | — | ⬜ |
orders |
✅¹⁵ | ⬜ | ⬜ | ⬜ | — | ⬜ |
purchasing |
✅¹⁶ | ⬜ | ⬜ | ⬜ | — | ⬜ |
tax |
✅¹⁷ | ⬜ | ⬜ | ⬜ | — | ⬜ |
billing |
✅¹⁸ | ⬜ | ⬜ | ⬜ | — | ⬜ |
admin |
✅²⁰ | ⬜ | ⬜ | ⬜ | — | ⬜ |
approvals |
✅²¹ | ⬜ | ⬜ | ⬜ | — | ⬜ |
audit |
⬜ | ⬜ | ⬜ | ⬜ | — | ⬜ |
notifications |
⬜ | ⬜ | ⬜ | ⬜ | — | ⬜ |
reporting |
⬜ | ⬜ | ⬜ | ⬜ | — | ⬜ |
consumer |
⬜ | ⬜ | ⬜ | ⬜ | — | ⬜ |
rewards |
✅²² | ⬜ | ⬜ | ⬜ | — | ⬜ |
offers |
✅²² | ⬜ | ⬜ | ⬜ | — | ⬜ |
returns |
✅²³ | ⬜ | ⬜ | ⬜ | — | ⬜ |
consumer_app |
⬜ | ⬜ | ⬜ | ⬜ | — | ⬜ |