Inspiration

Moving abroad for the first time is the rare problem where the information exists and the order does not. A student landing in Berlin has forty browser tabs: visa rules, Anmeldung, a blocked account, a SIM, a transit card, a bank appointment. None of them say that the bank account waits on the address registration, which waits on a landlord's confirmation, which waits on the lease. So people do the steps in the wrong order, get refused, and start again — while someone back home texts "did you register yet?" from a different timezone.

We wanted a tool that a stranger could use unaided on their second day in a country, not a slide with a fictional user journey.

What it does

You answer a handful of plain questions: destination city and country, which passport you hold, why you are moving (study / work / exchange), how long you will stay, where your visa stands, your age band, whether you have somewhere to stay, and when you land.

You get one checklist in three phases — Before departure, First 24–72 hours, First week — where every step says what to do, why it matters, how long it takes, and what it unlocks. Steps are dependency-gated: the bank-account step is locked until residence registration is ticked, and a locked row names the step it is waiting for. Order is enforced, not suggested.

The answers change the plan, not just the copy. Under 18 adds guardian consent and school enrolment instead of university. A late-night landing moves the SIM and transit errands to the next morning. Booked housing removes the housing search and unlocks proof-of-address steps earlier. Answering "No visa needed" (visa-free or exempt entry) produces a plan with no visa application in it at all.

On a phone it is one continuous scroll: swipe a row right to tick it, tap the title for the why and the source, tap an emergency chip to call police or ambulance, and use the Directions link on errands that mean physically going somewhere — it opens your own maps app with "city hall, Berlin" searched, no map SDK and no API key.

Then Share: the plan and the ticked steps are encoded into the URL hash. No account, no sign-in, nothing uploaded — your notes and your own saved phone numbers never leave the device, so a read-only link shows progress and official numbers only.

How we built it

Front end: Vite, React 19, TypeScript, hand-written CSS — no UI framework. The setup wizard is a typed port of React Bits' Stepper; checklist rows use a small CSS + pointer-events swipe primitive. Country, flag, city and coordinate tables are generated at build time from world-countries, country-flag-icons and world-cities-json, so none of it is hand-typed.

Model: Groq openai/gpt-oss-120b, called with strict JSON Schema, so the shape is constrained at the token level rather than politely requested. If strict mode is rejected we fall back to json_object, then to gpt-oss-20b, then to a curated plan. A Cloudflare Worker holds the key as a secret and exposes exactly one endpoint, POST /api/plan: it accepts only a landing-plan form (2 KB body cap, foreign Origin refused, 30 requests per IP per hour in a KV namespace), so the public URL cannot be used as a general model proxy or drain the free tier.

Trust is mechanical, not asserted. Nothing checks a URL at runtime, so we check in two other places:

  1. Every curated source link is fetched by scripts/check-links.mjs at build time; a 404/410 fails the build instead of shipping, and the survivors are written into a generated module.
  2. Links the model guessed are reduced to their site root unless the checker has seen that exact URL answer. Placeholder hosts and bare IPs are dropped entirely, and the step renders a "Verify locally" chip.
  3. Dangling dependencies, self-references and cycles are repaired client-side, and every plan loaded from localStorage or a share link is re-sanitised, because an older plan may carry an older, weaker link.

Deploy: npm run build && wrangler deploy — static assets with SPA fallback, plus the single Worker route. dist/ stays a plain static bundle, so the same build runs on any static host in Template Mode.

Challenges we ran into

  • The fourth visa answer. Our first version asked "still applying / approved / already landed" — which forces anyone who does not need a visa to lie, and then hands them a plan telling them to apply for one. Adding the option took a minute. Making the model obey it took longer: it kept inserting a visa-application step regardless. We verified the fix against the deployed endpoint rather than the prompt — visa: "exempt" returns a 14-step plan with zero visa-application steps, opening with passport validity → accommodation → enrolment proof → bank account → SIM, then residence registration.
  • Guessed URLs. Models invent plausible deep paths, and a 404 is worse than no link. Reducing unverified links to their origin cost us the ability to deep-link everything, which turned out to be the honest trade.
  • No dead screens. A judge typing an unfamiliar city, a revoked key or a dead venue Wi-Fi must not produce an error page. Everything falls back to a curated plan with a small toast and a visible badge saying which mode produced it.
  • Defects we found in our own output and have not fixed yet. Live runs still occasionally repeat an errand (two SIM steps in one plan) and the model sometimes truncates a step title mid-word, because the schema sets no maxLength and the validator's 90-character cap never engages.

Accomplishments that we're proud of

  • The demo cannot dead-end. Three curated cities plus a universal plan, one schema, one renderer — so the same screen serves a model plan and a hand-written one, and the source-link policy applies to both.
  • A share link with no backend. Progress travels entirely in the URL hash; a parent opens it and sees "0 of 14 done" and the trip facts, with no install and no account. We tested it cold, in a fresh browser with storage cleared.
  • Sources that survive an audit. No fabricated paths: a step links a URL the checker has seen answer, the site root of an authority, or says "Verify locally".
  • Verified in production, not just in dev. Type-check, lint, build and a Wrangler dry-run all clean; then the deployed URL was exercised at 1440 px and 390 px, the form and the plan were checked on the live bundle, and the /api/plan contract was tested with a real request.

What we learned

  • A prompt instruction is not a constraint. "Never add a visa step for an exempt traveller" is a wish. The enforcement has to be in code — a schema, a normaliser, a validator — or it is a hope.
  • Fallbacks are a design surface, not an error path. Deciding what the app says when the model is unavailable shaped the product more than the happy path did.
  • Stating a limit builds more trust than hiding it. "Verify locally" reads as honesty, not as a gap, and it let us ship in countries we cannot hand-verify.
  • Privacy can be structural. With no server-side state, no promise depends on a privacy policy.

What's next for First 72

  • Guards for model output quality: reject near-duplicate steps inside a plan and enforce title length, so the two defects above stop appearing.
  • Let curated plans inherit the answers. Today a template still lists "Confirm your visa status" even when the traveller answered "No visa needed".
  • More curated cities, and per-country rule packs instead of a universal fallback.
  • Thai UI for the team's own audience, then the language of the destination — the field labels are already centralised.
  • Optional sync for people who want their plan on two devices, keeping today's no-account mode as the default rather than a fallback.

First 72 is a planning aid, not legal or immigration advice.

Built With

Share this project:

Updates

Submission history