Most Chrome extensions that need data fall into one of two patterns: they call an external API, or they store a small amount of user-specific data locally. EntryCheck does neither. It bundles a static dataset of visa requirements for 190+ passport/destination combinations directly into the extension and resolves every lookup client-side with zero network requests.
This tradeoff — large bundle, instant lookups, no API dependency — turns out to be the right call for travel data. Here's why, and how it works.
Why Static Over API
Visa requirements don't change often. A country might update its visa-on-arrival list two or three times a year. An API would add latency, require authentication, and create a failure mode (network unavailable, API down, rate-limited) in a context where the user is typically trying to quickly check something before booking a flight.
More practically: there's no reliable free public API for visa requirements. The data sources are government websites and reference databases. Scraping or licensing these for a real-time API isn't worth it for a tool whose value is speed and simplicity.
A local JSON file loaded at extension startup sidesteps all of this.
Data Structure
The core dataset is a JSON object keyed by two-letter ISO passport code, then by two-letter destination code:
type VisaStatus =
| 'visa_free'
| 'visa_on_arrival'
| 'e_visa'
| 'visa_required'
| 'not_admitted';
interface EntryRequirement {
status: VisaStatus;
maxStay?: number; // days, undefined if no limit
notes?: string;
}
type VisaMatrix = Record<string, Record<string, EntryRequirement>>;
A lookup is just two array accesses:
function lookup(matrix: VisaMatrix, passport: string, destination: string): EntryRequirement | null {
return matrix[passport]?.[destination] ?? null;
}
The matrix itself compresses well: visa_free and visa_required cover the majority of combinations, so the JSON has a lot of repeated structure. Gzipped, the full dataset is under 30KB.
Bundling with WXT
The matrix lives in public/visa-matrix.json. WXT (the extension framework) copies the public/ directory to the output root verbatim. The background service worker loads it once on install and caches the result:
let cachedMatrix: VisaMatrix | null = null;
async function getMatrix(): Promise<VisaMatrix> {
if (cachedMatrix) return cachedMatrix;
const url = chrome.runtime.getURL('visa-matrix.json');
const resp = await fetch(url);
cachedMatrix = await resp.json();
return cachedMatrix;
}
chrome.runtime.getURL converts the relative path to the extension's internal chrome-extension:// URL. This is the standard pattern for accessing bundled assets from a service worker — it works in MV3 without any special permissions.
Content Script Injection on Google Flights
The lookup popup works fine on its own, but the more useful feature is automatic injection on Google Flights. When a user searches for a flight and the page shows a destination, EntryCheck's content script detects the destination, looks up the requirements for the user's saved passport, and injects a badge next to the search results.
The content script reads the current destination from the URL parameters and page DOM, calls the background for a lookup via chrome.runtime.sendMessage, and renders a small badge component inline.
chrome.runtime.sendMessage(
{ type: 'VISA_LOOKUP', passport: savedPassport, destination: detected },
(response: EntryRequirement | null) => {
if (response) renderBadge(response);
}
);
The background receives this, calls getMatrix(), and returns the result. Because the matrix is in memory after the first load, the response is synchronous from the content script's perspective.
The Maintenance Problem
Static data has one obvious downside: it goes stale. My current approach is to update the JSON file with each extension version bump and push it as a normal CWS update. This is manual but tractable — visa requirements change infrequently enough that quarterly checks cover 95% of changes.
For something that updates more frequently (exchange rates, business hours), this model breaks down and an API makes more sense. Visa requirements are the rare case where static actually wins.
🔗 EntryCheck on Chrome Web Store: Install
Top comments (0)