Architecture
Navaid is a browser application backed by Supabase, with request-scoped Edge Functions for privileged orchestration and a separate long-running worker for multiplayer voice. This page describes the checked-in architecture; it does not assert that every optional integration is configured in a remote environment.
System Architecture Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ Browser: React 19 / Vite │
│ │
│ Routes · Zustand · React Query · i18n · Sentry · Web Audio │
└───────────────┬─────────────────────┬───────────────────────┬───────┘
│ │ │
anon-key HTTPS/WSS function HTTPS/WSS presigned HTTPS
│ │ │
┌───────────────▼────────────┐ ┌────▼────────────────┐ ┌───▼──────────┐
│ Supabase platform │ │ 26 Edge Functions │ │ Cloudflare R2│
│ │ │ │ │ optional │
│ Postgres 17 + RLS │ │ Deno 2 │ └──────────────┘
│ Auth / OAuth 2.1 │ │ service-role client│
│ Realtime │ │ explicit auth/authz│
│ Storage: avatars │ └────┬──────┬─────┬──┘
└───────────────┬────────────┘ │ │ │
│ │ │ └──────── Resend
│ │ └────────────── Google Gemini
│ └───────────────────── LiveKit API
│
│ service-role HTTPS
┌───────────────▼─────────────────────────────────────────────────────┐
│ LiveKit Agents Node worker (`agent/`) │
│ Persistent room connection · Gemini Live · speaker attribution │
│ transcript persistence · periodic token accounting │
└───────────────┬─────────────────────────────────────────────────────┘
│
▼
LiveKit Cloud rooms
Observability: browser → Sentry ingest; traces propagate to Supabase requests.
Hosting: Cloudflare Pages serves the SPA and its security headers.
Key Architectural Boundaries
- Browser to Supabase: the browser uses the anon key. Direct table, Storage, and RPC access is constrained by RLS, grants, and function-internal authorization.
- Browser to Edge Functions: bearer tokens are verified with
supabase.auth.getUser(token). WebSocket upgrades carry a short-lived access token in the URL because browser WebSocket APIs cannot set an Authorization header. - Edge Functions to data: the shared
serviceClient()bypasses RLS. Functions must bind every requested company/session/document to the verified caller before reading or writing. - AI boundary: Gemini keys remain in Supabase function secrets or the LiveKit agent environment. They never enter the browser.
- Voice boundary: single-user voice is relayed by
chat-live; group voice uses LiveKit plus the persistent worker inagent/. - Object boundary: document bytes move directly between the browser and R2 through five-minute presigned URLs. Edge Functions authorize and meter the operation but do not proxy file bodies.
- Email boundary: application links are composed from server-owned
APP_URL; request-provided origins are ignored.
Conversation Data Flow
User submits text
│
├─ Browser validates the active session and attachment limits
│
└─ POST /functions/v1/chat with bearer token
│
├─ Verify user with Auth
├─ Load session and active company membership
├─ Enforce rate and input/output budget gates
├─ Load active v2 prompt, recent history, company facts,
│ shared maturity state, open tasks, partners, locale and scope
├─ Call Gemini text model
├─ Parse response, reasoning, maturity signals, facts and task signals
├─ Persist user/assistant messages and token audit row
├─ Atomically roll token totals into token_usage_monthly
├─ Apply maturity transitions using optimistic version persistence
├─ Generate/dedupe next-level tasks where needed
└─ Return response + transitions + task changes + budget remainder
The authoritative current maturity state is
company_profiles.maturity_state. sessions.maturity_state is a session
snapshot, while maturity_state_at_start supports completion diffs.
Persistence concurrency
Multiple sessions can update one company concurrently. The shared
_shared/maturity-persist.ts helper:
- reads the profile and version,
- applies signals,
- writes only when the expected version still matches,
- retries against the new state after a conflict, and
- keeps the session snapshot synchronized.
Do not replace this with an unconditional UPDATE; doing so can lose evidence
from a parallel session. See The Maturity Engine.
Summary Generation Flow
Client requests completion
│
└─ POST /functions/v1/generate-summary
├─ Verify owner/member access
├─ Claim session through claim_session_summary()
│ └─ competing completion receives the existing/in-progress result
├─ Enforce rate and budget limits
├─ Read transcript, maturity start/current state and company context
├─ Generate structured narrative with Gemini
├─ Create/dedupe recommended tasks
├─ Write maturity snapshot
├─ Persist token audit and atomic monthly roll-up
└─ Finalize pipeline-owned status/completed_at/summary fields
Database column guards prevent ordinary authenticated clients from finalizing a session directly.
Voice Conversation Flow
Single-user voice
Browser microphone
⇅ PCM/control frames over WebSocket
chat-live Edge Function
⇅ Gemini Live WebSocket
Gemini Live
chat-live additionally:
- verifies the query-string access token,
- verifies session ownership/membership,
- supports Gemini session-resumption handles,
- persists attributed transcripts,
- extracts maturity signals with a text-model side call,
- updates maturity state with the same concurrency helper,
- samples and records realtime token usage.
voice_enabled currently controls the browser affordance. It is not a
server-side kill switch inside chat-live; operators who need a hard stop must
also control function availability or add server enforcement.
Multiplayer text
participant message → messages table → Supabase Realtime
│
└─ chat-multi claims session turn
├─ drains unprocessed user turns
├─ composes speaker-aware prompt
├─ writes one assistant response
└─ releases/advances turn state
claim_session_turn() prevents duplicate AI responses when multiple clients
observe the same new messages.
Multiplayer voice
Browser participants ⇄ LiveKit room ⇄ LiveKit Agents worker ⇄ Gemini Live
│ │
│ ├─ validates session and current membership
│ ├─ switches input to active speaker
│ ├─ persists user and assistant transcripts
│ └─ records realtime usage periodically
│
└─ room token minted by livekit-token
├─ requires active company/session access
├─ requires participant/owner status
├─ enforces group_voice_enabled for session rooms
└─ checks company budget
Leaving/removing a participant must go through
livekit-session-control. Deleting only the database row would leave an
already-connected media participant in the room.
See Realtime and Voice.
Component Architecture
App.tsx
├── Sentry.ErrorBoundary
├── QueryClientProvider
├── ThemeLoader
├── BrowserRouter
├── ConfirmDialogProvider
├── DocumentTitle
├── public routes
│ ├── / Marketing
│ ├── /welcome Welcome
│ ├── /login Login
│ ├── /create-company CreateCompany
│ ├── /invite AcceptInvite
│ ├── /shared/:token SharedSession
│ ├── /oauth/consent OAuthConsent
│ └── /privacy /terms /cookies legal pages
└── /app through AuthGuard
└── AppLayout
├── dashboard, overview, sessions and summaries
├── maturity, progression and tasks
├── competitors and Prospect Radar
├── company and system administration
├── settings
└── hidden /app/voice-lab prototype
All route components are lazy loaded. A Vite preloadError handler and the
Sentry fallback attempt one guarded reload after stale hashed chunks disappear
during a deployment.
Layer Responsibilities
| Layer | Directory | Responsibility |
|---|---|---|
| Routes/pages | frontend/src/pages/ | Route composition and data orchestration |
| Components | frontend/src/components/ | Reusable UI and feature assemblies |
| Hooks | frontend/src/hooks/ | Auth, feature flags, voice and multiplayer lifecycles |
| Stores | frontend/src/stores/ | Ephemeral cross-component state |
| API services | frontend/src/services/api/ | Supabase and function contracts by domain |
| Types | frontend/src/types/ | Domain models and canonical maturity catalogue |
| Utilities | frontend/src/utils/ | Permissions, formatting, extraction, colour generation |
| Aida | frontend/src/aida/ | Tool registry and page-interaction contract |
See Frontend.
State Management Approach
Zustand
authStoreowns the current user/company, loading state, session-history hint, and device-local sidebar preference.conversationStoreowns the active session, messages, current maturity and legacy readiness state, touched signals, voice mode, sending/error state.
authStore.reset() intentionally preserves sidebarHidden; it is a per-device
preference and survives logout.
React Query
The application-wide defaults are a five-minute staleTime and one retry.
Feature code also uses direct Supabase subscriptions and imperative service
calls where real-time ordering or transactional control matters.
Durable server state
Zustand is never an authorization or durable storage layer. Reloadable state lives in Postgres/Auth/Storage/R2 and must be revalidated by the server.
Security Architecture
Layer 1 Browser security
CSP, HSTS, anti-framing, no-sniff, referrer and permissions policy
Layer 2 Identity
Supabase Auth token verified in RLS or by auth.getUser(token)
Layer 3 Tenant authorization
active membership + role + session/document binding
Layer 4 Data controls
RLS, grants, SECURITY DEFINER RPC checks, column guard triggers
Layer 5 Abuse and spend controls
fixed-window rate counters, feature flags, monthly budgets,
input limits, storage quotas, claim locks and idempotency ledgers
Layer 6 Audit and observability
token_usage, admin_audit_log, profile history, Sentry, function logs
Gateway verify_jwt = false does not mean unauthenticated access. It means the
function owns authentication. Public surfaces must be explicitly designed and
rate-limited. See Security and Privacy.
Secret Management
| Value | Browser | Edge Functions | Agent |
|---|---|---|---|
| Supabase URL | Yes | Auto-provided | Yes |
| Supabase anon key | Yes | Usually unnecessary | No |
| Supabase service role | Never | Auto-provided | Required |
| Gemini API key | Never | AI functions | Required |
| LiveKit API secret | Never | token/control functions | Required |
| R2 access secret | Never | documents only | No |
| Resend API key | Never | notification functions | No |
| Sentry browser DSN | Public | No | No |
| Sentry upload token | Never | No | Build environment only |
Node-side E2E setup may use the service-role key. Because it has no VITE_
prefix it is not exposed to the browser bundle.
External Service Dependencies
| Service | Used for | Degraded behaviour |
|---|---|---|
| Supabase | Auth, database, Realtime, Storage, functions | Core application unavailable |
| Gemini | Text, Live, TTS and grounded research | AI actions fail; stored data remains |
| LiveKit | Multiplayer media rooms | Text multiplayer remains; voice unavailable |
| LiveKit agent host | Group-voice AI participant | Humans may join rooms but no Navaid voice agent |
| Cloudflare R2 | Optional retained originals | Browser extraction still works; retention/download disabled |
| Resend | Invitation and notification mail | Database action can succeed with sent:false |
| Sentry | Browser telemetry | App continues; errors lack remote telemetry |
| Cloudflare Pages | SPA and static assets | Frontend unavailable or cached until expiry |
OTP delivery is controlled by Supabase Auth SMTP configuration, not by the
application's RESEND_API_KEY alone.
Database Architecture Summary
The current 31-table inventory and RPC/trigger catalogue are maintained in Database Schema. Important patterns include:
- active multi-company memberships
- shared company maturity plus session snapshots
- append-only profile history
- audit rows with server-stamped actors
- token audit rows plus daily/monthly aggregates
- claim/lock RPCs for concurrent turns, summaries, code consumption, and R2 quota commits
- pipeline-owned column guards
Extension Checklist
When adding a new capability:
- Decide which boundary owns the operation.
- Add the database migration before application code that depends on it.
- Define actor, company/session scope, and failure semantics.
- Use shared HTTP/authz/budget/locale helpers.
- Account for every Gemini call.
- Add rate limits and idempotency where retries or fan-out can amplify cost.
- Update CSP if the browser contacts a new origin.
- Add unit, E2E, and local security coverage.
- Update this guide and the function/data reference.