For Developers & Partners
Retrieval-Augmented Generation,
built for the highest-stakes
hallucination surface we have.
A wrong visa answer can mean a denied boarding or an overstay fine. VisaPath doesn't let the model answer from parametric memory — every response is generated from retrieved, sourced, dated rule text, with citations attached. This page is the technical breakdown for engineers, partners, and anyone evaluating the architecture.
Cloudflare Workers Vectorize (1536-dim) D1 R2 OpenRouter Embeddings DeepSeek V3
Why RAG, not just a bigger prompt
A generic LLM answering visa questions is guessing from training data that may be stale, wrong, or simply absent for a given passport/destination pair — with no way for the user to check. RAG forces a structural fix: retrieve the actual current rule text first, then generate the answer strictly from what was retrieved, and attach citations so the claim is falsifiable.
01
Ingest
Weekly cron fetches each country's official source, checks robots.txt first, archives the raw HTML to R2.
02
Chunk
Strip nav/footer/cookie boilerplate, split into ~400-token chunks on paragraph boundaries.
03
Embed
Each chunk embedded via OpenRouter (openai/text-embedding-3-small, 1536-dim), diffed against existing hashes to skip unchanged text.
04
Index
Vectors upserted to Vectorize with country_code metadata; chunk text + source + date stored in D1.
05
Query embed
User's passport/destination/purpose is embedded with the same model at request time.
06
Retrieve
Vectorize.query() with a country_code metadata filter, topK=6, returns the closest matching chunks.
07
Ground
Retrieved chunks + source + date are injected into the prompt; the model is instructed to answer only from them.
08
Cite
DeepSeek V3 generates the answer plus a structured citations array, rendered directly in the UI.
Stack
Everything runs on Cloudflare's edge platform — no separate vector DB vendor, no separate application server.
Workers
Runs ingestion (cron-triggered) and query-time retrieval + generation, same codebase.
Vectorize
1536-dim cosine index with a metadata index on country_code for filtered ANN search.
D1
rule_chunks (text + source + tier + date) and ingestion_log (per-run audit trail) tables.
R2
Raw HTML archive per source per date — re-chunk without re-scraping, audit trail for what was actually fetched.
OpenRouter
Embeddings (openai/text-embedding-3-small) and generation (deepseek/deepseek-chat) through one API, one existing key.
Cron Triggers
Weekly re-ingestion sweep; diffing means only changed chunks get re-embedded.
robots.txt pre-check
Every source is checked before first fetch; disallowed sources are skipped, not force-scraped.
Graceful fallback
No grounded chunks for a country → model still answers, but citations: [] and the UI marks it "not source-verified" instead of faking a source.
Two-tier trust model
The corpus is hybrid by design: official government sources for the highest-traffic destinations, with an honest signal when a country hasn't cleared that bar yet. Every answer's tier and last-verified date ships to the frontend — it's not just a backend implementation detail.
Official
Chunk text fetched directly from a government/embassy domain, ToS-checked via robots.txt before ingestion. Shown with a green badge and verified-as-of date.
Ungrounded (current fallback)
When no cleared official-tier chunk exists yet for a country, the model still answers from parametric knowledge, but the UI explicitly says "not source-verified" instead of presenting it as sourced.
Current coverage — honestly reported
This is a live number, not a marketing target. Real government sites are inconsistent to scrape: some block automated fetches outright, others render their actual policy text behind JavaScript rather than serving it as static HTML. Coverage grows as sources are individually curated and cleared, not by force-scraping around bot defenses.
10
countries with official-tier grounding live
38
retrieved chunks indexed
1536
embedding dimensions
7d
re-ingestion cadence
Every destination outside the current 10 falls back to ungrounded, clearly labeled in the UI — never silently presented as sourced.
What a grounded response looks like
Simplified excerpt of an actual /visa-check response for a country with official-tier chunks indexed.
{ "visa_required": false, "summary": "United Kingdom passport holders do not require a visa for tourism visits to South Africa for stays up to 90 days.", "citations": [ { "source_url": "https://www.dha.gov.za/index.php/immigration-services/types-of-visas", "source_tier": "official", "last_verified_date": "2026-08-02" } ], "grounded": true // false → citations: [] → UI shows "not source-verified" }
Pipeline 2 — Document Readiness
Vision-LLM extraction, not a form users fill in by hand.
The Document Readiness checker doesn't ask travellers to type in their passport number or expiry date — they photograph the document and a vision-capable model reads it. Classification, image-quality assessment, and structured field extraction happen in one call, then a separate rules engine checks the extracted fields against the trip dates and against each other.
01
Upload
User photographs a passport, visa, health cert, or ID (JPEG/PNG/WEBP/HEIC) and uploads it for a trip.
02
Store raw
Original image bytes written to R2 before any processing, so the source scan is preserved for audit or re-extraction.
03
Classify + OCR
Image sent as a base64 data URL to a vision-LLM call via OpenRouter: document type, image quality, and every legible field extracted in one pass.
04
Persist
document_type, quality_status, model_used, and per-field values stored in D1 against the traveler's document record.
05
Cross-check
Rules engine compares extracted fields across documents and against trip dates: visa required but missing, visa validity window vs. travel dates, likely health-cert requirement.
06
Quality flags
Any scan the model marked blurry, glared, cropped, low-res, or unreadable is surfaced as a warning instead of silently extracting a guessed value.
07
Score
Issues roll up into a readiness score: critical issues (e.g. missing visa) cap the score low, warnings (e.g. blurry scan) cap it moderately.
Model choice
Primary: google/gemini-2.5-flash, called via OpenRouter — chosen for OCR accuracy-per-dollar on dense printed text like passport MRZ lines and visa stickers. Fallback: google/gemini-2.5-flash-lite if the primary call fails. The model is explicitly instructed not to guess or fabricate a field it can't actually read from the image — an unreadable field is left blank for the user to fill in manually, not silently invented.
OpenRouter
Single API for the vision-LLM call: image + prompt in, document_type + quality_status + extracted fields out as structured JSON.
Gemini 2.5 Flash
Primary OCR/classification model — best accuracy-per-dollar on dense printed document text.
Gemini 2.5 Flash-Lite
Automatic fallback if the primary model call errors, so an extraction failure doesn't block the user.
R2
Stores the original uploaded document image, separate from the raw-source archive used by the RAG pipeline.
D1
documents and extracted-fields tables, plus the readiness issues generated per trip.
No fabricated fields
Prompt instructs the model to leave a field empty rather than guess — a missed field is safer than a wrong one.
See it live, or talk architecture
Run a real check against a grounded country, or reach out if you're evaluating this for a partnership, integration, or data-source collaboration.
Run a Live Check → See the Traveler-Facing Version