A Next.js app that finds remote developer jobs every day, filters them by country, scores them against your three CVs, logs everything in a Google Sheet, and gives you a dashboard to approve and apply from. Built for Daniel Chimezie. Runs on your PC for testing and on a RackNerd VPS for the daily run.
cron (7am daily) -> POST /api/run
|
1. lib/fetchers/ pull jobs from 9 sources
2. lib/filters.js keep eligible countries, drop duplicates
3. lib/scorer.js score each job, pick the right CV
4. lib/sheets.js write the best new matches to the sheet
5. lib/emailApply.js find application emails in descriptions
6. approvals send email applications, queue the rest
7. lib/mailer.js follow-up reminders and one daily digest
Dashboard (app/page.js) reads and updates the same sheet.
- Open the dashboard. Jobs are sorted by match score.
- Approve a job you want. For an email job, your CV is sent at once (after a confirmation that shows the address). For a web-form job, it moves to Needs Manual Apply.
- Open Apply kit on a job to copy your details and saved answers and to download the right CV. Open the application, fill it in, submit.
- Click Mark Applied. A follow-up reminder is set for 7 days later.
Jobs scoring at or above AUTO_APPROVE_MIN_SCORE skip step 2. Only email jobs
are ever submitted for you; web forms are always submitted by you.
| File | What it does |
|---|---|
middleware.js |
Password login for the dashboard and API (HTTP Basic Auth). |
package.json |
Dependencies: next, react, googleapis, nodemailer, rss-parser. |
.env.local.example |
Template for every setting. Copy to .env.local. |
| File | What it does |
|---|---|
countries.json |
Countries where mid-level developer pay clears $28K a year. |
companies.json |
Company names checked on Greenhouse, Lever, Ashby, BambooHR. |
cvMapping.json |
Title and skill keywords that choose between your 3 CVs. |
applyKit.json |
Your details and saved answers for the Apply kit. Replace the EDIT ME lines. |
| File | What it does |
|---|---|
page.js |
The dashboard: stats, tabs, Approve, Apply kit, LinkedIn links. |
api/run/route.js |
The daily pipeline. Protected by CRON_SECRET. |
api/approve/route.js |
Approve and status changes. Sends email applications. |
api/jobs/route.js |
Reads the sheet for the dashboard. |
api/kit/route.js |
Serves applyKit.json to the dashboard. |
api/resume/route.js |
Downloads one of the 3 CVs. |
api/clear/route.js |
Clear all (only works when ALLOW_CLEAR=true). |
api/format/route.js |
One-time sheet styling. Run it once. |
| File | What it does |
|---|---|
sheets.js |
All Google Sheet reads and writes. |
filters.js |
Country filter, remote check, duplicate removal. |
scorer.js |
Match score and CV choice. Word lists at the top are editable. |
emailApply.js |
Finds an application email in a job description. |
linkedin.js |
Builds LinkedIn search links per company. |
mailer.js |
Daily digest and follow-up emails. |
autoApply.js |
Sends one application email with the right CV attached. |
loadJson.js |
Loads the config files. |
fetchers/ |
One file per job source, plus index.js that runs them all. |
| File | What it does |
|---|---|
check-companies.mjs |
Tests every company in companies.json, removes dead ones, adds new ones. |
resumes/ holds your 3 CV PDFs. cron/ holds the scripts that call /api/run
daily. logs/ holds the daily run log.
| Setting | What it does |
|---|---|
GOOGLE_SERVICE_ACCOUNT_KEY_PATH, GOOGLE_SPREADSHEET_ID, GOOGLE_SHEET_TAB |
Connect to your sheet. |
SMTP_EMAIL, SMTP_APP_PASSWORD, SMTP_SERVER, SMTP_PORT, DIGEST_RECIPIENT_EMAIL |
Sending email. Gmail needs an App Password. |
MINIMUM_MATCH_SCORE |
Jobs below this never reach the sheet. Default 55. |
AUTO_APPROVE_MIN_SCORE |
Jobs at or above this skip the Approve step. Default 80. |
REQUIRE_REMOTE |
true drops career-page jobs that don't look remote. Default false. |
AUTO_SEND_EMAILS |
true sends strong email-apply matches without your click. Default false. |
MAX_EMAILS_PER_RUN |
Cap on automatic emails per run. Default 10. |
CRON_SECRET |
Protects /api/run. |
DASHBOARD_USER, DASHBOARD_PASSWORD |
Switch on the dashboard login. |
ALLOW_CLEAR |
Enables Clear all. Keep false on the VPS. |
pnpm install
cp .env.local.example .env.local # then fill it in
pnpm dev # open http://localhost:3000Google setup: create a Google Cloud project, enable the Sheets and Drive APIs,
make a service account, download its JSON key into credentials/, create the
sheet, and share it with the service account's email as Editor. Then run once:
curl -X POST http://localhost:3000/api/format
curl -X POST http://localhost:3000/api/run -H "x-cron-secret: YOUR_CRON_SECRET"(If you set a dashboard login, add -u USER:PASSWORD to the format call.)
Keep the project in a plain folder such as C:\Users\YOU\job-pipeline. Do not
put it in OneDrive: syncing breaks node_modules.
node scripts/check-companies.mjs # report
node scripts/check-companies.mjs --fix # remove dead names
node scripts/check-companies.mjs --add=greenhouse:dropbox,ashby:retoolRestart pnpm dev after changing the list, because the fetchers read it at startup.
- Install Node 20 and pnpm, upload the project, run
pnpm installandpnpm build. - Fill in
.env.localwith Linux paths. SetALLOW_CLEAR=falseand set a dashboard login. - Start it with PM2:
pm2 start "pnpm start" --name job-pipeline, thenpm2 saveandpm2 startup. - Fix Windows line endings in the scripts, then install the daily job:
sed -i 's/\r$//' cron/*.sh && chmod +x cron/*.sh && ./cron/setup_cron.sh. - Block everything except SSH with the firewall and reach the dashboard
through an SSH tunnel:
ssh -L 3000:localhost:3000 user@your-vps-ip.
.env.localandcredentials/must never be committed. Check withgit check-ignore -v .env.local credentials/keys.json.- The dashboard login uses Basic Auth, which is readable on plain
http://. Use the SSH tunnel or HTTPS. config/applyKit.jsonholds your saved answers. Do not commit a real salary number to a public repo.- Rotate the service account key, app password, and
CRON_SECRETif they are ever pasted somewhere public.
- Only email applications are sent for you. Web forms (most jobs) are submitted by you; the Apply kit makes that faster.
- Scoring is a keyword-based first pass. Read the posting before applying.
- Country matching is text based and can miss unusual wording. Extend
SYNONYMSinlib/filters.jswhen it does. - Salaries are not read from postings, so the sheet shows "Not listed".
- Email detection can pick the wrong address. Check the address on the dashboard card before approving.
- LinkedIn and Indeed are not sources: neither offers individual API access.
- Watch a few real runs and tune the word lists in
lib/scorer.jsandMINIMUM_MATCH_SCORE. - Run
check-companies.mjsevery month and add companies from your target list. - Deploy to the VPS (section above).
- Build the LinkedIn alert-email reader.
- Build an assisted-apply script if submitting forms takes too long.
- More sources: NaukriGulf for UAE and Qatar, StepStone for Germany.
- Revisit
config/countries.jsonevery few months. Salaries change.