Skip to content

Repository files navigation

job-tracker

A local-first personal resume & job application tracker. No database, no cloud, no auth — runs entirely on your machine.

License: MIT Node Next.js TypeScript PRs Welcome

Table of Contents

Screenshots

Drop UI screenshots into docs/screenshots/ using the filenames below and they will render here. See docs/screenshots/README.md for guidance.

Dashboard Add Application Resume Preview
Dashboard Add Application Resume preview

Features

  • Application tracking — company, role, source, status, applied date, job URL, notes
  • Resume management — upload a custom PDF per application; auto-falls back to a single starter resume
  • PDF preview — view resumes inline in an iframe modal or open in a new tab
  • Color-coded status badges — Applied (blue), Interview (amber), Offer (green), Rejected (red), Withdrawn (gray)
  • Inline status updates — change status directly in the table row without opening a modal
  • Search — filter by company or role (client-side, case-insensitive)
  • Status filter chips — multi-select filter by one or more statuses
  • Sortable columns — sort by date applied, company name, or status
  • Export — download all data as CSV (via papaparse) or JSON
  • Stats header — total count and per-status breakdown at a glance
  • Keyboard shortcut — Cmd/Ctrl+K to open the Add Application modal
  • Responsive layout — works on desktop and tablet

Tech Stack

Layer Choice
Framework Next.js 16 (App Router) + TypeScript
Styling Tailwind CSS v4
Storage Local JSON file + PDF files on disk
Validation Zod
Forms react-hook-form + @hookform/resolvers
Data fetching SWR
Notifications react-hot-toast
CSV export papaparse

Prerequisites

  • Node.js ≥ 20.0.0 (Next.js 16 requirement). An .nvmrc is included — nvm use will pick the right version.
  • npm ≥ 10.0.0 (ships with Node 20).

Platform Support

Platform Status
Windows 10 / 11 Supported
macOS 12+ Supported
Linux (any modern distro) Supported

The app uses Node's path and fs modules with cross-platform-safe joins. Resume files are stored on the local filesystem in data/resumes/.

Browser Support

Browser Status
Chrome / Edge (Chromium ≥ 100) Fully supported
Firefox ≥ 100 Fully supported
Safari ≥ 16 Fully supported

The inline PDF preview relies on the browser's built-in PDF viewer (via <iframe>). If your browser cannot render PDFs natively (e.g. some Linux Firefox builds without pdf.js), use the Open in new tab action instead.

Setup

git clone https://github.com/KrupeshVachhani/job-tracker.git
cd job-tracker
npm install
npm run dev

Open http://localhost:3000.

Production build

npm run build   # compiles the app to .next/
npm run start   # serves the production build on http://localhost:3000

npm run start is what you want once you stop actively developing — it is faster and uses less memory than npm run dev.

Environment variables

None required. The app is local-first and does not connect to any external service. An .env.example file is included to make this explicit; you do not need to copy it.

Optional — Starter Resume

Drop your default resume PDF at data/resumes/starter.pdf. Any application submitted without a custom PDF will use this file automatically. If it doesn't exist, the resume field will simply be blank.

Usage

A first-run walkthrough — every step happens in the browser at http://localhost:3000.

  1. Add an application. Click + Add Application in the top-right (or press Cmd/Ctrl+K). Fill in company, role, source, applied date, and status. Job URL and notes are optional.
  2. Attach a resume (optional). In the Add Application modal, click Upload PDF to attach a custom resume (max 10 MB). Skip this and the app uses data/resumes/starter.pdf if you placed one there; otherwise the resume field stays blank.
  3. Update status as things move. In the table row, click the status pill to change it inline — no modal needed. Use this as interviews are scheduled, offers come in, or applications get rejected.
  4. Filter and search. Type into the search box to filter by company or role. Click the colored status chips above the table to narrow by one or more statuses. Click a column header to sort.
  5. Preview a resume. Click the resume filename in any row to open it in an inline PDF viewer, or use the external-link icon to open it in a new tab.
  6. Export your data. Use Export CSV or Export JSON in the header to download everything. CSV opens cleanly in Excel / Google Sheets; JSON is the raw shape from the API.
  7. Delete an application. Use the row's delete action. The associated PDF is removed too — unless it's the shared starter.pdf.

Data & Privacy

This app is local-first by design:

  • Where your data lives. Application records are stored as JSON at data/applications.json. Resume PDFs live in data/resumes/. Both paths are relative to the project root.
  • What leaves your machine. Nothing. There is no telemetry, analytics, error reporting, or auto-update check. The app makes no outbound network requests of its own.
  • No accounts, no auth. The app assumes a single user on a trusted machine. Anyone with access to the running server or the data/ folder can read every application and resume.
  • Git ignores your data. .gitignore excludes data/applications.json and data/resumes/*.pdf, so cloning the repo never ships personal data. Be careful not to add them with git add -f.

Backup, Reset & Migration

Back up your data. Everything you care about lives under data/. To snapshot it:

# from the project root
tar -czf job-tracker-backup-$(date +%F).tar.gz data/
# Windows PowerShell:
Compress-Archive -Path data -DestinationPath "job-tracker-backup-$(Get-Date -Format yyyy-MM-dd).zip"

Store the archive somewhere off the project directory (cloud drive, external disk, encrypted vault). Schedule it as often as you'd commit work — weekly is plenty for most.

Reset to a clean state. Stop the dev server, then:

rm data/applications.json          # wipe all application records
rm -rf data/resumes/*.pdf          # wipe uploaded resumes (keeps the folder)

On Windows PowerShell:

Remove-Item data\applications.json
Remove-Item data\resumes\*.pdf

applications.json is re-created automatically on the next API call. Keep data/resumes/starter.pdf if you want the fallback resume to survive the reset.

Move to another machine. Copy the data/ folder. That's it — there's no database to dump or migration to run. On the new machine:

git clone https://github.com/KrupeshVachhani/job-tracker.git
cd job-tracker
# overwrite the empty data/ with your backup
cp -r /path/to/backup/data/. data/
npm install
npm run dev

Deployment

This app is not designed to be deployed publicly.

There is no authentication, no per-user isolation, and no rate limiting. If you put it on the open internet, anyone who finds the URL can list, read, create, and delete your applications and resumes.

Supported ways to run it:

  • Locally (recommended) — npm run dev or npm run build && npm run start.
  • On your own LAN, behind a firewall, on a machine only you can reach.
  • Inside a VPN / Tailscale / Cloudflare Tunnel with access control, if you want to reach it from a second device.

If you genuinely need a hosted multi-user version, you'll need to add auth, per-user data isolation, and durable storage (Postgres or similar) — none of which are in scope for this project.

Data Schema

All applications conform to this shape (see types/application.ts):

type ApplicationStatus =
  | 'Applied'
  | 'Interview'
  | 'Rejected'
  | 'Offer'
  | 'Withdrawn';

interface Application {
  id: string;              // server-assigned, immutable
  company: string;         // required, non-empty
  role: string;            // required, non-empty
  source: string;          // required, non-empty — e.g. "LinkedIn", "Referral"
  appliedDate: string;     // required, ISO-8601 date (YYYY-MM-DD)
  status: ApplicationStatus;
  resumeFile: string;      // filename inside data/resumes/ (server-resolved)
  isStarterResume: boolean;// true if falling back to starter.pdf
  jobUrl?: string;         // optional
  notes?: string;          // optional
  createdAt: string;       // ISO-8601 timestamp, server-assigned
  updatedAt: string;       // ISO-8601 timestamp, server-maintained
}

Fields marked required must be present and non-empty on POST. id, resumeFile, isStarterResume, createdAt, and updatedAt are managed by the server — do not send them on create.

API Reference

Base URL: http://localhost:3000

Method Endpoint Description
GET /api/applications List all applications
POST /api/applications Create new application
GET /api/applications/:id Get single application
PUT /api/applications/:id Update application (partial)
DELETE /api/applications/:id Delete application + its PDF
POST /api/resumes/upload Upload PDF (max 10 MB, multipart/form-data)
GET /api/resumes/:filename Stream PDF file inline

Examples

Create an application

curl -X POST http://localhost:3000/api/applications \
  -H "Content-Type: application/json" \
  -d '{
    "company": "Acme Corp",
    "role": "Senior Engineer",
    "source": "LinkedIn",
    "appliedDate": "2026-05-18",
    "status": "Applied",
    "jobUrl": "https://acme.example.com/careers/123",
    "notes": "Referred by Sam"
  }'

Response — 201 Created:

{
  "id": "f3a1...",
  "company": "Acme Corp",
  "role": "Senior Engineer",
  "source": "LinkedIn",
  "appliedDate": "2026-05-18",
  "status": "Applied",
  "resumeFile": "starter.pdf",
  "isStarterResume": true,
  "jobUrl": "https://acme.example.com/careers/123",
  "notes": "Referred by Sam",
  "createdAt": "2026-05-18T10:12:43.000Z",
  "updatedAt": "2026-05-18T10:12:43.000Z"
}

Update status

curl -X PUT http://localhost:3000/api/applications/<id> \
  -H "Content-Type: application/json" \
  -d '{ "status": "Interview" }'

Upload a resume PDF

curl -X POST http://localhost:3000/api/resumes/upload \
  -F "file=@/path/to/resume.pdf"

Response — 201 Created:

{ "filename": "8f2c1e0a-....pdf" }

Pass that filename as resumeFile when creating the application.

List all applications

curl http://localhost:3000/api/applications

Delete an application

curl -X DELETE http://localhost:3000/api/applications/<id>

Also removes the associated PDF unless it is the shared starter.pdf.

Error responses

Status When
400 Validation failed (missing/invalid fields, non-PDF upload, file > 10 MB, bad filename)
403 Resume filename resolves outside data/resumes/ (path-traversal guard)
404 Application or resume file not found

Validation errors include a Zod-flattened error body, e.g. { "error": { "fieldErrors": { "company": ["Required"] } } }.

Troubleshooting / FAQ

Port 3000 is already in use. Either stop the other process or run on a different port: npm run dev -- -p 3001 (and npm run start -- -p 3001 for the production build).

npm install fails with engine warnings. Your Node version is below 20. Run nvm use (the repo's .nvmrc pins Node 20) or upgrade Node from nodejs.org.

The inline PDF preview is blank. Your browser couldn't render the PDF in an <iframe>. Click the Open in new tab action on the row instead. Firefox on some Linux distros ships without the PDF viewer — installing firefox-pdfjs (or switching to a Chromium-based browser) fixes it.

My starter resume isn't being used. Make sure it is exactly at data/resumes/starter.pdf (lowercase, relative to the project root). Restart the dev server after adding the file. If you submitted an application before adding starter.pdf, its resumeFile is locked to whatever was selected at the time — edit the application to use the starter.

data/applications.json looks corrupt / the app won't list anything. Stop the dev server, back up the file, then delete it. It is recreated empty on the next API call. Restore from your last backup if needed (see Backup, Reset & Migration).

Permission errors writing to data/. The Node process needs read/write to data/ and data/resumes/. On macOS/Linux: chmod -R u+rw data/. On Windows, check the folder isn't read-only and that no other process (cloud sync clients sometimes lock files) is holding it open.

Build errors after pulling new code. Stale caches are usually the cause:

rm -rf .next node_modules
npm install
npm run build

Windows: paths over 260 characters fail. Enable long paths: open an admin PowerShell and run New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force (one-time).

Can I run this on Node 18? No. Next.js 16 requires Node ≥ 20. Anything older is unsupported.

Where did my data go after npm run dev? It didn't move. Everything is in ./data/ relative to the project root. The dev server doesn't move, encrypt, or rewrite it.

Known Limitations

  • Single user, no auth. Anyone with access to the running server can read and modify everything. Do not expose this to the public internet (see Deployment).
  • No multi-instance safety. lib/storage.ts uses an in-process mutex to serialize JSON writes. If you run two next dev servers against the same data/ folder, they will not coordinate and last-write-wins applies.
  • No sync between devices. Move the data/ folder manually (see Backup, Reset & Migration). There is no built-in cloud sync.
  • Mobile is not officially supported. The layout is responsive down to tablet width; phone-sized screens will work but the data table is not optimized for them.
  • No bulk import. Applications can only be added one at a time through the UI or one POST request at a time through the API.
  • No undo for deletes. Deleting an application also deletes its PDF (unless it is the shared starter.pdf). Restore from a backup if you need it back.
  • Practical record limit. Everything is loaded into memory and sorted/filtered client-side. Performance degrades past a few thousand applications; the app is not designed for high-volume use.
  • PDFs only. Resume uploads are restricted to PDF files (validated by both MIME type and the %PDF magic bytes). DOCX, ODT, and other formats are rejected.

Folder Structure

job-tracker/
├── app/
│   ├── api/
│   │   ├── applications/
│   │   │   ├── route.ts              # GET all, POST new
│   │   │   └── [id]/route.ts         # GET one, PUT, DELETE
│   │   └── resumes/
│   │       ├── upload/route.ts       # POST — validate & save PDF
│   │       └── [filename]/route.ts   # GET — stream PDF with path sanitization
│   ├── layout.tsx
│   └── page.tsx                      # Dashboard (all UI state)
├── components/
│   ├── ApplicationModal.tsx          # Add/edit form modal
│   ├── InlineStatusSelect.tsx        # Quick status dropdown per row
│   ├── ResumePreviewModal.tsx        # PDF iframe preview
│   └── StatusBadge.tsx               # Color-coded status pill
├── lib/
│   ├── config.ts                     # Constants (STARTER_RESUME)
│   └── storage.ts                    # Atomic JSON read/write with mutex
├── types/
│   └── application.ts                # Application interface + status union
├── docs/
│   └── screenshots/                  # README screenshots
├── data/                             # Git-ignored at runtime
│   ├── .gitkeep
│   ├── applications.json             # Auto-created on first run
│   └── resumes/
│       ├── .gitkeep
│       └── starter.pdf               # Drop your resume here
└── public/

Contributing

See CONTRIBUTING.md.

License

MIT © Krupesh Vachhani — see LICENSE

About

Local-first personal resume & job application tracker. No database, no cloud, no auth — runs entirely on your machine.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages