================================================================================ RESUME TRACKER A self-hosted, per-recipient resume open-tracking system github.com/seniho | seniho.com ================================================================================ OVERVIEW -------- Resume Tracker is a lightweight, self-hosted tool that lets you know exactly when (and how many times) a recruiter or hiring manager has opened your resume link. Each recipient gets a unique tracking URL. When they click it, the system silently logs the visit and immediately redirects them to your actual resume — no delay, no popup, nothing suspicious. You get the data; they get your resume. The dashboard lives at seniho.com/tracker behind a password-only login. All data is stored locally in a SQLite database file, and the whole system runs in a single Docker container. WHY THIS APPROACH ----------------- Two options exist for resume tracking: 1. Track PDF opens — Requires embedding a tracking pixel or remote resource in the PDF. Most PDF readers block external network calls, and email clients often strip attachments before scanning. Unreliable. 2. Track link clicks — The recipient clicks a URL (seniho.com/r/abc123) which logs the visit server-side, then redirects them to the resume. 100% reliable, works in every browser and email client. This project uses option 2. WHAT'S TRACKED -------------- For each visit, the system captures: - Timestamp (UTC) - IP address - Browser (Chrome, Safari, Outlook Web, etc.) - OS (macOS, Windows, iOS, Android) - Device type (desktop, mobile, tablet) - Full user-agent string (stored for reference) What is NOT tracked: cookies, session data, scroll depth, time-on-page, or any personal information beyond the standard HTTP request headers. TECH STACK ---------- Runtime Node.js 20 (Alpine) Web Framework Express 4 Database SQLite via better-sqlite3 (WAL mode, zero-config) UA Parsing ua-parser-js Auth Password-only login, HMAC-signed session cookie (scrypt-hashed password stored in SQLite, no extra deps) Container Docker + Docker Compose Reverse Proxy Nginx (snippet provided in nginx.conf) Frontend Vanilla HTML / CSS / JavaScript (no framework, no build step) ARCHITECTURE ------------ ┌─────────────────────────────────────┐ │ Nginx (seniho.com) │ │ │ │ /tracker ──────────────────────┐ │ │ /r/* ─────────────────┐ │ │ └──────────────────────────────┼───┼──┘ │ │ ┌────────────▼───▼────────────┐ │ Express host:3847→ctr:8080 │ │ │ │ GET /r/:token │ │ → log view to SQLite │ │ → 302 redirect to resume │ │ │ │ /tracker (session cookie) │ │ → serves dashboard SPA │ │ → /tracker/api/* (JSON) │ └──────────────────────────────┘ │ ┌────────────▼────────────┐ │ SQLite (./data/) │ │ │ │ recipients │ │ tracking_links │ │ views │ └─────────────────────────┘ Database schema: recipients — id, name, email, company, notes, created_at tracking_links — id, recipient_id, token (16-char hex), created_at views — id, link_id, viewed_at, ip_address, user_agent, device_type, browser, os DIRECTORY STRUCTURE ------------------- resume-tracker/ ├── docker-compose.yml Compose config (env vars, port, volume) ├── nginx.conf Nginx location blocks (add to your server{}) ├── README.txt This file ├── data/ SQLite database (created at runtime, git-ignored) └── backend/ ├── Dockerfile ├── package.json └── src/ ├── server.js Express routes & auth ├── database.js SQLite schema, queries, UA parsing └── public/ ├── index.html Dashboard SPA ├── style.css Dark-theme styles └── app.js Dashboard logic (vanilla JS) DEPLOYMENT ---------- Prerequisites: Docker, Docker Compose, Nginx already running for seniho.com. 1. Clone / copy this directory onto your server. 2. Edit docker-compose.yml — set these environment variables: DASHBOARD_PASSWORD Initial login password (default "admin"), seeded into the DB on first run only; change it later in the UI RESUME_URL Where tracking links redirect to (your actual resume) BASE_URL Your domain, e.g. https://seniho.com 3. Start the container: docker compose up -d 4. Add the Nginx proxy rules from nginx.conf into your seniho.com server block, then reload Nginx: sudo nginx -t && sudo nginx -s reload 5. Visit https://seniho.com/tracker and log in. The default password is "admin" (or whatever DASHBOARD_PASSWORD was set to on first run). Change it from the dashboard via the "Change Password" button in the header. 6. Click "New Recipient", fill in the recruiter's details, and copy the generated tracking link. Paste it into your email or application. USAGE ----- Adding a recipient: Dashboard → New Recipient → fill in name (required), email, company, notes. A unique 16-char hex token is generated. The full URL is automatically copied to your clipboard: https://seniho.com/r/ Sending the link: Paste the tracking URL wherever you'd normally link your resume — email body, LinkedIn message, application form, etc. The recipient sees a normal resume URL and is redirected instantly. Checking opens: The dashboard shows: • Status badge — Opened (green) or Not Opened (red) • View count — total number of opens • Last seen — relative timestamp of most recent open • Device — desktop / mobile / tablet icon Click the ▸ arrow to expand a row and see the full view history with timestamps, IPs, browsers, and OS for each individual open. Deleting a recipient: Click Delete on any row. This removes the recipient, their tracking link, and all associated view data from the database. RUNNING LOCALLY (development) ------------------------------ cd resume-tracker/backend npm install DASHBOARD_PASSWORD=admin RESUME_URL=https://example.com BASE_URL=http://localhost:8080 npm start Then open http://localhost:8080/tracker (password: admin) Test a tracking link: http://localhost:8080/r/ SECURITY NOTES -------------- • The dashboard uses a password-only login. The password is hashed with scrypt and stored in SQLite; the session is an HMAC-signed, expiring cookie (HttpOnly, SameSite=Lax, Secure over HTTPS). Changing the password rotates the signing secret, invalidating all other existing sessions. Always run behind HTTPS (Nginx + Let's Encrypt). • The /r/:token endpoint is intentionally public (no auth) so that recipients don't see a login prompt when clicking your link. • Tracking tokens are 8 bytes of cryptographically random data (16 hex chars), making enumeration attacks infeasible. • The SQLite file is stored in ./data/ (bind-mounted Docker volume). Back it up if you care about the history. • IP addresses are stored as-is. If your Nginx sets X-Forwarded-For and Express is behind the proxy (trust proxy = 1, already set), req.ip will reflect the real client IP, not the proxy. PORTFOLIO NOTES --------------- This project demonstrates: • Containerization with Docker and Docker Compose • Reverse-proxy integration with Nginx • RESTful API design with Express • Persistent storage with SQLite and WAL mode for concurrent writes • Server-side analytics (UA parsing, IP capture) • Auth-protected admin dashboard in vanilla JS (no framework) • Security considerations: crypto tokens, HTTPS enforcement, proxy headers ================================================================================