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" }
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