Skip to main content

Frontend architecture

The frontend is a Next.js 14 App Router app in TypeScript, in frontend/src. It is a thin, fast client over the backend API: no server-side data layer of its own, just pages and components that call /api and render.

Stack​

ConcernChoice
FrameworkNext.js 14 (App Router), React 18
LanguageTypeScript
StylingTailwind CSS with a project design system in app/globals.css
AnimationFramer Motion
Data fetchingTanStack Query; large lists virtualized with TanStack Virtual
Iconslucide-react
Build/runnext dev (port 3000 locally), next start in production (standalone output)

Routing map​

RouteFilePurpose
/app/page.tsxEntry; routes the user into the app or to login.
/dashboardapp/dashboard/page.tsxThe team's claims, per-claim usage, and headline stat cards (claims, items, processing speed); create a claim.
/claims/[id]app/claims/[id]/page.tsxA claim: its rooms, details, submission history (components/SubmissionsCard.tsx: date, items, photos, claim key, submission id, processing time), and submit. Send for Estimation disables once every item is submitted; audio-only items show a neutral "no photo to send" note instead of a warning.
/rooms/[id]app/rooms/[id]/page.tsxA room: upload media, list jobs, review room inventory.
/jobs/[id]app/jobs/[id]/page.tsxA job: processing progress and its inventory.
/settingsapp/settings/page.tsxProfile (time zone preference), auto-approval rules, AI prompts, integrations.
/changelogapp/changelog/page.tsxCustomer changelog, rendered from src/content/changelog/customer.md. The sidebar item shows a dot while there are unseen entries (localStorage).
/updatesapp/updates/page.tsxInternal release log (admins only), rendered from src/content/changelog/internal.md; same unseen-dot behavior. Note the gate is UI-only: the markdown ships in the client bundle.
/adminapp/admin/page.tsxInternal admin dashboard (admins only): system-wide health across all teams. Overview tab (live stats, range-scoped credits and AI cost, last 10 jobs, claims, jobs, team leaderboard), Teams tab (per-team component visibility with global defaults; training ships hidden), Activity Log tab (all teams, team filter), and Media tab (browse and play every upload).
/logsapp/logs/page.tsxPipeline Logs (admins only). Jobs tab: every job system-wide with detailed logs, filters, grouping, plus per-job Mark failed (dismiss a stuck job with a refund) and Delete (hard-delete with confirm). API Requests tab (components/ApiRequestsPanel.tsx): OpenRouter calls grouped by job with per-job totals (requests, cost, time), searchable by job/claim/filename and filterable by company/user, expanding to per-call request/response JSON with an open-in-OpenRouter link.
/training, /training/missions/[key]app/training/*The training hub and mission runner. Hidden (nav and page) when the admin switches the training component off for the team. See training.
/verify/[code]app/verify/[code]/page.tsxPublic certificate verification (no sign-in).
/help, /help/[slug]app/help/*The in-app Help Center.
/login, /logout, /auth/callbackapp/*The SSO flow.
error.tsx, not-found.tsxapp/*Error and 404 pages.

A client-side SSE proxy route also exists at app/api/jobs/[id]/stream/route.ts to relay the backend stream during local development.

Dates and times format through lib/datetime.ts, which shows every timestamp in the user's chosen time zone (Settings > Profile, stored on the user row) or the browser's local zone by default. Do not call toLocaleString with a hardcoded zone.

Changelogs are markdown files in src/content/changelog/ imported as raw strings (a webpack rule in next.config.js), parsed by lib/changelog.ts (## YYYY-MM-DD - [New|Improved|Fixed] Title, an optional summary line, then optional bullets) and rendered by components/ChangelogFeed.tsx as cards grouped under a date heading, with kind pills, a month dropdown and a search box across the top. The kind tag is optional in the parser so an untagged entry still renders (as Improved) instead of dropping off the page. Customer entries are capped at two lines and one change each; internal entries deliberately are not. Updating them on every product commit is a hard rule in CLAUDE.md. The footer (components/Footer.tsx) shows the team's credit balance and a Buy credits button (on prepaid-billing instances, moved here from the old top bar), the copyright line, and the build's commit hash (linking to /changelog) and date from NEXT_PUBLIC_COMMIT_SHA / NEXT_PUBLIC_COMMIT_TIME, which docker-compose bakes in at build time. Video playback goes through components/MediaFrame.tsx, which reserves a 16:9 box with a logo placeholder and spinner so pages never shift while media loads.

The app shell​

app/layout.tsx wraps everything in three context providers and the shell:

  • components/PostHogProvider.tsx initialises product analytics and wires page views and user identity. It is a no-op when no PostHog key is configured (the default), so it is invisible until analytics is turned on.
  • components/AppShell.tsx is the frame: the responsive sidebar and the content column. There is no desktop top bar — the team's credit balance now lives in the footer (Footer takes a billing prop), and the content column's left margin follows the sidebar's collapsed width. On mobile a slim top bar carries only the hamburger + logo.
  • components/Sidebar.tsx is the left nav with the Adjust Square logo, an environment badge (hidden in production, amber for stage, violet for local), and the signed-in user with a sign-out button. On desktop the rail is collapsible to an icon strip via a toggle above the user chip; the choice persists in the cv_nav_collapsed cookie through a shared useSyncExternalStore store (lib/navCollapsed.ts) that both the sidebar (its width) and AppShell (the content margin) read, so a toggle re-renders them together. Collapse applies at the xl breakpoint only; the mobile drawer is always full width. It also renders an Admin tab, but only when useAuth().user?.is_admin is true (the allowlisted operators). Hiding it is cosmetic; the /admin page and its API enforce access on their own (see authentication).
  • The environment comes from NEXT_PUBLIC_APP_ENV via getAppEnv() in layout.tsx.

The API client​

All backend calls go through lib/api.ts. There is no axios or generated client; it is hand-written fetch/XMLHttpRequest wrappers.

  • Auth header: actorHeaders() reads the token from localStorage (cv_auth_token) and adds Authorization: Bearer <token> to every request.
  • Base URL: everything is relative to /api, which nginx (prod) or the Next proxy (dev) routes to the backend.
  • Uploads: video/photo/audio uploads use XMLHttpRequest so a real upload-progress percentage can drive the UI.
  • Errors: failed calls try to surface the backend's detail message so users see something meaningful.
  • Streaming: subscribeToJobProgress opens an EventSource to the job stream and dispatches progress, log, and metrics callbacks.
// lib/api.ts: every call attaches the token
function actorHeaders(): Record<string, string> {
const token = getAuthToken() // localStorage 'cv_auth_token'
return token ? { Authorization: `Bearer ${token}` } : {}
}

Authentication on the client​

lib/auth.tsx provides AuthProvider/useAuth. On mount it restores a saved session from localStorage, and if none exists (and the user did not just log out) it probes /api/auth/me in case a same-subdomain AdjustSquare cookie is present. The login redirect, token exchange at /auth/callback, and logout are covered in authentication.

Real-time job progress​

When a job is processing, the page subscribes to the SSE stream and updates components/ProcessingStatus.tsx live: an animated progress bar, the current stage text, and a scrolling log. When the stream sends complete, the UI swaps to the inventory.

Component map​

AreaComponents
Shell & navAppShell, Sidebar, ModalShell, ModalPortal
Claims & roomsCreateClaimModal, CreateRoomModal, RoomUpload
UploadRoomUpload (one drop area for any media mix), ConfirmUploadsModal, GroupPhotosModal
Trainingapp/training (hub + mission runner), app/verify/[code] (public certificate page), TrainingTour (spotlight tours), TrainingTracker (floating mission task list), TrainingBadgeIcon
ProcessingProcessingStatus, JobCompletionChime
ReviewInventoryTable, ItemPhotosModal, ItemModal, ReviewMode, TranscriptPanel, VideoPlayer
SubmitSubmitClaimModal
SettingsPromptsCard, automation UI
HelpersStyledSelect, ConfirmModal, HelpArticleBody

Helper libraries: lib/media.ts (upload classification: extension lists, classifyFile, folder traversal for drag-and-drop), lib/confidence.ts (confidence buckets and colors), lib/sourceTag.ts (video/photo/audio badges), lib/automationSettings.tsx (auto-approval context), lib/chime.ts (the completion sound), hooks/useEscapeToClose.ts.

The design system​

The visual language lives in frontend/src/app/globals.css as a set of component classes (.btn-primary, .card, .as-input, .as-table, .pill-*, .eyebrow, motion primitives). The handbook you are reading reuses the same fonts (Inter Tight, JetBrains Mono, Instrument Serif), the #0B80F0 blue, and the slate palette so the two surfaces feel like one product.

Building UI? Read the conventions

The project has firm UI and motion rules (use the design-system classes, never a native <select>, animate everything in and out with Framer Motion). They are summarized in engineering conventions and live in full in the repo's CLAUDE.md.

The Help Center​

In-app help is content-driven. Articles are TypeScript modules in frontend/src/content/help/ (one file per article, registered in articles.ts), rendered by app/help/[slug]/page.tsx through components/HelpArticleBody.tsx. The house style: short verb-first headings, terse steps, screenshots, and a path that always starts from the Dashboard. This is distinct from the handbook (this Docusaurus site), which documents how the system works rather than how to perform a task in the UI.