Vrida — Technical Architecture
Refreshed 2026-06-11 to current architecture (post-pivot). Conceptual overview; see
docs/MODULE_INDEX.mdfor the module catalog,docs/PROJECT_DECISIONS.mdfor decisions.
Overview
Multi-tenant SaaS on PostgreSQL with schema-per-module organization, Row-Level Security for tenant isolation, and service-layer boundaries between modules. 17 schemas locked — 167 tables · 2,492 cols as of 2026-06-11.
History note: Vrida was originally designed as a "Generic-Core + Vertical-Extension" model with a core schema for shared primitives (item, party, UoM). That model was retired 2026-05-13 with the Nursery-Only Focus decision. There is no core schema. Masters live in owning modules: item in inventory, customer in crm, vendor in purchasing. The module separation (Platform, Identity, Payments, Integrations, AI, Files, Search as service-layer modules; nursery-facing operational modules) was preserved; the core-indirection layer was not.
Current Architecture Model
Vrida is a generic multi-vertical ERP — nursery is the first and reference vertical, but the schema is vertical-neutral (item catalog in inventory, customers in crm, vendors in purchasing). Tenant isolation is enforced at the database level via Row-Level Security. All cross-module access goes through service classes, not direct table queries.
17 locked schemas grouped by layer:
| Layer | Schemas (tables · cols) |
|---|---|
| Platform & auth | platform (16t · 301c), identity (11t · 140c) |
| Reference data | shared (7t · 57c) |
| Site topology | multi_loc (1t · 21c) |
| Item catalog & pricing | inventory (21t · 236c), pricing (4t · 56c) |
| Customers | crm (8t · 107c) |
| Transactions | pos (19t · 279c), orders (7t · 132c), purchasing (16t · 340c) |
| Financial control | billing (8t · 113c), payments (8t · 112c) |
| Configuration | admin (11t · 145c) |
| Support | audit (7t · 118c), notifications (11t · 166c) |
| Service layer | integrations (9t · 127c), files (3t · 43c) |
See docs/MODULE_INDEX.md for per-module owns, dependencies, and build order.
Tenancy Model
Pattern 2: Shared multi-tenant (v1.0–v1.5)
- All tenants share a single Postgres database
tenant_id(UUID) on every tenant-scoped table- Postgres Row-Level Security (RLS) policies enforce tenant isolation at the database level — even if application code has a bug, the database refuses cross-tenant queries
- Each request sets
app.current_tenant_idsession variable before executing queries - Application-layer tier enforcement reads
platform.tenant_entitlementto gate features
Pattern 4: Dedicated infrastructure (v2.0+ add-on)
- Top Enterprise customers can request dedicated Supabase project
- Same application code; per-tenant connection routing via registry table
- Sold as $500–$2,000/mo add-on for compliance, data residency, or contractual reasons
Schema-Per-Module Organization
Each module has its own Postgres schema. Cross-module data access goes through service classes, not direct table queries.
Platform & Service Layer
| Schema | Purpose |
|---|---|
platform |
Vrida's control plane — 16 tables locked 2026-06-09. Tenant identity, subscriptions, billing accounts, payments-to-Vrida, legal agreements, entitlements, usage tracking, setup tasks, lifecycle events, CS / internal activity. |
identity |
Authorization layer — users, tenant membership, roles, permissions, site access, SSO config, support-access grants. Supabase Auth owns authN; Identity owns authZ. 11 tables locked 2026-06-09. |
payments |
PaymentsService over Stripe — Terminal (POS card), Connect (merchant payouts), customer card payments. Owns the Stripe event log; owner-processes-own-webhooks. 8 tables locked 2026-06-10. |
integrations |
Generic external-connector runtime — OAuth credential rotation, sync runs, provider-call log, inbound/outbound webhooks, field mapping. Config lives in admin; execution lives here. 9 tables locked 2026-06-11. |
files |
Generic file-metadata registry over Cloudflare R2. 3 visibility tiers (public CDN / private tenant-RLS / signed time-limited URL). 11 R2 seams closed across 8 modules (admin logo/compliance, crm cert, pos signatures, purchasing shipment_photo, integrations sync_run, audit exports/DSR/DPA, inventory item_image). R2 owns the bytes; Files owns metadata + access grants + quota. 3 tables locked 2026-06-11. |
ai |
AIService over AWS Bedrock — model-as-config, routing, rate limits, fallback, RAG corpus. Owns the AI-powered zero-mapping import pipeline (import_job, import_file, import_record). No module calls Bedrock directly. Spec pending. |
search |
Single search abstraction. No module calls the search provider directly. Spec pending. |
Reference & Topology
| Schema | Purpose |
|---|---|
shared |
Non-tenant global reference data: country, us_state, currency, unit_of_measure, usda_hardiness_zone, plant + plant_common_name. Seed-managed; never tenant-scoped. 7 tables locked 2026-06-09. |
multi_loc |
The site concept — a physical nursery property; defines what site_id points to system-wide. Multi-site transfers + cross-site fulfillment → v1.5. 1 table locked 2026-06-09. |
pricing |
Odoo-style price lists, per-variant rules, customer/group assignments, quantity breaks, dated sales. Layers on inventory base price. 4 tables locked 2026-06-09. |
Nursery-Facing Operational Modules
| Schema | Purpose |
|---|---|
inventory |
Vertical-neutral item catalog + variants + stock + lots + kits + counts. Owns stock_reservation, weighted-avg cost, item_image. Plant fields live nursery-first directly on item. 21 tables locked 2026-06-09. |
crm |
Customers (individual/business), contacts, addresses, groups, notes, merge audit, consent log + tax certificates. Customer is tenant-scoped in crm. 8 tables locked 2026-06-09. |
pos |
In-store transactions — only true offline-first module. Sales, tenders, refunds, registers, gift cards/store credit, layaway, guarantees, sync-conflict resolution. Payments via PaymentsService. 19 tables locked 2026-06-10. |
orders |
Quotes/orders/special-orders/preorders; deposits + installment schedule; light fulfillment. Link-don't-convert to POS sale; tax finalizes at POS. 7 tables locked 2026-06-10. |
purchasing |
Vendors, POs, receiving, vendor invoices/bills, returns, landed cost. Vendor master lives in purchasing directly. Calls InventoryService to register received stock; QuickBooks via IntegrationsService. 16 tables locked 2026-06-10. |
billing |
Merchant-side A/R + A/P control layer — charge accounts, customer credit, statements, vendor bill payments. NOT SaaS subscriptions (that's Platform). 8 tables locked 2026-06-10. |
admin |
Tenant configuration only: settings, branding, tax config (Stripe Tax), integration/webhook config, compliance docs. HR/labor permanently out of scope. 11 tables locked 2026-06-10. |
audit |
Cross-cutting integrity-chained audit log + compliance workflow (GDPR/CCPA DSR, breach incidents, DPA, subprocessors, exports). Append-only; reference-don't-copy. 7 tables locked 2026-06-10. |
notifications |
Event-driven delivery orchestrator: templates, delivery attempts, preferences, in-app inbox, journeys, campaigns, two-way SMS. Consent queried from CRM at send-time. 11 tables locked 2026-06-10. |
reporting |
Materialized views over all operational modules; AI analytics via AIService. Spec pending. |
Future / Deferred
| Schema | Phase | Purpose |
|---|---|---|
production |
v1.2 | Propagation, crop lots, mother plants (pure nursery-vertical — non-nursery deployments omit). |
delivery |
v1.5 | Routes, drivers, logistics. |
services |
v2.0 | Work orders, jobs. |
Consumer Layer (consumer, rewards, offers, consumer_app) |
post-v1.0 | Platform-level consumer identity, loyalty, offers, consumer app experience. |
Cross-Cutting (Layer-Free)
These are not modules but cut across all layers:
- API & webhooks — exposed by all modules; orchestration pattern owned at the platform level
- Real-time sync — Supabase Realtime, used by all modules
- i18n — supported via
sharedreference tables
Platform Module (Locked 2026-06-09)
Platform is Vrida's control plane — it manages nurseries as Vrida's customers, from Vrida's perspective as the SaaS operator. It is not a tenant-facing operational module.
Scope: tenant identity, subscriptions, billing accounts, payments, legal agreements, entitlements, usage tracking, setup tasks (combined provisioning + onboarding), data lifecycle, lifecycle events, CS / internal activity.
16 tables locked: tenant, tenant_profile, tenant_contact, billing_account, subscription, subscription_invoice, payment, tier_definition, tenant_entitlement, agreement_version, agreement_acceptance, tenant_usage_summary, tenant_setup_task, tenant_data_lifecycle, tenant_lifecycle_event, tenant_internal_activity.
See PROJECT_DECISIONS "Platform Module (Locked 2026-06-09)" for the full table-by-table description and design rationale.
AI-Powered Onboarding Pipeline
Nursery onboarding uses AI to read uploaded files (CSV, Excel, PDF, photos) and auto-load data into tenant tables. The import pipeline lives in the AI module (import_job, import_file, import_record — 3 tables). Platform tracks onboarding milestones via tenant_setup_task. AI processing routes via AIService.
Flow:
- Tenant uploads files → AI module stores against
import_job+import_file - AIService detects data type and maps fields → records extracted into
import_record - Validation runs; flagged records surfaced for tenant review
- Tenant resolves flags
- Confirmed records loaded into target module tables (Inventory, CRM, Purchasing)
tenant_setup_taskmilestone marked complete in Platform
See PROJECT_DECISIONS "AI-Powered Self-Serve Onboarding (Locked 2026-05-13)".
Tier-Aware Architecture
- All three tiers (Starter, Pro, Enterprise) supported from day one in schema and code
platform.tenant_entitlementdrives feature gatestenant.feature_flagsJSONB allows per-tenant overrides- Usage metering enforces caps (SKU count, user count, site count, zone count)
- v1.0 launches Pro tier UI publicly; Starter and Enterprise UIs hidden until v1.1/v1.5
Architecture-First Capabilities (v1.0 schema-ready, UI may be deferred)
- Multi-site hierarchy (site → zone → bin)
- API endpoints for all data access (UI is just one consumer)
- Webhook event emitter (events captured even though webhooks not exposed)
- SSO provider table (exposed in Enterprise tier later; now in
identityschema) - Custom roles and per-zone permissions (now in
identityschema) - Custom fields per variant (Enterprise) — applies at the
inventory.item_variantlevel; nocore.itemindirection - Multi-currency support (USD only in v1.0 UI; UoM in
shared, pricing rules inpricingschema) - Per-tenant rate limits (defaults in v1.0)
Data Storage
- Primary transactional store: PostgreSQL (Supabase)
- File/blob storage: Cloudflare R2 (photos, receipts, exports, tag PDFs)
- Mobile offline cache: SQLite via Drift (Flutter)
- Real-time sync: Supabase Realtime (Postgres logical replication)
- Search: Postgres full-text search in v1.0; consider Typesense/Algolia at scale
- AI embeddings: pgvector extension inside Postgres
- Caching: None in v1.0; add Redis only when measurably needed
Infrastructure Services
- AI / inference: AWS Bedrock (Claude Haiku 4.5 primary)
- Email: Resend
- SMS: Twilio (Pro+ tier, v1.1+)
- Payments: Stripe Connect + Stripe Terminal
- CDN: Cloudflare
- Monitoring: Sentry (errors), PostHog (analytics)
Mobile-First Design
- Flutter app is the primary surface for store staff
- Phone primary, tablet secondary
- Assumption: 5G always-online with 60-second offline buffer (NOT full offline-first) — applies to all modules EXCEPT POS
- Mobile cache via Drift/SQLite for offline buffer
- POS carve-out: POS is true offline-first (cart, scan, sale completion, queue and sync). See "Offline-First Resolution" below.
Multi-Site Architecture
Vrida is architected to support multi-site tenants from day one, even though the multi-site UI activates in v1.5. The schema and service-layer patterns are multi-site-ready in v1.0.
Core Principle: site_id on every transactional record
Every operational/transactional table includes a site_id (UUID, FK to multi_loc.site). Master data (customers, vendors, plant catalog, settings) is tenant-wide and does NOT have site_id.
| Data type | site_id required? |
|---|---|
| Sale, sale_line, refund | ✅ Required |
| Reservation, reservation_line, fulfillment | ✅ Required (separate site_id and fulfillment_site_id) |
| Register, register_session, cash_drop, cash_count | ✅ Required |
| Stock, stock_movement, receipt, transfer | ✅ Required |
| Zone, bin | ✅ Required (belong to specific sites) |
| Purchase order, PO_receipt | ✅ Required (PO targets a site) |
| Crop_lot, production_movement | ✅ Required (production at a site) |
| Customer | ❌ NOT site-specific (tenant-wide) |
| Vendor | ❌ NOT site-specific (tenant-wide) |
| Plant catalog (master), variants | ❌ NOT site-specific (tenant-wide) |
| Tenant configuration | ❌ NOT site-specific (tenant-wide, may have per-site overrides) |
| Customer-facing app data | ❌ Tenant-wide for customer; per-site for orders/loyalty |
Single-site tenants
Single-site tenants auto-populate site_id from the tenant's primary/only site at record creation time. This means:
- Single-site tenants operate normally with no extra UI complexity
- Schema is multi-site-ready from day one
- Multi-site upgrade later requires zero migration of existing data
- Reports always have
site_idavailable for filtering
Cross-site operations
When operations span multiple sites:
| Pattern | How handled |
|---|---|
| Customer reserves at Site A, picks up at Site B | Reservation has site_id=A; on fulfillment, sale at Site B uses Site B's stock OR transfer from A to B happens first |
| Customer refunds at Site B for sale at Site A | Refund record has site_id=B; original sale at site_id=A; both linked |
| Stock moves from Site A to Site B | New record in multi_loc.transfer (source_site_id, destination_site_id, items, qty) |
| Customer shops across sites | Customer record is tenant-wide; sale_id at any site tags the sale to that site |
Multi-site reporting
Site_id is a first-class dimension in reporting. Every report can be filtered or broken down by site. Multi-site dashboards (v1.5+) consolidate across sites.
Offline-First Resolution
Decision (Locked v1.0): True offline-first for POS only. POS Group 13 features stand as written. 60-second buffer remains the architecture default for all other modules.
Rationale: outdoor garden centers have known network dead spots; an outage during a sale is direct revenue loss; competitive parity with Rapid/KORONA who market offline as a differentiator. POS is the only module where transient connectivity directly blocks revenue.
Scope of POS offline-first: cart, item scan/lookup, customer attach, payment via Stripe Terminal (queued), sale completion, receipt printing. Inventory deduction is local with sync on reconnect. Cross-tenant/cross-site features (charge account validation, gift card balance) remain online-required.
All other modules (Inventory, Orders, CRM, Purchasing, Reporting, Production, Customer App, Multi-loc, Admin, Billing, Audit, Notifications): 60-second offline buffer; assume always-online.
Resolves MARKET_RESEARCH_GAPS Gap #6 — Accepted.
AI Capacity and Cost
Routing: All AI inference routes through the AIService abstraction owned by the AI / Intelligence module. Model selection is model-as-config. No module calls AWS Bedrock directly. Per-tier rate limits, fallback paths, and (later) RAG corpus are owned at the AIService level.
Placeholder structure — values TBD in a later pass.
Inference volume estimates per feature per tenant
TBD.
Per-tier rate limits
TBD.
Customer App consumer chat caps
TBD.
Bedrock unavailability fallback
TBD. Options under consideration: Anthropic API direct, queue-and-retry, degrade-to-rule-based.
Real-Time vs Streaming Reconciliation
Decision (Locked v1.0): "Real-time" dashboards are redefined as polled every 30 seconds from materialized views. This is NOT streaming.
- Reporting Configurable Defaults retain the "Real-time" label but with this explicit definition.
- Reporting Out-of-Scope retains "Real-time streaming analytics — Future v2.0" because true CDC/streaming pipelines are still excluded.
- Materialized view refresh strategy is governed by CROSS_MODULE_CONTRACTS Rule 6.
Snapshot Architecture
Cross-reference: SCHEMA_CONVENTIONS.md → "Snapshot Storage".
Storage model:
- Daily deltas in
audit.snapshot_delta(Postgres, partitioned bytenant_id+ date), retained 90 days. - Weekly full snapshots exported to Cloudflare R2 (Parquet, partitioned by
tenant_id+ week), retained 7 years.
Restore procedure:
- Identify target restore date.
- Load the most-recent R2 weekly full snapshot at or before target date for the tenant.
- Forward-replay Postgres
audit.snapshot_deltarows from that weekly full to the target date. - If the target date is older than 90 days (Postgres delta horizon), the restore granularity is one week (the next weekly full after the target date).
- Restored data is loaded into a quarantined schema for verification before merge or used for read-only forensic queries.
Customer App Realtime Pattern
A consumer connected to N nurseries does NOT open N Supabase Realtime subscriptions. The Customer App opens Realtime ONLY for the consumer's primary nursery (most-recently-active or user-designated). Other connected nurseries are polled at 60-second intervals when the consumer browses that nursery's inventory.
Signature Infrastructure
Two signature paths, used based on the legal binding required by the document.
Path A — HelloSign (Dropbox Sign): legally-binding documents
Used for documents that establish legal obligations or create financial liability:
- Billing 14.3 — MSA, TOS signing during signup/upgrade
- CRM 6.2 — Credit application for charge accounts above the configurable threshold
Flow:
- Vrida generates the document and submits it to HelloSign via API
- Signer receives email with HelloSign-hosted signing page
- HelloSign returns signed PDF + envelope ID on completion
- Vrida stores signed PDF in R2 + envelope ID + signer metadata in the requesting module's schema
- HelloSign retains its own audit-trail copy independently
Path B — In-app canvas signature: casual cases
Used for signatures where legal weight is not required:
- POS 6.9 — Customer signature for charge accounts in-store, guarantees, age-restricted items, low-value signatures
Flow:
- Signer draws on touchscreen / trackpad canvas
- Signature captured as PNG
- PNG stored in R2 + metadata in the requesting module's schema (timestamp, signer ID, document reference)
Threshold rule
Configurable per tenant via Owner Dashboard. Default:
- HelloSign required for any document with legal binding (MSA, TOS, credit applications creating an A/R relationship)
- Canvas for everything else (in-store signatures, guarantees, low-value acknowledgements)
Rationale
Canvas alone is legally weak for MSA/TOS — chargeback or contract disputes need a real audit trail. HelloSign gives legal weight where it matters and keeps friction low where it doesn't. Stripe Identity (identity verification) is a different problem and is not used as the signing path.
Audit Architecture
Implementation pattern for the immutable audit log:
- Append-only enforced by Postgres trigger that blocks UPDATE and DELETE on
audit.*tables. - Per-row hash: each audit row stores SHA-256 of (
previous_row_hash||row_payload). - Daily integrity check job verifies the chain.
- External attestation (v1.0): signed-export-only. Weekly export to R2 is signed with a tenant-specific key. No object-lock or WORM storage in v1.0.
- External attestation (v2.0 / SOC 2 prep): upgrade to R2 Object Lock when SOC 2 Type II audit prep begins (Year 2 per ROADMAP).