Skip to content

berkinduz/job-apply-tracker

Repository files navigation

JobTrack β€” Your Job Hunt's Command Center

Track every application, follow up on time, and see what's actually working. Free, forever.

🌐 Live: jobapplytracker.com

Next.js React TypeScript Supabase Tailwind CSS Resend


What it does

The job-search loop is messy: spreadsheets that started clean and ended unreadable, follow-ups that slipped, no idea which channels were actually converting. JobTrack replaces the spreadsheet with a calm, opinionated tracker that knows about real-world job-hunt mechanics β€” kanban progress, follow-up reminders that actually email you, drag-paste a job URL to autofill the form, import from CSV, share a stripped-down stats profile, and a per-application activity log so you can see what moved when.

It's a full-stack TypeScript app built on Next.js 16 (App Router, RSC), Supabase (Postgres + Auth + Storage), Resend for outbound mail, and Vercel for hosting + cron.


Visual tour

Applications list with pipeline health card and quick filters

List view β€” pipeline health strip, quick filter chips, sortable list.

Kanban board with status columns

Kanban β€” drag applications across stages, sticky horizontal scroll.

Application detail with pipeline, activity timeline, follow-up card, details sidebar

Detail β€” pipeline progress, real activity timeline, follow-up reminder card, details sidebar.

Analytics with insights, funnel, source performance

Analytics β€” funnel, source performance, conversational insights.

Dark-mode variants are bundled at public/*_dark.png and used automatically when the visitor's system or theme preference is dark.


Feature catalog

Capture

  • Paste a job URL β†’ autofill. Server-side scraper (/api/jobs/parse) tries schema.org JobPosting JSON-LD first (covers Greenhouse, Lever, Workable, Ashby, SmartRecruiters), then LinkedIn-specific DOM hooks, then OpenGraph, then page title heuristics. SSRF-safe (blocks private hosts, 12s timeout, auth-gated). Pulls company, role, location, JD content, work type, source, and salary range when present.
  • Quick add bar. "Company β†’ Role" inline form at the top of the list β€” 30-second new entry without leaving the page.
  • CSV import. papaparse-based ingestion with header auto-detection (EN+TR synonym table), status/work-type/date aliasing (phone screen β†’ hr_interview, DD/MM/YYYY β†’ ISO), per-row validation with skip-and-continue on errors, override-able column mapping in the preview step. Sample CSV download included.
  • Resume attachments. Per-application PDF upload (≀2 MB) to a private Supabase Storage bucket with per-user RLS.

Track

  • 8-stage pipeline. Applied β†’ Test Case β†’ HR Interview β†’ Technical Interview β†’ Management Interview β†’ Offer β†’ Accepted / Rejected. Status changes log to the activity feed.
  • Drag-and-drop kanban. Edge-scroll while dragging, always-visible scrollbar so the off-screen columns are obvious.
  • List view. Pinned-first, recent-first sort, inline status pills, follow-up badges (Overdue Nd / Follow up today / Follow up in Nd).
  • Pipeline health strip. Active count Β· This week Β· Response rate % Β· either Follow-ups due (red when > 0) or Best converting source.
  • Quick filter chips. Active / This week / Stale / Follow-up due / All / Closed, each with a live count.
  • Cmd+K command palette. Jump to any application by name, or to Applications / Analytics / Settings / New Application.

Engage

  • Follow-up reminders. Pick a date (preset chips or calendar). When the date arrives the daily cron at 08:00 UTC (/api/cron/follow-ups) emails the user via Resend, grouped per user (one email per inbox per day, no spam). Idempotent via follow_up_sent_at stamping. Snooze (+1d / +3d / +1w), Mark done, Reschedule, Clear β€” all inline in the detail sidebar.
  • Activity event log. Real activity_events table; the timeline shows every status change, note, resume upload, follow-up set/done, pin/unpin, and creation event with payload-aware labels.
  • Quick notes. Add an inline note from the detail page without opening the full edit form.

Analytics

  • Funnel by stage, source performance bars, average-response-time, conversational insights ("Your response rate is 100% β€” that's strong. Whatever you're doing, keep doing it.").
  • Per-user rendering β€” every figure is RLS-scoped.

Share

  • Public profile at /u/[handle]. Opt-in, picks a unique handle (case-insensitive, 2–32 chars, reserved-name list). Shows applications count, response rate, offers, weekly streak, funnel %, top sources. Optional company highlights (off by default β€” good for stealth job hunts). Dynamic OG image at /u/[handle]/og so shared links preview with the user's actual stats. Sitemap auto-discovers opted-in handles.

Account & Settings

  • Theme toggle (light / dark / system) β€” works for logged-out visitors on the landing page too.
  • i18n β€” English + TΓΌrkΓ§e across landing, login, settings, status labels. Inline language switcher in the landing header for logged-out visitors.
  • Custom sources & industries β€” bend the source/industry pickers to your search style.
  • Hide rejected toggle to focus the active pipeline.
  • Follow-up email opt-in toggle (synced to user_settings.follow_up_emails, respected by the cron).
  • Public profile card with handle picker, live availability check, display name, show-companies toggle, copy link.
  • Export to JSON β€” full dump of your applications + settings.
  • Clear all data β€” wipes apps, activity, resumes, settings on the server. Sign-in preserved.
  • Delete account β€” type-DELETE-to-confirm; cascades via FK deletes on auth.users. Resume files removed from storage. Signs out + redirects home.

Auth

  • Email + password, Google OAuth, GitHub OAuth, passwordless magic links β€” all via Supabase Auth.
  • Password-reset flow, password strength meter on signup.
  • Middleware-enforced route protection.

Polish

  • PWA installable. Dynamic manifest + apple-touch-icon, SVG favicon multi-size ICO fallback.
  • Privacy + Terms pages at /privacy and /terms with shared JtLegalShell and readable typography.
  • Sitemap + robots.txt auto-generated, public profiles included.
  • Dynamic OG image at /opengraph-image for the home page, separate per-handle generator for public profiles.
  • Toast feedback (sonner) on every mutation.
  • Skeleton loaders for application list and detail.
  • Mobile bottom nav, in-app new-app form sticky save bar on mobile, inline footer on desktop.

Tech stack

Frontend

Tool Purpose
Next.js 16 App Router, RSC, route handlers, dynamic OG via ImageResponse
React 19 UI
TypeScript 5 Strict types end-to-end
Tailwind CSS 4 Utility styling
shadcn/ui Accessible primitives (Dialog, Popover, Command, Dropdown, etc.)
Radix UI Underlying headless primitives
Zustand Client store with optimistic mutations
React Hook Form Form state
date-fns Date math + formatting
Lucide React Icon set
Sonner Toasts
cmdk Command palette
next-intl EN / TR localization
next-themes Theme switching

Backend / data

Tool Purpose
Supabase Postgres + Auth + Storage + RLS
Resend Outbound transactional email (follow-up reminders)
Vercel Hosting + cron (vercel.json daily schedule)
cheerio HTML parsing for the URL-paste autofill
papaparse CSV parser for import flow
sharp Build-time favicon ICO generation from the SVG brand mark

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Browser (RSC + Client)                    β”‚
β”‚  Landing β€’ Login β€’ /applications β€’ /analytics β€’ /u/[handle]  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚          β”‚                                              β”‚
   β–Ό          β–Ό                                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Server β”‚  β”‚ /api/jobs/parse      β”‚    β”‚ /api/cron/follow-ups     β”‚
β”‚ Actionsβ”‚  β”‚ (cheerio scraper,    β”‚    β”‚ (Bearer-auth, service    β”‚
β”‚        β”‚  β”‚  auth-gated)         β”‚    β”‚  role, daily 08:00 UTC)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   β”‚                  β”‚                                β”‚
   β”‚                  β”‚                                β–Ό
   β”‚                  β”‚                       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚                  β”‚                       β”‚ Resend          β”‚
   β”‚                  β”‚                       β”‚ (verified domain)β”‚
   β”‚                  β”‚                       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   β–Ό                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Supabase                             β”‚
β”‚  Auth Β· Postgres (RLS) Β· Storage (resumes/)                  β”‚
β”‚  Tables: applications Β· activity_events Β· user_settings      β”‚
β”‚          skill_suggestions                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Data model

auth.users
   β–²       β–²       β–²
   β”‚       β”‚       β”‚  ON DELETE CASCADE
   β”‚       β”‚       β”‚
   β”‚       β”‚       └─── user_settings
   β”‚       β”‚              Β· hide_rejected, custom_sources, custom_industries
   β”‚       β”‚              Β· follow_up_emails
   β”‚       β”‚              Β· public_handle (unique), public_enabled,
   β”‚       β”‚                public_show_companies, public_display_name
   β”‚       β”‚
   β”‚       └─── applications
   β”‚              Β· status (enum), work_type (enum), is_pinned
   β”‚              Β· follow_up_date, follow_up_sent_at,
   β”‚                follow_up_completed_at
   β”‚              Β· contacts (jsonb), skills (text[])
   β”‚              β–²
   β”‚              β”‚  ON DELETE CASCADE
   β”‚              β”‚
   β”‚              └─── activity_events  (append-only)
   β”‚                     Β· kind, payload (jsonb)
   β”‚
   └─── storage.objects in resumes/{user_id}/{app_id}/resume.pdf

RLS is enforced on every user-owned table β€” direct queries are scoped by auth.uid(). The cron worker uses the service-role key only inside the Bearer-gated route, never on the client.


Getting started (local)

Prerequisites

  • Node 18+
  • A Supabase project (free tier is fine)
  • A Resend account if you want follow-up emails to actually send locally

Setup

git clone https://github.com/berkinduz/job-apply-tracker.git
cd job-apply-tracker
npm install
cp .env.example .env.local   # then fill in the values below

Environment variables

# Supabase β€” both client and server
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...

# Server-only (never expose to the browser)
SUPABASE_SERVICE_ROLE_KEY=eyJ...

# Public URL β€” used in OG, sitemap, email links
NEXT_PUBLIC_SITE_URL=http://localhost:3000

# Follow-up email cron
CRON_SECRET=long-random-string                # openssl rand -hex 32
RESEND_API_KEY=re_xxx                         # optional locally
EMAIL_FROM=JobTrack <reminders@example.com>   # optional; default is set in code

Database

Apply the SQL files in supabase-*.sql in order via the Supabase SQL editor, or rebuild from the migrations history in src/types/database.ts. Key tables: applications, user_settings, activity_events, skill_suggestions.

Don't forget to:

  • Enable Row Level Security on every user table (each has its own policy).
  • Create the private resumes storage bucket with per-folder RLS ((storage.foldername(name))[1] = auth.uid()::text).
  • Turn on "Leaked password protection" under Auth β†’ Policies.
  • Connect a custom SMTP (or use Supabase's default for testing β€” rate-limited to 3 mails/hour).

Run it

npm run dev
# Landing  β†’ http://localhost:3000
# App      β†’ http://localhost:3000/applications

Test the cron locally

curl -i -H "Authorization: Bearer $CRON_SECRET" \
  http://localhost:3000/api/cron/follow-ups

Without RESEND_API_KEY, the route returns { ok: true, sent: 0, errors: [...] } with a reason-skipped notice β€” useful while you wire Resend up.


Project structure

src/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ api/
β”‚   β”‚   β”œβ”€β”€ cron/follow-ups/route.ts    # daily reminder worker
β”‚   β”‚   └── jobs/parse/route.ts         # URL paste autofill
β”‚   β”œβ”€β”€ applications/                   # list, new, detail, edit (RSC)
β”‚   β”œβ”€β”€ analytics/                      # analytics page
β”‚   β”œβ”€β”€ auth/                           # OAuth callback
β”‚   β”œβ”€β”€ login/                          # email/OAuth/magic link
β”‚   β”œβ”€β”€ onboarding/                     # 3-step welcome flow
β”‚   β”œβ”€β”€ settings/                       # account, theme, customization, danger zone
β”‚   β”‚   β”œβ”€β”€ danger-actions.ts           # clearAllData + deleteAccount
β”‚   β”‚   └── public-profile-actions.ts   # handle pick + save profile
β”‚   β”œβ”€β”€ u/[handle]/                     # public profile + dynamic OG
β”‚   β”œβ”€β”€ privacy/  terms/                # legal pages
β”‚   β”œβ”€β”€ icon.svg  apple-icon.tsx        # favicon assets
β”‚   β”œβ”€β”€ manifest.ts  robots.ts  sitemap.ts
β”‚   └── opengraph-image.tsx             # landing OG generator
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ jt/                             # "Design v2" components β€” app shell,
β”‚   β”‚   β”œβ”€β”€ app-shell.tsx               # header + nav + Cmd+K palette
β”‚   β”‚   β”œβ”€β”€ application-form.tsx        # new/edit form with URL paste autofill
β”‚   β”‚   β”œβ”€β”€ application-detail.tsx      # detail page
β”‚   β”‚   β”œβ”€β”€ application-detail-extras.tsx  # pipeline, timeline, follow-up card
β”‚   β”‚   β”œβ”€β”€ applications-page.tsx       # list + kanban + filters
β”‚   β”‚   β”œβ”€β”€ csv-import-dialog.tsx       # CSV import wizard
β”‚   β”‚   β”œβ”€β”€ landing.tsx                 # marketing homepage
β”‚   β”‚   β”œβ”€β”€ login.tsx                   # auth screen
β”‚   β”‚   β”œβ”€β”€ onboarding.tsx              # 3-step welcome
β”‚   β”‚   β”œβ”€β”€ public-profile.tsx          # /u/[handle] renderer
β”‚   β”‚   β”œβ”€β”€ public-profile-card.tsx     # settings card
β”‚   β”‚   β”œβ”€β”€ primitives.tsx              # JtButton, JtPill, JtDot, status tokens
β”‚   β”‚   └── settings.tsx                # the settings page
β”‚   β”œβ”€β”€ applications/                   # list/card/kanban (data-bound)
β”‚   └── ui/                             # shadcn primitives
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ csv/import.ts                   # papaparse + auto-detect + alias map
β”‚   β”œβ”€β”€ email/send.ts                   # Resend wrapper + email shell
β”‚   β”œβ”€β”€ jobs/parse.ts                   # cheerio JD scraper
β”‚   β”œβ”€β”€ public-profile/stats.ts         # funnel aggregation
β”‚   └── supabase/                       # browser, server, admin (service-role)
β”‚       β”œβ”€β”€ applications.ts             # CRUD + bulk + follow-up
β”‚       └── activity.ts                 # event log writer + reader
β”œβ”€β”€ messages/{en,tr}.json               # i18n
β”œβ”€β”€ store/index.ts                      # Zustand store (applications + settings)
β”œβ”€β”€ i18n/request.ts                     # next-intl setup
└── types/                              # JobApplication, ActivityEvent, Database

Deployment

The app deploys cleanly to Vercel β€” push to main, Vercel rebuilds, Cron picks up vercel.json automatically.

Production checklist

  • NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY set in Vercel
  • SUPABASE_SERVICE_ROLE_KEY set (marked Sensitive)
  • NEXT_PUBLIC_SITE_URL=https://yourdomain.com
  • CRON_SECRET set (marked Sensitive) β€” a fresh openssl rand -hex 32
  • RESEND_API_KEY set (marked Sensitive) and the sending domain verified (SPF + DKIM)
  • EMAIL_FROM=JobTrack <reminders@yourdomain.com>
  • Storage bucket resumes exists, private, with the per-user RLS policy
  • auth.users leaked-password protection enabled in Supabase dashboard
  • Vercel Cron lists /api/cron/follow-ups at the daily schedule

The HANDOFF doc (HANDOFF.md) walks through the rest of the manual setup if you're forking this and going to production with it.


What's next

Built, not yet built:

  • Email forwarding β€” you@inbox.jobtrack.app β†’ forward a job listing email, auto-create application. Resend inbound webhook + HTML extractor.
  • Browser extension β€” "Track in JobTrack" button on LinkedIn / Indeed job pages. Manifest v3 + popup auth flow.
  • AI features (gated behind a future paid tier) β€” JD summarizer, cover-letter draft from JD + resume, interview prep prompts.
  • Notification preferences β€” per-kind opt-in (only today, only overdue, weekly digest).
  • Bulk actions β€” multi-select on the list view for bulk status change / bulk archive.

License

MIT β€” see LICENSE.

Author

Berkin Duz


If JobTrack helps you land your next role, a ⭐ on the repo is the nicest way to say thanks.

About

🎯 A modern, full-stack job application tracking system built with Next.js 16 and Supabase. Keep track of your job search journey with an intuitive interface, real-time updates, and powerful filtering capabilities.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors