Tracker is a single-worker application: a Cloudflare Worker serves both the REST API and the static SPA assets. There is no separate backend server.
Cloudflare Worker
┌──────────────────────────────────────┐
/api/* ───────────────► Hono Router │
│ ├── /api/auth/* (no auth req.) │
│ ├── /api/* (auth required) │
│ ├── /api/jobs/* │
│ └── /api/stats │
│ │
/* (non-API) ──────────► ASSETS binding ──────────────────► SPA
└──────────────────────────────────────┘
tracker/
├── src/ # React SPA
│ ├── components/ # UI components (JobCard, KanbanColumn, JobDrawer, Logo, etc.)
│ ├── lib/ # API client, auth context, theme, custom hooks
│ ├── pages/ # Route pages (Landing, Login, Dashboard, Board, TableView)
│ ├── App.tsx # Root component with routing + auth gating
│ └── main.tsx # Entry point
├── worker/ # Hono API (Cloudflare Worker)
│ ├── lib/auth.ts # JWT signing/verification, password hashing
│ ├── routes/ # Route modules (auth, jobs, items, stats)
│ ├── index.ts # Worker entry — Hono app, middleware, router setup
│ └── api.test.ts # Integration tests
├── shared/ # Types shared between client and server
├── migrations/ # D1 SQL migrations
└── wrangler.jsonc # Cloudflare Workers configuration
Built with React 19 + Vite + Tailwind CSS v4.
No external state library — React context and component state are sufficient:
- AuthContext — stores the current user, provides
login/register/logout - RemindersContext — polls
/api/reminders/upcomingevery 5 minutes, provides reminder list andcompleteaction - Component state — each page manages its own data via
useState+useEffect(e.g.,Board.tsxfetches jobs on mount)
react-router-dom v7 with a simple auth gate in App.tsx:
- Unauthenticated: Landing page (
/), Login page (/login), everything else redirects to/ - Authenticated: Dashboard (
/), Board (/board), Table (/table)
Brutalist design system built on Tailwind CSS v4 @theme directives:
- Colors:
brut-ink(#111),brut-paper(#f2eee3),brut-surface(#fff),brut-yellow(#ffd60a), plus five pipeline stage colors (CVD-validated Okabe-Ito palette) - Components:
btn-brut,card-brut,panel-brut,input-brut,badge-brut— thick borders, hard box-shadows, bold uppercase type - Typography: Space Grotesk (500–800 weight), zero border-radius everywhere
Uses dnd-kit (@dnd-kit/core + @dnd-kit/sortable):
onDragOver— optimistic local state update (cross-column drag)onDragEnd— callsPATCH /api/jobs/:id/move, replaces the destination column from the server response- Collision detection —
closestCornersstrategy
Built with Hono v4. Runs on Cloudflare Workers with D1 database and static asset binding.
Request → Worker (fetch handler)
→ URL matches /api/*? → Hono router
→ /api/auth/* → requireAuth middleware (skip for auth routes)
→ Route handler → D1 query → JSON response
→ Otherwise → ASSETS.fetch(request) → SPA or static file
- Registration/Login — validates with Zod, hashes the password (PBKDF2-HMAC-SHA256 via WebCrypto), signs a JWT (HS256, jose), sets the httpOnly cookie
tracker_auth. Cookie options (secure,sameSite,path,httpOnly) come from one sharedsessionCookieOptshelper used by bothcreateSessionandclearSession— over HTTPS the deletion must carrySecuretoo or the browser ignores it against the existing Secure cookie. - Verification —
requireAuthreads the cookie, verifies the JWT, checks the token version against the database, then setsc.set("userId", userId) - Logout — clears the auth cookie in the response (with matching flags, see above). It does not revoke the JWT: the token stays valid until it expires, so logout protects the browser it ran in, not a token that has already been stolen. Revocation is what
token_versionis for. - Password change — increments
users.token_version, which invalidates every session issued before it
Notes:
- Why PBKDF2 and not bcrypt.
bcryptjsis pure JS and costs ~74ms of CPU per hash. The Workers Free plan allows 10ms per request, so login hard-failed there. PBKDF2 runs natively. Seeworker/lib/password.tsfor the iteration count and the strength/cost trade-off behind it — it is a deliberate compromise, not a default. - Legacy hashes. Accounts created under bcrypt still log in and are transparently re-hashed to PBKDF2 on their next successful login.
- Rate limiting. The credential endpoints are throttled per-IP via Cloudflare's rate-limit binding. The binding is optional: without it the endpoints still work and log a warning.
Cloudflare D1 (SQLite via workerd). Five tables with ON DELETE CASCADE:
users ──→ jobs ──→ contacts
├──→ activities
└──→ reminders
Child resources (contacts, activities, reminders) use a factory pattern in worker/routes/items.ts — childRoutes() and itemRoutes() generate consistent CRUD handlers with ownership enforcement:
-- Every child query joins through the parent job to verify ownership
WHERE job_id IN (SELECT id FROM jobs WHERE user_id = ?)Stats are computed client-side from D1 aggregate queries in worker/routes/stats.ts. Four concurrent queries via Promise.all:
- Pipeline funnel (COUNT per status)
- Total active, response rate, offers count
- Avg days from application to first interview
- Weekly application volume (last 12 weeks)
Every stats query filters archived = 0. The response-rate denominator and the weekly chart additionally gate on status = 'applied', because applied_at is a permanent "first applied" stamp that is retained when a job is demoted — gating on the stamp alone would keep a demoted job in those counts. List endpoints (GET /jobs, the per-job child GETs, and the top-level /contacts·/activities·/reminders lists) are capped with a ?limit clamp so no query is unbounded.
See Setup Guide for deployment instructions.