Study-abroad outreach, automated end to end.
You have a list of professors, programmes, and scholarships. Each one needs to be researched, judged for fit, and written to individually. Doing that well takes about an hour per target, so most people either send forty identical emails or give up after five.
Flow does the process, not the decision. It researches each target, tells you honestly whether the fit is real, and drafts a message you edit and send yourself.
It is built for applicants anywhere, in any field. Nothing in it assumes a country, a discipline, a grading system, or a degree structure — and the prompts that drive it are yours to rewrite.
Your profile ──┐
├──► Research ──► Fit analysis ──► Draft ──► Review ──► You edit and send
Your targets ──┘ (web) (honest score) (any lang) (verify)
- Research every target from public sources, with a hard rule against inventing anything.
- Score the fit honestly. A "weak" verdict saves you from a message that costs you a reputation.
- Draft in the language and tone the target expects, using only facts the earlier steps verified.
- Review the draft, strip filler and unverifiable claims, and list what you still need to check yourself.
Every message comes with the factual claims it made about the target, so you can verify before your name is attached to them.
git clone https://github.com/youssefx99/study-abroad.git
cd study-abroad
npm install
cp .env.example .env # optional — see "Running without a key" below
npm run seed # loads a worked example spanning three countries
npm run devOpen http://localhost:3000.
That starts eight backend services and the web app in one terminal. Stop everything with Ctrl+C.
Flow works fully with no API key. Every stage returns representative, correctly structured output instead of calling a model, and says so on every screen. You can click through the entire product, run pipelines, and read results before spending anything.
To run for real, add your own OpenAI key under Settings → API key.
There is no authentication, by design — this is a single-user tool holding your own data. What enforces that is the network binding:
- Every service binds to 127.0.0.1 only. Nothing is reachable from another machine.
- CORS is restricted to the web app's origin, so a malicious tab in your browser cannot read your data from
localhost.
Both are configurable (FLOW_BIND_HOST, FLOW_WEB_ORIGIN) for container deployments, where isolation comes from published ports instead. Do not set FLOW_BIND_HOST=0.0.0.0 on a shared network — anyone who can reach the port would have full read and write access to your profiles, CVs, and model budget.
On vercel.com/new, import this repository and change one setting:
| Setting | Value |
|---|---|
| Root Directory | apps/web ← the only thing you must change |
| Framework Preset | Next.js (detected) |
| Build / Install / Output | leave blank (detected) |
| Environment Variables | none |
Then press Deploy. That is the whole deployment: no database, no environment variables, and no key of yours.
Root Directory matters because this is an npm-workspace monorepo. Pointing it at apps/web is what makes Vercel detect Next.js properly and turn /api/execute into a function; it still installs from the repo root, so the @flow/* packages resolve.
You do not need to set NEXT_PUBLIC_FLOW_MODE. Vercel sets VERCEL=1 during its build, and next.config.mjs switches to cloud mode on that automatically. Set NEXT_PUBLIC_FLOW_MODE=local only if you are also hosting the eight services somewhere and want the app to talk to them.
The CLI equivalent, if you prefer it:
npm i -g vercel
cd apps/web && vercel --prodCloud mode changes where things live:
Self-hosted (npm run dev) |
Hosted (Vercel) | |
|---|---|---|
| Data | JSON files in data/ |
Each visitor's own browser |
| Backend | Eight services behind a gateway | One route: /api/execute |
| API key | Yours, entered once | Each visitor's own, in their browser |
| Live progress | Server-sent events | The tab running the work |
This app never holds an OpenAI key. /api/execute does not read OPENAI_API_KEY — there is no fallback in the code, so there is none to remove or misconfigure later. A request without a key is refused even when the server environment has one.
Each visitor enters their own key under Settings → API key, or inline on the last step of the run wizard. It stays in their browser, is sent only on the calls they trigger, and is never written into a run record, a log, or an exported workspace. "Remember on this device" is off by default, so on a shared computer the key dies with the tab.
The consequence, stated plainly: every visitor pays for their own runs, and you can neither see nor use their key. That is what makes the link safe to share.
- Data does not follow people between browsers or devices. There are no accounts. Settings → System has an export button.
- The tab does the orchestrating, so closing it stops a run in progress. Finished targets are already saved, and the wizard says so before launch.
- Web-search research can outrun the function limit. Vercel allows 60s on Hobby and up to 300s with Fluid compute;
vercel.jsonasks for 300. If research times out, use a faster model for that stage or turn web search off on the prompt. - Only the most recent 40 runs are kept, since browser storage is finite.
Any host that runs Next.js works the same way — set NEXT_PUBLIC_FLOW_MODE=cloud at build time.
This is the part that matters, so it is worth being specific.
| What varies | How Flow handles it |
|---|---|
| Country | Nothing is US-centric. Grades are a value plus a free-text scale, so 4.42 / 5.0 GPA, First Class, 1.3 German, and 16/20 all fit. |
| Field | No prompt names a discipline. A law applicant in Nairobi and a physics applicant in Seoul use the same library. |
| Who you write to | Professors, degree programmes, admissions offices, scholarships, labs, and companies are one entity with one process. |
| Language | Drafts are written in any language, per run or per target. Tone presets map to regional convention. |
| Tests and qualifications | Test names are free text. IELTS, TestDaF, JLPT, HSK, and a national exam nobody on this repo has heard of all work. |
| What the system asks the model | Every prompt is editable, versioned, testable, and restorable in the UI. |
| The order of the steps | Pipelines are user-defined. Add, remove, reorder, disable, or mark steps optional. |
| Anything else | Applicants and targets both carry user-defined custom fields, and prompts can read them. |
If something still does not fit your situation, the fix is an edit in the Prompts screen, not a pull request.
| Screen | What it is for |
|---|---|
| Dashboard | Where everything stands, and one clear next action. |
| Applicants | The person applying. A weighted completeness score shows what would most improve your drafts, and a preview shows the exact text the model reads. |
| Targets | A filterable ledger. Add one at a time, or paste a JSON array or spreadsheet export — the importer previews rows and reports problems per row instead of rejecting the file. |
| Prompts | Read and rewrite what the system asks the model. Publish versions, roll back, and render against real records without spending a token. |
| Pipelines | The order the steps run in, and what each one feeds the next. |
| Runs | A five-step wizard, then a live console that streams progress as it happens. Drafts are editable in place. |
| Settings | API key, per-stage models, run defaults, theme, and service health. |
Eight services behind one gateway. See ARCHITECTURE.md for the reasoning.
| Service | Port | Owns |
|---|---|---|
gateway |
4000 | Single browser-facing origin, health aggregation, SSE relay |
profiles |
4001 | Applicants, completeness scoring, prompt blocks |
targets |
4002 | Targets, filtering facets, bulk import |
prompts |
4003 | Prompt library, versions, pipelines |
research |
4004 | Web research stage |
analysis |
4005 | Fit analysis and custom steps |
outreach |
4006 | Drafting and review |
orchestrator |
4007 | Runs, live streaming, settings, secrets |
Frontend: Next.js 16, React 19, Tailwind v4, Radix primitives. Backend: Node 20+, Express 5, Zod, plain-JSON storage behind a repository interface.
Storage is JSON files under data/. Nothing is uploaded anywhere except the model calls you trigger yourself. Swapping in Postgres means implementing five methods in packages/store.
npm run dev # everything, one terminal
npm run dev:services # backend only
npm run dev:web # frontend only
npm run build # production build of the web app
npm run seed # fill empty collections with the example
npm run reset # wipe and re-seed (keeps your API key)
npm run typecheck # tsc --noEmitPer-service ports and models are configurable in .env — see .env.example.
Prompts are records, not code. Open Prompts, pick one, and edit. Variables use {{double braces}}:
APPLICANT
{{applicant.block}}
TARGET
{{target.block}}
RESEARCH FINDINGS
{{steps.research}}
Write in {{options.language}}, in a {{options.tone}} tone, about {{options.wordCount}} words.
The Test tab renders your unsaved edits against real records and tells you which variables resolved to nothing — the cheapest possible way to catch a typo before it costs you forty emails.
Publishing creates a new version. The old one is kept, so a run that already happened stays explainable.
A tool that writes on your behalf can do real damage if it invents things. Flow is built so it cannot, quietly:
- Prompts are instructed to report what they could not verify, and to leave gaps visible rather than filling them.
- The fit analysis must produce at least one concern, even when the score is high.
- Drafts list every factual claim they made about the target, for you to check.
- Demo output says it is demo output, in every field, on every screen.
- Prompt variables that resolve to nothing raise a visible warning on the step, instead of silently producing a thinner message.
Read every draft before you send it. The system is a first draft, not a delegate.
MIT