Skip to content

The web app

nextjs-frontend/ is a Next.js App Router app, the Lens web app (built on the Aladdin design system). NextAuth holds the session; every screen talks to the API through the generated, typed client.

Layout

nextjs-frontend/
  app/
    (auth)/                 sign in, first-run setup, password reset, signin-link/ (emailed sign-in links),
                            external-signin/ (sign in with another provider)
    (setup)/welcome/        the setup wizard after the first admin signs in (components/setup/)
    (public)/explore/       pages for visitors, no sign-in needed (docs/access.md): the home page,
                            collections/[ns], search, recordings/[id]
    (app)/                  every signed-in screen, inside the app shell
      page.tsx              Home, with the welcome tour for new people (components/home/welcome-tour.tsx)
      library/ import/ reports/ recordings/[id]/ notes/
      search/ chat/ speakers/ graph/ topics/ entities/ batches/ collections/
      activity/ sources/ sensors/ pipelines/ templates/ routines/ workflows/
      extensions/ content-types/ resources/
      settings/ admin/ account/ iiif/
    api/v1/, embed/, s/, iiif/, reports/, static/, mcp/, id/, ns/, .well-known/
                            route handlers that proxy those paths to the API (lib/api/backend-proxy.ts)
    styles/tokens.css       design tokens: Aladdin + Lens additions, light and dark
  components/
    ui/                     primitives from the design's Foundations (see below)
    app-shell/              nav rail, top bar, namespace switcher, ⌘K palette, activity drawer, account menu
    <area>/                 one folder per screen area; pure logic in *-model.ts / model.ts, tested in __tests__/
  lib/
    api/                    typed clients (server and browser), the proxy, SSE streams
    hooks/                  session and archive context (roles, current namespace), jobs
    format.ts               times, dates, sizes
  app/openapi-client/       generated from the API's OpenAPI schema; never edit by hand

The nav rail (components/app-shell/nav-config.ts) is in four groups, each named above its first item (a line when the rail is collapsed to icons): what people use every day (Home, Library, Search, Chat, Notes), Explore what Lens found (Timeline, Entities, Topics, Graph, Podcasts, Reports), Automate what runs by itself (Approvals while something waits, Automations, Pipelines, Extensions, Sensors, or Sources for people who aren't admins), and Manage Lens (Costs, Analytics, Settings, Admin). Every page stays one click from Home; ⌘K reaches every page, recording and setting by name. The nav fits a phone screen without a swipe (ci/clicks counts one), so a new page shares a row rather than adding one: Schedules is a tab of Automations and the Routines page opens from both (New routine is at the top of each), and workflows, templates and content types are tabs of Pipelines.

Design system

Colour is semantic, never decorative: blue means working, selected or a link; green means ready or succeeded; red means failed or destructive; gold ◆ means a person needs to decide; grey means waiting. Pipeline steps use the four-colour loop (Transcribe blue, Diarize red, Analyze green, Summarize and Report gold).

  • Tokens live in app/styles/tokens.css and are mapped into Tailwind (bg-surface, text-fg-secondary, border-blue-border, spk-1…8, emo-*, rounded-pill, shadow-2...). Don't hard-code colours: the dark theme switches by variables ([data-theme=dark], or the system preference).
  • Tailwind 4 is configured in CSS: the @theme block in app/globals.css maps the tokens, and there is no tailwind.config.js. Its utilities stay outside cascade layers there, so a class still beats the notes editor's global element styles (BlockSuite adds rules such as input { font-size: 12px } to the page).
  • Type: DM Sans for the UI (13–14 px), Source Serif 4 for an asset's text (.transcript-text, 17.5/1.6), JetBrains Mono for paths and code. All three are self-hosted in public/fonts.
  • Primitives (components/ui): Button (pill; disabledReason explains a disabled action), IconButton, Badge, StatusChip, RoleChip, SpeakerChip, EmotionChip, JobStepChip, Field/Input/Select/Switch/Checkbox, SecretField, Tabs, Dialog, Drawer, Menu, Popover, Toast, Banner, Table with sortable headers and Pagination, Skeleton, EmptyState, CodeBlock, DateTime, Avatar, Progress, PageHeader, Panel, StepLoop, VerdictCard, AgentChip.
  • On a phone (below 640 px, Tailwind's sm), nothing needs a sideways swipe and nothing is out of reach: the namespace switcher sits at the top of the menu, Import is an icon in the top bar, settings sections open as a list from the current section's name, tabs wrap, and wide tables (sensors, connections) become cards. A form's main buttons take phone-actions (app/globals.css) to stay pinned at the bottom of the screen, clear of the floating Ask button; a Dialog's actions stay in sight on their own.
  • The floating Ask button (components/page-chat/ask-place.ts) finds what it would cover with the browser's own hit testing, through a transparent probe just under it, and rises above any control there that scrolling can't carry clear (one in a bar that's fixed or stuck to the bottom, or with the page at its end), or steps out of sight when that would take it past the middle of the screen, so a new page needn't leave room for it (phone-actions and the Settings save row still do, and it stays put there). The clicks survey fails a journey whose control it covers, and checks every page with the page scrolled to its end. The clicks survey measures every journey on a phone too.
  • The role rule: actions a person can't take stay visible, disabled, with a tooltip saying who can (needRole()); namespaces they have no role in never appear. useArchive() gives can(role, ns) and the current namespace.

Data

  • Client components use useApiClient() with React Query and the generated SDK; data() unwraps a call and throws ApiError with the API's message, and page() unwraps a list call into {items, total} (the total comes from the X-Total-Count header). Server components use getApiClient().
  • The loading bar along the top (NProgress, lib/progress.ts) runs while a page or its first data loads, as in Gmail. instrumentation-client.ts starts it on every page change (a link, router.push, back and forward) and finishes it when Next writes the new address into the browser's history, which it does for every page change that lands, even a redirect back to the page showing. DataProgress in the providers runs it while a query has no answer yet. Refreshing data already on screen (polling, refetching after a change) and anything quicker than 150 ms don't show it.
  • Streams (job events, chat answers) use streamSSE() with the session's access token.
  • Reading again after a change the server just made: refetchFresh(qc, key) (components/chat/data.ts), not invalidateQueries alone. A query's first read still on its way was asked before the change, and invalidating waits for that read rather than making a new one, so a new conversation showed its question without the answer just saved.
  • Media links in API responses are signed relative URLs; the proxy serves them from the app's origin, so they work in <audio>, <video> and <img>.

The proxy

/api/v1, /embed, /s (short share links), /iiif, /reports, /static, /mcp (the MCP server), /id/... and /ns (linked-data URIs and vocabulary) and /.well-known/ (OAuth discovery) are forwarded to API_BASE_URL by route handlers at request time (bodies, byte ranges and event streams pass straight through). Because it runs per request, one build works against any API; next.config rewrites would have fixed the API address at build time.

Developing

pnpm dev              # :3000, with API_BASE_URL pointing at the API
pnpm test             # jest
pnpm lint && pnpm tsc
pnpm build

Several dev servers can run side by side with separate build folders: NEXT_DIST_DIR=.next-a next dev --turbopack -p 3021. The dev server runs on Turbopack (pnpm dev); the production build (pnpm build) stays on webpack. Type errors in dev come from your editor or pnpm tsc: there is no type checker in the dev server. next dev adds its build folder's type paths to tsconfig.json; don't commit those lines.

When the API changes, regenerate the client: make openapi from the repository root.

Features the design shows but the API can't provide yet are listed in What the design needs from the API next.