Add a Visa Sponsor Provider
What it is
This guide explains how to add a new country's visa sponsor register that is auto-discovered by the orchestrator at startup.
Each provider is a directory under visa-sponsor-providers/ containing a manifest.ts file. The manifest owns only what is country-specific: fetching and parsing the upstream register. Storage, scheduling, caching, and search are handled by the shared service layer.
Provider ids must be registered in shared/src/visa-sponsor-providers/index.ts to be accepted at runtime.
Why it exists
Without a manifest contract, adding a new country's register required touching multiple orchestrator files.
With the provider system, contributors only need to:
- Add a manifest in
visa-sponsor-providers/<id>/. - Register the new id in the shared catalog.
The service layer handles everything else.
How to use it
- Create a directory under
visa-sponsor-providers/<id>/where<id>is a short lowercase slug (e.g.au,ca). - Add a
manifest.tsin that directory (orsrc/manifest.ts). - Export a manifest that satisfies
VisaSponsorProviderManifest:id— matches the directory name and the catalog entrydisplayName— human-readable country namecountryKey— lowercase country string compatible withnormalizeCountryKey()(e.g."australia")scheduledUpdateHour(optional) — UTC hour for the daily refresh; defaults to2fetchSponsors()— fetches the upstream source and returnsVisaSponsor[]; throws on failure
- Add the new id to
shared/src/visa-sponsor-providers/index.ts:- append to
VISA_SPONSOR_PROVIDER_IDS - add an entry in
VISA_SPONSOR_PROVIDER_METADATA
- append to
- Start the server and confirm the startup log reports the provider in the registry.
- Run the full CI checks.
Example manifest:
import type {
VisaSponsor,
VisaSponsorProviderManifest,
} from "../../shared/src/types/visa-sponsors";
export const manifest: VisaSponsorProviderManifest = {
id: "au",
displayName: "Australia",
countryKey: "australia",
scheduledUpdateHour: 3,
async fetchSponsors(): Promise<VisaSponsor[]> {
// Fetch and parse the upstream register here.
// Return an array of VisaSponsor objects.
// Throw on failure — the service layer handles error state.
return [];
},
};
export default manifest;
Example catalog update in shared/src/visa-sponsor-providers/index.ts:
export const VISA_SPONSOR_PROVIDER_IDS = ["uk", "au"] as const;
export const VISA_SPONSOR_PROVIDER_METADATA = {
uk: { label: "United Kingdom", countryKey: "united kingdom" },
au: { label: "Australia", countryKey: "australia" },
};
Common problems
Provider not registered at startup
- Check the file path: valid locations are
visa-sponsor-providers/<id>/manifest.tsorvisa-sponsor-providers/<id>/src/manifest.ts. - Ensure the file exports
defaultor a namedmanifest. - Check startup logs for registry warnings such as skipped invalid manifests, duplicate ids, duplicate country keys, or ids missing from the shared catalog.
Provider id rejected at runtime
- The id must be in
VISA_SPONSOR_PROVIDER_IDSinshared/src/visa-sponsor-providers/index.ts. - Duplicate ids or duplicate
countryKeyvalues are skipped with a warning.
Provider loads but returns no sponsors
- Verify
fetchSponsors()returns a non-empty array and does not silently swallow errors. - Check
GET /api/visa-sponsors/statusfor the provider's error field. - Trigger a manual refresh with
POST /api/visa-sponsors/update/<id>and watch server logs.
countryKey does not match job locations
- The
countryKeymust produce the same output asnormalizeCountryKey()when called on job location strings. - Use lowercase, no diacritics, matching the canonical country name used in job data.