Skip to main content

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

RoutePageNotes
/MarketingLocalised public marketing site
/welcomeWelcomeAuth-oriented welcome page
/marketingredirectRedirects to /
/loginLoginSign-in and invite-gated signup
/create-companyCreateCompanyAuthenticated onboarding step
/inviteAcceptInviteToken query parameter
/shared/:tokenSharedSessionPublic read-only summary capability
/oauth/consentOAuthConsentSupabase OAuth server consent
/privacylegal pageLocalised
/termslegal pageLocalised
/cookieslegal pageLocalised

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.

RoutePage
/appDashboard
/app/overviewMulti-company Overview
/app/new-sessionNewSession
/app/new-group-sessionNewGroupSession
/app/sessionsSessionHistory
/app/session/:idSingle or multiplayer Session dispatcher
/app/session/:id/summarySessionSummary
/app/settingsSettings
/app/competitorsCompetitors
/app/competitors/newNewCompetitorAnalysis
/app/competitors/:idCompetitorAnalysisDetail
/app/tasksTasks
/app/maturityMaturityMap
/app/progressionProgression
/app/companyCompanyAdmin
/app/adminSystemAdmin through SystemAdminGuard
/app/prospectradarfeature-gated SecretHunt
/app/voice-labhidden 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:

  • SessionSingle for ordinary sessions
  • SessionMultiplayer for 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:

  • PageHeader for consistent page title, subtitle, eyebrow, and actions
  • PrimaryButton, Select, ConfirmDialog, and AlertMessage
  • MaturityRingMap, maturity detail, radar, transition cues
  • chat markdown, composer, attachment extraction, token indicator
  • LanguageSelector
  • FeedbackWidget
  • CompanyKnowledgeTab, DocumentUpload, and CompanyDocumentsList
  • VoiceRoomPanel, 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/factoryResponsibility
useAuthAuth subscription, current profile/memberships, company selection
useConversationSingle-session message lifecycle
useLiveVoicechat-live WebSocket, PCM audio and reconnect/resume
useMultiplayerSessionParticipants, messages, presence and Realtime
useGroupInvitesGroup-session notification state
useChangeLocalei18next, local cache and best-effort DB locale persistence
createFeatureFlagShared cached flag reader/broadcaster
useVoiceEnabledSingle-user voice UI flag
useMultiplayerAccessMultiplayer UI access
useOverviewAccessPortfolio access based on flag, roles and memberships
useAidaAccessAida flag and system-admin bypass
useGroupVoiceEnabledGroup voice UI access
useLeadHuntEnabledProspect Radar access
useCompanyBudgetsEnabledBudget-indicator visibility
useWorldCupThemeTemporary 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.

ModuleMain responsibilities
_helpers.tsauth headers, image validation, shared client/logger access
auth.tsOTP, profile, avatar, export, account deletion
companies.tscompany, memberships, invitations, logo, profile/history
sessions.tssessions, messages, resume/delete, shares
chat.tstext chat, voice URL, summary, usage, Prompt Lab
config.tstheme and feature configuration
admin.tscross-company admin RPCs, audit log helper, usage reports
partners.tsactive partner reads and referral tracking
inviteCodes.tspreflight and system-admin code management
multiplayer.tsgroup sessions, messages, participants, LiveKit
feedback.tsstructure, submit, notify, list, triage, delete
maturity.tscompany maturity and task list/update/dismiss
assistants.tsAida text/live, TTS, lead hunt, early access
documents.tsdocument metadata and R2 presigned workflow
demo.tsseed/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:

  1. create a company_documents metadata row,
  2. check the fresh company store_documents setting and provider status,
  3. request a presigned upload URL,
  4. PUT the bytes directly to R2,
  5. ask confirm_upload to HEAD and atomically commit the measured size.

See Company Knowledge and Documents.

Types

FileSource-of-truth responsibility
types/maturity.ts15-dimension catalogue, five levels, colours, signals, tasks
types/readiness.tsLegacy three-state readiness compatibility
types/conversation.tsSession, message and summary contracts
types/company.tsCompany, documents, memberships, users and roles
types/companyProfile.tsFacts, learnings and profile history
types/multiplayer.tsParticipant, message and presence models
types/feedback.tsFeedback structure and triage
types/competitor.tsDiscovery, analysis and grounding
types/admin.tsAdmin 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:

  1. calls the demo function to create a company/session owned by the caller,
  2. inserts synthetic maturity/tasks through controlled service-role paths,
  3. navigates a scripted set of stable routes and data-demo hooks,
  4. plays committed narration clips where available,
  5. falls back to gemini-tts,
  6. tears down on exit and best-effort on page unload,
  7. 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:preloadError at window
  • alternate chunk-load shapes in the Sentry fallback
  • one guarded reload to avoid loops
  • /version.json polling through UpdatePrompt

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:

  1. add a lazy route and a titles translation key,
  2. add sidebar navigation only when intended for discovery,
  3. put reusable domain calls in the appropriate API module,
  4. add types near the domain source of truth,
  5. add English plus all locale resources and run the audit,
  6. use feature flags only for UX unless server enforcement is also implemented,
  7. add stable test/Aida hooks only where needed,
  8. verify permissions at the server boundary,
  9. add unit and persona-appropriate E2E coverage,
  10. check bundle output and CSP requirements.