Skip to content

Latest commit

 

History

History
167 lines (124 loc) · 7.15 KB

File metadata and controls

167 lines (124 loc) · 7.15 KB

Job Tracker

A personal referral-tracking tool for job hunting.

Why this exists

When you're job hunting, applying cold rarely works as well as getting a referral. But referrals are tedious to manage: you ask a recruiter, a LinkedIn connection, or a friend; you send a connection request and wait for it to be accepted before you can even ask; someone says "ping me next week"; another goes quiet and you lose track of whether you ever followed up. Multiply that across a few dozen open roles and it's easy to lose the thread entirely.

This app is a small, personal audit trail for that process: who you've asked for a referral, on which job, what stage that ask is at, and when you need to nudge someone or just apply directly because the referral window has passed.

It was built purely for my own job search — it is not a polished product, just a hobby project scratching a personal itch. Feel free to fork it and adapt it for your own use.

Want to use the hosted version instead of running your own? Comment on this repo with your email and I'll add you to the allow-list.

Nomenclature

The app is built around a small vocabulary. If something in the UI is unclear, this section is the reference.

Referral types — who you're asking

Type Meaning
Talent Partner A recruiter or Talent Acquisition contact (internal or agency) who can submit you for a role. Shown in the API/database as TA.
Connection An existing LinkedIn connection, or someone you intend to connect with, who could refer you internally.
Friend A personal contact you'd reach out to organically — a call, a WhatsApp message, etc. — rather than through LinkedIn.

Job Status vs. Referral Status — two independent tracks

A Job has two separate status concepts that are easy to conflate:

  • Job Status — manually set by you: Saved, Applied, Rejected, Closed.
  • Referral Statusderived automatically from that job's Referrals, not something you set directly: Not Started, In Progress, Referred, Not Required.

A job can be Applied while its Referral Status is independently In Progress (you applied without waiting on the referral) — the two tracks don't have to move together.

The one manual override is a "Referral not required" checkbox on a job, which forces Referral Status to Not Required. It clears itself automatically the moment you add a Referral to that job, since adding one means you're actively pursuing it again.

Referral (record) statuses

Each individual Referral has its own status, and which ones are valid depends on the referral type (e.g. a Friend referral has no "Connection Requested" step, since there's no LinkedIn connection to request):

PlannedConnection Requested (Talent Partner / Connection only) → Referral RequestedReferred, or Declined at any point.

Task categories — what needs your attention

The Tasks page is a computed checklist, not something you fill in by hand. Every task falls into one of four categories:

Category Fires when...
Pending Referral Request A referral request has been sent to a Talent Partner/Connection and is awaiting a response.
Pending Connection Request A LinkedIn connection request is out and awaiting acceptance, before you can ask for a referral.
Pending Ask Friend A friend has been identified as a referral source but hasn't been asked yet (or asked and awaiting response).
Apply Today The job's referral waiting window (4 days by default) has passed with no referral, and you haven't applied yet — time to just apply directly.

Tasks disappear once the job is marked Applied, or once the referral waiting window has passed (at which point only Apply Today shows for that job).

Dashboard widgets

  • Jobs Waiting for Referral — count of jobs with at least one live Pending Referral Request, Pending Connection Request, or Pending Ask Friend task. Clicking it opens the Tasks page pre-filtered to those three categories.
  • Apply Today — count of jobs with a live Apply Today task. Clicking it opens the Tasks page filtered to just that category.

Tasks page filter

The only filter on the Tasks page is by Task Category (checkboxes for the four categories above). There's no filter by job, status, or date.

Known limitations

  • Dismissal is permanent. Checking off a task (with a 5-second undo window) removes it for good — there's no snooze or "remind me tomorrow." If someone asks you to follow up later, you currently have to track that yourself (e.g. in the job's Notes field) rather than relying on the app to resurface it.
  • Referral-status-per-type rules are hand-synced. The set of valid Referral statuses per referral type is duplicated between the frontend and backend (no shared schema/codegen), so a future change to one must be mirrored in the other by hand.
  • No password/session management beyond Google sign-in — see the security note below.

Tech stack

  • Backend: Spring Boot 4.1 (Java 25), PostgreSQL, Flyway migrations, Spring Security with an app-issued JWT (Google ID token verified server-side, not Spring Security's OAuth2 login flow).
  • Frontend: React 19 + TypeScript + Vite, React Router v7, TanStack Query v5, Tailwind CSS v4 + shadcn/ui.

See docs/PRD.md for the full product spec and docs/TECH_DESIGN.md for the data model, API surface, and task-computation rules in detail.

Running it locally

Backend

docker compose up -d          # starts Postgres
cd backend
./mvnw spring-boot:run         # http://localhost:8080

Configure via environment variables (all have local-dev defaults — see backend/src/main/resources/application.yml):

  • DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
  • JWT_SECRET, JWT_EXPIRATION_MINUTES (default 1440)
  • GOOGLE_CLIENT_ID — your Google OAuth Client ID
  • CORS_ALLOWED_ORIGINS (default http://localhost:5173)
  • SERVER_PORT (default 8080)

Frontend

cd frontend
npm install
cp .env.example .env    # then fill in VITE_GOOGLE_CLIENT_ID
npm run dev              # http://localhost:5173

Without VITE_GOOGLE_CLIENT_ID set, the login page shows a fallback message instead of the Google Sign-In button — useful for local testing with hand-crafted JWTs in localStorage instead of real Google auth.

Security notes

  • JWT_SECRET has a dev-only default fallback (application.yml). It's a publicly-committed placeholder — anyone deploying this app must override it with a real, private secret, or an attacker who knows the default can forge a valid login token for any user. Never run this in any non-local/shared environment without setting JWT_SECRET explicitly.
  • The app JWT is stored in localStorage on the frontend (not an httpOnly cookie). This is a deliberate simplification for a personal-use SPA with no CSRF surface, but it does mean an XSS vulnerability (e.g. from a compromised dependency) could exfiltrate the token. Acceptable for personal use; would need revisiting for anything handling other people's data at scale.

License

MIT — see LICENSE.