Skip to main content

Architecture overview

Claimpilot Chat is one Cloudflare Worker, one React application and one shared contract. The Worker serves the built front end, the JSON and SSE API, and the voice WebSocket relay. The browser renders the claim screens and the chat. A shared package defines the document and the protocol both sides speak.

Principles​

The claim is the unit of everything. The assistant answers about one claim: the one on screen. The browser sends that document with every turn, so the assistant cannot see any other. Every capability, tool and prompt is written against the Claim contract.

Read through tools, not through the prompt. The system prompt carries a compact brief. Everything specific (wording, dates, amounts, who said what) comes through tool calls, which keeps the context small and every answer traceable. The UI shows which lookups produced each answer.

Do arithmetic in code. Where a judgement depends on counting, dates or money (was the policy in force, which payments are duplicates, how many days since first contact), a deterministic tool computes it and the model reasons over the result.

One seam per moving part. Each concern has one home and one interface.

ConcernIts one homeThe seam
The claim's shapepackages/shared/src/claim.tsthe Claim zod schema
What the chat sends and receivespackages/shared/src/chat.tsChatRequest, ChatEvent
Prompt textapi/src/prompts/catalogue.tsresolvePrompt(id, overrides)
A capabilityone object in api/src/capabilities/<group>.tsCapability
The modelapi/src/ai/AiProvider
The claims storeapi/src/claims/ClaimRepository
The feedback storeapi/src/feedback/FeedbackRepository
What differs between UK and USfrontend/src/region/regions.tsuseRegion()
Every number on a claim screenfrontend/src/claimpilot/claimView.tsderiveClaimView(claim)

Stateless where it can be. The Worker keeps no transcript and no session. Prompt Lab edits live in the browser and travel with each request. A turn id is minted and forgotten. The only things the Worker writes are claims (by the CLI) and feedback votes.

The request pipeline​

Every /api/* request passes the same middleware in api/src/index.ts, in this order:

  1. cors (access.ts). Same-origin requests pass. A cross-origin request from an origin not in ALLOWED_ORIGINS gets a 403 before any work is done. Preflights from allowed origins get a 204 with the CORS headers.
  2. Services. The claims repository, AI provider and feedback repository are built from env for this request and set on c.var. A misconfigured store throws here, which onError turns into a 500 naming the problem.
  3. requireEmbedKey on /api/chat, /api/voice and /api/feedback only. Unset key means open.
  4. The route.
  5. onError. A ZodError becomes a 400; anything else a 500, with the message.
  6. notFound. An unknown /api/* path is a JSON 404. Anything else goes to the static assets, which fall back to index.html for client-side routes.

assertCatalogueMatches() runs when the module loads: the Worker refuses to start if the server's capability registry and the shared catalogue disagree.

Where to go next​

To understandRead
The document everything is built onThe Claim contract
What happens in one chat turnThe chat pipeline
How capabilities and tools are put togetherCapabilities and tools
The model port, Gemini, the gateway, the test modelAI providers
D1, Neon and "no store at all"Data stores
The Live audio relayVoice
The React sideFront end
Safety, access and data protectionSecurity

The stack​

LayerChoice
Front endVite 8, React 19, TypeScript, Tailwind v4, react-router, react-markdown, lucide icons
Back endCloudflare Worker running Hono; Workers Static Assets for the SPA
DataOne JSON document per claim; Cloudflare D1 (SQLite) by default, Neon Postgres as an alternative
ModelsGemini Flash Lite for chat and generation, Gemini Live for voice, through @google/genai, proxied by Cloudflare AI Gateway
Contractzod 4 schemas in packages/shared
TestsVitest 5 (Node and jsdom), Testing Library, Playwright