Vrida — Technical Architecture

Refreshed 2026-06-11 to current architecture (post-pivot). Conceptual overview; see docs/MODULE_INDEX.md for the module catalog, docs/PROJECT_DECISIONS.md for 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_id session variable before executing queries
  • Application-layer tier enforcement reads platform.tenant_entitlement to 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 shared reference 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:

  1. Tenant uploads files → AI module stores against import_job + import_file
  2. AIService detects data type and maps fields → records extracted into import_record
  3. Validation runs; flagged records surfaced for tenant review
  4. Tenant resolves flags
  5. Confirmed records loaded into target module tables (Inventory, CRM, Purchasing)
  6. tenant_setup_task milestone 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_entitlement drives feature gates
  • tenant.feature_flags JSONB 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 identity schema)
  • Custom roles and per-zone permissions (now in identity schema)
  • Custom fields per variant (Enterprise) — applies at the inventory.item_variant level; no core.item indirection
  • Multi-currency support (USD only in v1.0 UI; UoM in shared, pricing rules in pricing schema)
  • 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_id available 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 by tenant_id + date), retained 90 days.
  • Weekly full snapshots exported to Cloudflare R2 (Parquet, partitioned by tenant_id + week), retained 7 years.

Restore procedure:

  1. Identify target restore date.
  2. Load the most-recent R2 weekly full snapshot at or before target date for the tenant.
  3. Forward-replay Postgres audit.snapshot_delta rows from that weekly full to the target date.
  4. 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).
  5. 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:

  1. Vrida generates the document and submits it to HelloSign via API
  2. Signer receives email with HelloSign-hosted signing page
  3. HelloSign returns signed PDF + envelope ID on completion
  4. Vrida stores signed PDF in R2 + envelope ID + signer metadata in the requesting module's schema
  5. 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:

  1. Signer draws on touchscreen / trackpad canvas
  2. Signature captured as PNG
  3. 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).
Last modified: Jun 17, 2026, 6:57 PM PT
On this page
Esc