Frontend
The frontend is a React 19 single-page application in frontend/. Vite performs
route-level code splitting, Tailwind v4 consumes CSS-first Navaid tokens, and
Supabase provides Auth, data, Storage, Realtime, RPC, and Edge Function access.
Application Shell
main.tsx
├── Sentry instrumentation (loaded first)
├── i18next initialization
├── React 19 root error hooks
└── App
├── Sentry.ErrorBoundary
├── QueryClientProvider
├── ThemeLoader
├── BrowserRouter
├── ConfirmDialogProvider
├── DocumentTitle
├── Suspense + lazy routes
├── CookieConsent
└── UpdatePrompt
UpdatePrompt is a deployment-version poller. It fetches /version.json,
compares the build identifier, and offers a reload when a newer bundle is
available. Prompt editing lives in System Admin and Prompt Lab.
Routing Structure
All route pages in App.tsx are lazy imports.
Public routes
| Route | Page | Notes |
|---|---|---|
/ | Marketing | Localised public marketing site |
/welcome | Welcome | Auth-oriented welcome page |
/marketing | redirect | Redirects to / |
/login | Login | Sign-in and invite-gated signup |
/create-company | CreateCompany | Authenticated onboarding step |
/invite | AcceptInvite | Token query parameter |
/shared/:token | SharedSession | Public read-only summary capability |
/oauth/consent | OAuthConsent | Supabase OAuth server consent |
/privacy | legal page | Localised |
/terms | legal page | Localised |
/cookies | legal page | Localised |
OAuthConsent manages its own auth redirect because the authorization request
identifier must survive sign-in.
Protected routes
/app is wrapped by AuthGuard and rendered inside AppLayout.
| Route | Page |
|---|---|
/app | Dashboard |
/app/overview | Multi-company Overview |
/app/new-session | NewSession |
/app/new-group-session | NewGroupSession |
/app/sessions | SessionHistory |
/app/session/:id | Single or multiplayer Session dispatcher |
/app/session/:id/summary | SessionSummary |
/app/settings | Settings |
/app/competitors | Competitors |
/app/competitors/new | NewCompetitorAnalysis |
/app/competitors/:id | CompetitorAnalysisDetail |
/app/tasks | Tasks |
/app/maturity | MaturityMap |
/app/progression | Progression |
/app/company | CompanyAdmin |
/app/admin | SystemAdmin through SystemAdminGuard |
/app/prospectradar | feature-gated SecretHunt |
/app/voice-lab | hidden group-voice prototype |
Routes existing in the bundle are not authorization. Pages, service calls, Edge Functions, and RLS each enforce their applicable layer.
Page and Component Organization
src/
├── pages/
│ ├── auth/
│ ├── app/
│ │ └── maturity/
│ ├── company/
│ ├── admin/
│ ├── marketing/
│ └── legal/
├── components/
│ ├── admin/
│ ├── chat/
│ ├── common/
│ ├── company/
│ ├── layout/
│ └── voice/
├── aida/
├── demo/
├── hooks/
├── services/api/
├── stores/
├── types/
├── utils/
└── i18n/
Session dispatch
pages/app/Session.tsx loads enough session metadata to choose:
SessionSinglefor ordinary sessionsSessionMultiplayerfor group sessions
This keeps real-time participant/turn logic out of the single-user component. Both ultimately use the same company maturity model and persisted message schema.
Shared UI
Notable reusable components include:
PageHeaderfor consistent page title, subtitle, eyebrow, and actionsPrimaryButton,Select,ConfirmDialog, andAlertMessageMaturityRingMap, maturity detail, radar, transition cues- chat markdown, composer, attachment extraction, token indicator
LanguageSelectorFeedbackWidgetCompanyKnowledgeTab,DocumentUpload, andCompanyDocumentsListVoiceRoomPanel, listening UI, and voice toggle controls- admin tabs under
components/admin/
There is no separate application Header component; mobile and desktop
navigation are composed by AppLayout and Sidebar.
State Management
authStore
The Zustand auth store contains:
{
user,
company,
loading,
hasSessions,
sidebarHidden,
setUser,
setCompany,
setLoading,
setHasSessions,
setSidebarHidden,
reset
}
sidebarHidden is a per-device UI preference. reset() clears identity and
session-derived state but deliberately preserves this preference across logout.
The selected company is convenience state. Every server operation still
binds identifiers to the verified user's active memberships.
conversationStore
The conversation store contains:
- active session
- ordered messages
- current maturity state and legacy readiness state
- recent signal/dimension IDs for transition highlighting
- sending, loading, error, and voice-mode state
- add/upsert/remove/set message operations
- reset
Do not treat the message array as durable. After reconnect, reload authoritative messages and reconcile by ID.
React Query
The global query client uses:
staleTime: 5 * 60 * 1000
retry: 1
Realtime features use Supabase subscriptions rather than waiting for normal query invalidation. Keep subscriptions scoped and unsubscribe on teardown.
Hooks
| Hook/factory | Responsibility |
|---|---|
useAuth | Auth subscription, current profile/memberships, company selection |
useConversation | Single-session message lifecycle |
useLiveVoice | chat-live WebSocket, PCM audio and reconnect/resume |
useMultiplayerSession | Participants, messages, presence and Realtime |
useGroupInvites | Group-session notification state |
useChangeLocale | i18next, local cache and best-effort DB locale persistence |
createFeatureFlag | Shared cached flag reader/broadcaster |
useVoiceEnabled | Single-user voice UI flag |
useMultiplayerAccess | Multiplayer UI access |
useOverviewAccess | Portfolio access based on flag, roles and memberships |
useAidaAccess | Aida flag and system-admin bypass |
useGroupVoiceEnabled | Group voice UI access |
useLeadHuntEnabled | Prospect Radar access |
useCompanyBudgetsEnabled | Budget-indicator visibility |
useWorldCupTheme | Temporary flag plus date gate |
Feature hooks improve UX; server-side checks remain authoritative where the feature creates cost or privileged access. The exact enforcement matrix is in Administration and Feature Flags.
API Service Layer
frontend/src/services/api/ is split by domain and re-exported through
index.ts.
| Module | Main responsibilities |
|---|---|
_helpers.ts | auth headers, image validation, shared client/logger access |
auth.ts | OTP, profile, avatar, export, account deletion |
companies.ts | company, memberships, invitations, logo, profile/history |
sessions.ts | sessions, messages, resume/delete, shares |
chat.ts | text chat, voice URL, summary, usage, Prompt Lab |
config.ts | theme and feature configuration |
admin.ts | cross-company admin RPCs, audit log helper, usage reports |
partners.ts | active partner reads and referral tracking |
inviteCodes.ts | preflight and system-admin code management |
multiplayer.ts | group sessions, messages, participants, LiveKit |
feedback.ts | structure, submit, notify, list, triage, delete |
maturity.ts | company maturity and task list/update/dismiss |
assistants.ts | Aida text/live, TTS, lead hunt, early access |
documents.ts | document metadata and R2 presigned workflow |
demo.ts | seed/sweep/teardown demo workspaces |
Auth contracts
Normal sign-in:
signInWithOtp(email, { shouldCreateUser: false })
Gated signup:
signInWithOtp(email, {
shouldCreateUser: true,
inviteCode: validNewUserCode,
})
The client preflight improves feedback; the database Auth hook is the actual creation gate.
Function invocation
Use the shared Supabase client for normal HTTP functions. For browser
WebSockets, call buildLiveVoiceWsUrl or buildAidaLiveWsUrl; these obtain the
current session token and construct the expected URL.
Never log a WebSocket URL containing an access token.
Document workflow
captureUpload() owns the optional-retention decision:
- create a
company_documentsmetadata row, - check the fresh company
store_documentssetting and provider status, - request a presigned upload URL,
- PUT the bytes directly to R2,
- ask
confirm_uploadto HEAD and atomically commit the measured size.
See Company Knowledge and Documents.
Types
| File | Source-of-truth responsibility |
|---|---|
types/maturity.ts | 15-dimension catalogue, five levels, colours, signals, tasks |
types/readiness.ts | Legacy three-state readiness compatibility |
types/conversation.ts | Session, message and summary contracts |
types/company.ts | Company, documents, memberships, users and roles |
types/companyProfile.ts | Facts, learnings and profile history |
types/multiplayer.ts | Participant, message and presence models |
types/feedback.ts | Feedback structure and triage |
types/competitor.ts | Discovery, analysis and grounding |
types/admin.ts | Admin stats, prompts and partners |
The frontend does not currently have generated Supabase database types. When a migration changes a row shape, update the relevant hand-written type and all select lists together.
Internationalisation
i18next is initialized before React renders. English JSON is the typed canonical resource shape; French, Finnish, Welsh, Polish, and Estonian mirror it. American English is derived at runtime from canonical UK English.
The locale detector checks query string, local storage, browser locale and the
HTML tag. Authenticated changes are persisted as BCP-47 values on users.
Run:
npm run i18n:audit
when adding or changing copy. See Internationalisation.
Aida Integration
AppLayout mounts AidaWidget when access is enabled. The model proposes
client tool calls; the browser tool registry executes only allowlisted actions
with explicit validation. Page-interaction tools use stable data-aida-*
attributes.
User confirmation and browser/server authorization are separate concerns. Sensitive actions must still go through an authorized API. See Aida.
Guided Demo
The guided demo:
- calls the
demofunction to create a company/session owned by the caller, - inserts synthetic maturity/tasks through controlled service-role paths,
- navigates a scripted set of stable routes and
data-demohooks, - plays committed narration clips where available,
- falls back to
gemini-tts, - tears down on exit and best-effort on page unload,
- sweeps abandoned demo workspaces on later runs.
Demo narration exists for English, French, Finnish, Polish, and Estonian. Welsh falls back to English narration even though the UI remains Welsh.
Error Handling
Error Boundary
App.tsx uses Sentry.ErrorBoundary, not a custom class boundary. Production
fallback copy does not expose raw exception messages. Development may show the
message for debugging.
React 19 onUncaughtError, onCaughtError, and onRecoverableError handlers
forward errors to Sentry.
Stale-deploy recovery
Vite generates hashed route chunks. After a release, an old shell may request a removed chunk. Navaid handles:
vite:preloadErroratwindow- alternate chunk-load shapes in the Sentry fallback
- one guarded reload to avoid loops
/version.jsonpolling throughUpdatePrompt
API errors
Service helpers throw controlled errors. User-facing components:
- translate known error codes
- avoid rendering server internals
- retain form state where safe
- make retries idempotent
- send unexpected failures to the logger/Sentry path
Accessibility
Current patterns include:
- semantic route titles through
DocumentTitle - visible focus rings
- keyboard-operable menus, selectors, tabs, and dialogs
- roving tab index in Company Admin tabs
- live regions for asynchronous status/errors
- labels and descriptions for OTP, upload and feature controls
- reduced-motion handling in visual effects
- automated axe coverage in Playwright
New components must work with keyboard-only navigation, 200% zoom, narrow viewports, and screen-reader announcements. See Testing and Accessibility.
Build and Performance
vite.config.ts creates stable vendor chunks for React, Supabase, D3,
Markdown, jsPDF, and i18n. It emits hidden source maps, uploads them when Sentry
build credentials exist, and deletes them before deployment. Without Sentry
credentials a fallback plugin strips maps.
Every route should remain a lazy import. Avoid moving heavy PDF, Markdown, visualisation, or audio dependencies into the initial shell.
Extension Guidance
When adding a page or feature:
- add a lazy route and a
titlestranslation key, - add sidebar navigation only when intended for discovery,
- put reusable domain calls in the appropriate API module,
- add types near the domain source of truth,
- add English plus all locale resources and run the audit,
- use feature flags only for UX unless server enforcement is also implemented,
- add stable test/Aida hooks only where needed,
- verify permissions at the server boundary,
- add unit and persona-appropriate E2E coverage,
- check bundle output and CSP requirements.