Visaroy

codebase handbook · pre-beta review · may 2026

A Schengen visa preparation assistant. Chat-driven data collection → PDF form generation → submission pack. This handbook maps every module, seam, and decision for a senior frontend engineer reviewing agent-written code before beta.

4
Packages
21
Countries Supported
27
Test Files
15
API Routes
1 — Overview

What Visaroy Does

Helps applicants prepare Schengen visa applications so they're accepted at the VFS/BLS counter on the first attempt. It is not a visa approval predictor or submission agent.

Conversational AI
Chat collects trip data, not a form
PDF Generation
Fills official blank PDFs per-country
Confidence Score
Predicts counter acceptance (0-100)

Domain Vocabulary

Application — A single visa application: profile + trip + documents + pack.
Profile — Applicant's personal/passport info. Extracted from previous PDF or entered via chat.
Pack — Final bundle: cover page + cover letter + checklist + filled form + uploaded docs → single PDF.
Topic — A step in the chat flow. 8 topics in sequence from start → review.
Confidence Score — Counter acceptance likelihood (0-100). Blocking issues → score 0.
Country Catalog — Registry of 29 Schengen states with render strategy + portal preference.

Chat Topic Flow

start personal_info trip accommodation employment documents cover_letter review

The chat currently collects ~30 of ~60 harmonised Schengen fields. Missing fields render blank (acceptable for beta). Tier 2 defaults are wired.

2 — Architecture

Module Map

@visaroy/schemas

packages/schemas

All Zod schemas + TypeScript types. Country catalog lives here. 9 schema files, 3 with tests.

chat form pack profile validation consistency confidence extraction application
@visaroy/chat-core

packages/chat-core

LangGraph conversational engine. Handles chat turns, tool calling, field extraction, validation. Uses OpenRouter → Gemini 3.

runChatTurn assertions fallback validation
@visaroy/form-engine

packages/form-engine

PDF generation. Two strategies: flat overlay (calibrated rects) and AcroForm fill (native fields). 15 country modules.

renderer fill draw acroform calibration
@visaroy/rules

packages/rules

Pure deterministic business rules. Document validation, cross-doc consistency, confidence scoring. No framework deps.

validateDocuments runConsistencyChecks calculateConfidenceScore
apps/web

Next.js 15 + Supabase Auth + Tailwind v4

Thin API routes delegating to packages. Chat UI, doc upload, calibration tool at /internal/.

apps/cli

Developer tooling CLI

Form preview, form review, chat replay, red-team testing. Not user-facing.

Seams (Package Boundaries)

API routes@visaroy/rules

validate-documents, check-consistency, confidence-score routes are thin wrappers around pure rule functions.

API routes@visaroy/schemas

All request/response validation uses Zod schemas. No inline ad-hoc validation.

API routes@visaroy/form-engine

generate-form and generate-pack delegate to form-engine for PDF rendering.

chat-turn route@visaroy/chat-core

Thin wrapper around runChatTurn().

3 — Data Flow

Ctrl/Cmd + wheel to zoom · Drag to pan · Double-click to fit

Loading...
4 — Schemas Package

packages/schemas/src/ — The single source of truth for all API contracts. Every other package imports types from here.

FileKey ExportsTestsUsed By
chat.schema.tsChatTopic, TopicData, ChatMessage, CHAT_MESSAGE_MAX_LENGTHyeschat-core, web
form.schema.tsSchengenFormData request/response schemasform-engine, web
profile.schema.tsProfile, ExtractedProfile schemasyesweb, chat-core
extraction.schema.tsDocument extraction schemasyesweb
validation.schema.tsValidationResult, DocumentValidationrules, web
consistency.schema.tsConsistencyCheckResultrules, web
confidence.schema.tsConfidenceIssue, ConfidenceLevelrules, web
pack.schema.tsPack generation request/responseform-engine, web
application.schema.tsApplication state schemasweb
country-catalog.tsCOUNTRY_CATALOG, CountryCode, RenderStrategy, helperseverywhere
Review tip: Start here. Read chat.schema.ts to understand TopicData shape (what fields the chat collects per topic), then form.schema.ts for what goes into SchengenFormData (the ~60 harmonised fields). The gap between those two is intentional for beta.
5 — Chat Engine

LangGraph State Machine

Core Function
runChatTurn(input: RunChatTurnInput): Promise<RunChatTurnResult>

Takes topic, message, history, current fields → returns reply, extracted fields, isComplete flag, execution mode.

Input

  • topic — current ChatTopic
  • message — user's message
  • history — previous turns
  • currentFields — already-collected data
  • firstName — for personalization
  • signal — AbortSignal for cancellation

Output

  • reply — assistant message
  • extractedFields — data parsed from user input
  • isComplete — topic done?
  • executionMode — "deterministic" | "llm" | "fallback"
  • toolCalls — trace of LLM tool usage
  • fallbackReason — if mode is fallback

Execution Modes

deterministic

No LLM needed. Pattern-matched input (e.g. "yes", country names).

llm

Full LangGraph pipeline: prompt → LLM → structured output → tools.

fallback

LLM failed/timed out. Generic safe response via buildFallbackReply.

LangGraph Graph Shape

State = messages[] + structuredResponse + toolCalls[] + usedAccommodationLookup. Single tool available: accommodation_address_lookup (searches hotel names → structured address).

Model: Gemini 3 via OpenRouter. Temperature 0.2. Structured JSON output enforced via Zod schema.

Assertions (Quality Guards)

The assertions.ts module exports guards that validate LLM behavior. Used in tests and can be run in production.

assertOnlyCurrentTopicFieldsExtracted
LLM doesn't leak fields from other topics
assertRequiredFieldsBeforeCompletion
Can't mark topic done with missing required fields
assertSupportedCountryOnly
Blocks non-Schengen/online-only countries
assertNoOffTopicAdvance
LLM stays on current topic
assertNoCrashFallback
Detects crash-to-fallback patterns
assertAccommodationLookupOnlyInAccommodation
Tool only fires in right topic
6 — Form Engine

Two Rendering Strategies

Flat Overlay

Draws text/checkboxes onto blank PDFs using calibrated rectangle maps.

Process: Load blank PDF from R2 → look up country module (rect map) → fillFlatOverlay() draws onto pages.

Calibrated via: Internal tool at /internal/calibration. Drafts saved to data/calibration-drafts/{code}.json.

Countries:

AT BE BG CZ DE DK FR HR HU IT LU MT PT SI SK
AcroForm Fill

Fills native PDF form fields programmatically. No manual calibration needed.

Process: Load PDF → get PDFForm → map SchengenFormData keys to country-specific AcroForm field names → fill text/radio/checkbox.

Shared renderer: renderers-acroform.ts with per-country field name maps.

Countries:

ES 129 CH 108 LI 108 IS 86 NO 105 SE 85

Numbers = AcroForm field count per country.

SchengenFormData Model

The ~60-field harmonised data model in packages/form-engine/src/model.ts:

applicant
  • surname, firstNames, dateOfBirth
  • placeOfBirth, countryOfBirth
  • nationality, sex, civilStatus
  • nationalIdNumber
passport
  • type, number
  • issueDate, expiryDate
  • issuedBy
contact
  • address, email, phone
  • residencePermitType/Number
  • residencePermitValidUntil
employment
  • occupation, employerName
  • employerAddress, employerPhone
trip
  • mainDestination, firstEntry
  • arrivalDate, departureDate
  • purpose, entries
  • fingerprints, previous visa
euFamily (optional)
  • surname, firstNames
  • dateOfBirth, nationality
  • travelDocumentNumber
  • relationship

Key Files

FilePurposeReview Priority
renderer.tsUnified flat overlay renderer. renderFlatOverlayForm()high
renderers-acroform.tsShared AcroForm renderer + per-country field mapshigh
fill.tsfillFlatOverlay() — draws onto PDF pageshigh
draw.tsLow-level text/checkbox drawing primitivesmedium
model.tsSchengenFormData type definitionhigh
country-catalog.tsRe-exports from schemas (compat shim)low
resolve-country-strategy.tsReturns render strategy for a country codemedium
source-pdfs.tsLoads blank PDFs from R2medium
countries/*.ts15 flat-overlay country modules (rect maps)low
calibration/Internal calibration tooling (7 files)low
review.tsForm review logic (imports from deprecated renderer)medium
Legacy alert: current-pack-renderer.ts is deprecated — no countries route through it. Kept only for constants imported by review.ts. renderers-sweden-base.ts is also legacy — SE migrated to shared AcroForm renderer.
7 — Rules Package

Pure functions with no framework dependencies. 3 files, each with full test coverage.

Validation

Per-document checks: type identified, format correct, not expired, name matching, readable quality.

validation.ts + validation.test.ts

Consistency

Cross-document checks: flight dates vs form, insurance dates vs trip, hotel country vs entry, salary vs bank.

consistency.ts + consistency.test.ts

Helpers: parseDate, namesMatch, isDateWithinTolerance

Confidence

Scoring from 100 down. Blocking = 0, High deduction = -25, Medium = -15, Low = -5. Consistency: -10 per fail, -5 per warn.

confidence.ts + confidence.test.ts

Confidence Scoring Rules (detailed)
CategoryConditionEffect
BLOCKINGInsurance rejected, Flight rejected, Passport <3mo expiry, BRP rejected/expiredscore → 0
HIGHBank statements rejected/missing, Employment letter rejected/not addressed to consulate−25
MEDIUMAccommodation warning (no guest name), missing passport/BRP copy−15
LOWAny other document warning−5
CONSISTENCYPer failed check / per warning−10 / −5
8 — Web App

API Routes

RouteMethodDelegates ToRate Limit
/api/chat-turnPOSTchat-core → runChatTurn30/min
/api/extract-profilePOSTAnthropic LLM (PDF extraction)5/min
/api/extract-documentPOSTLLM doc extraction60/min
/api/correct-extractionPOSTManual extraction correction60/min
/api/validate-documentsPOSTrules → validateDocuments60/min
/api/check-consistencyPOSTrules → runConsistencyChecks60/min
/api/confidence-scorePOSTrules → calculateConfidenceScore60/min
/api/generate-formPOSTform-engine5/min
/api/generate-packPOSTform-engine (full pack)5/min
/api/generate-cover-letterPOSTLLM10/min
/api/applicationsGET/POSTSupabase60/min
/api/profilesGET/POSTSupabase60/min
/api/healthGET60/min
/api/health/authGETSupabase auth check60/min
/api/health/dbGETSupabase DB check60/min

Middleware

In-memory rate limiter in src/middleware.ts. Buckets keyed by JWT user ID (when present) or IP fallback.

  • Active only in production (or when RATE_LIMIT_FORCE=1)
  • Stale bucket cleanup every 5 minutes
  • Per-instance only — needs Redis/KV before handling multi-instance hostile traffic

Frontend Components

Apply Page

app/apply/page.tsx — Main user flow. useReducer-based state machine managing topics, profile, trip, accommodation, employment, documents, cover letter.

Supporting files: _reducer.ts, _types.ts, _utils.tsx, _greetings.ts, _icons.tsx, _file-upload.tsx, _checkpoint-summary.tsx, topic-mapping.ts

Chat Components

components/chat/ — ApplyChat, ChatBubble, plus document-specific components: AttachSlot, DocCard, ExtractionEditor, ExtractionSummary, CorrectionInput.

Calibration Tool

app/internal/calibration/ — Developer-only tool for calibrating flat overlay field positions. Canvas rendering, inspector, sidebar, preview panel.

UI Primitives

shadcn/ui components: Button, Card, Input, Label, Progress. In components/ui/.

9 — Database

Supabase Schema

Ctrl/Cmd + wheel to zoom · Drag to pan · Double-click to fit

Loading...

RLS Policies

  • profiles: SELECT/INSERT/UPDATE restricted to auth.uid() = user_id
  • applications: SELECT/INSERT/UPDATE restricted to auth.uid() = user_id
  • documents: SELECT restricted via join to application's user_id
Privacy model: No raw document storage — uploads processed in memory, only extracted JSON persisted. Generated PDFs in R2 with 15-min signed URLs. GDPR delete-all supported.
10 — Country Support Matrix
CountryCodeStrategyPDF SupportedPortal Preferred
AustriaATflat_overlayyes
BelgiumBEflat_overlayyes
BulgariaBGflat_overlayyes
CroatiaHRflat_overlayyes
Czech RepublicCZflat_overlayyes
DenmarkDKflat_overlayyes
FranceFRflat_overlayyes
GermanyDEflat_overlayyes
HungaryHUflat_overlayyes
IcelandISacroformyes
ItalyITflat_overlayyes
LiechtensteinLIacroformyes
LuxembourgLUflat_overlayyes
MaltaMTflat_overlayyes
NorwayNOacroformyes
PortugalPTflat_overlayyes
SlovakiaSKflat_overlayyes
SloveniaSIflat_overlayyes
SpainESacroformyes
SwedenSEacroformyes
SwitzerlandCHacroformyes
EstoniaEEonline_onlyportal
FinlandFIonline_onlyportal
GreeceGRonline_onlyportal
LatviaLVonline_onlyportal
LithuaniaLTonline_onlyportal
NetherlandsNLonline_onlyportal
PolandPLonline_onlyportal
RomaniaROonline_onlyportal
11 — Tests & CI

Test Distribution

PackageTest FilesKey Tests
packages/schemas3profile, chat, extraction schema validation
packages/chat-core5helpers, validation, assertions, chat turn (unit + agent-level)
packages/form-engine5draw, AcroForm renderer, rects, signing place, source PDFs
packages/rules3validation, consistency, confidence (full coverage)
apps/web5middleware, chat helpers, doc extraction/retry/completion
apps/cli5args, replay fixture, replay, topic-state conversion, workspace
e2e/9 specsapply flow, profile extraction, form smoke, auth, health, API

CI Pipeline

.github/workflows/ci.yml — Runs on push to main + PRs:

pnpm install tsc --noEmit vitest run playwright (API) pnpm audit pnpm build

Separate job: gitleaks secret scanning (full history).

Run commands:
pnpm exec tsc --noEmit && pnpm build — verify before shipping
pnpm test:unit — unit tests with coverage
pnpm test:form — form-engine specific (separate vitest config)
pnpm test:e2e — Playwright E2E
pnpm knip — dead code detection
12 — Review Guide

Recommended Review Order

1
packages/schemas — Read types first

Start with chat.schema.ts (TopicData shape) and form.schema.ts. Then country-catalog.ts for the full country model. This sets the vocabulary for everything else.

2
packages/rules — Pure business logic

3 files, fully tested, no deps. Verify scoring logic in confidence.ts, consistency checks in consistency.ts. Look for edge cases in date parsing.

3
packages/form-engine — PDF rendering core

Focus on model.tsrenderer.tsrenderers-acroform.tsfill.ts. Verify page-4 is never touched. Check AcroForm checkbox hack in makeFiller.

4
packages/chat-core — LLM integration

Review run-chat-turn.ts for the LangGraph setup, prompt engineering in helpers.ts, and the assertion guards. Check fallback behavior and abort signal handling.

5
apps/web — API routes + UI

Verify routes are thin wrappers. Check middleware rate limiting. Review the apply page's useReducer state machine in _reducer.ts. Verify auth patterns in API routes.

6
tests + e2e — Coverage & confidence

Run pnpm test:unit with coverage. Check E2E specs cover the happy path. Run pnpm knip for dead code.

Things to Watch For

Security
  • Rate limiter is in-memory, per-instance only
  • RLS policies on documents use a join — verify no bypass
  • Signed URLs for R2 (15-min TTL) — verify enforcement
  • CHAT_MESSAGE_MAX_LENGTH enforced?
Agent Code Smells
  • Legacy files kept "for constants" — are they still needed?
  • Type casts at seams — verify they're safe
  • AcroForm checkbox hack (bypassing pdf-lib) — fragile
  • Country catalog duplicated (schemas + form-engine re-export)
LLM Reliability
  • Fallback behavior when LLM fails — is it graceful?
  • Assertion guards — are they run in production or tests only?
  • AbortSignal handling — does it clean up properly?
  • Accommodation lookup tool — injection surface?
Data Integrity
  • 30 collected vs 60 rendered fields — default handling
  • Date parsing in rules — multiple formats handled?
  • Non-Latin field detection in chat validation
  • Page 4 never touched — verify in all code paths
Key decision: NL (Netherlands) is explicitly blocked from PDF generation — online-only with a contaminated asset. Verify the block is enforced at both the catalog level and the form-engine level.