TV show tracker app
Find a file
Brian Ladner 7765de45c6
All checks were successful
CI / check (push) Successful in 1m39s
CI / deploy (push) Successful in 1m23s
wrangler: disable workers.dev and preview URLs (#3)
The app is served on a custom domain. With workers_dev unset, wrangler
deploy calls the account-level workers/subdomain endpoint, which the
Worker-scoped CI token cannot read, so the deploy failed after upload
(Authentication error 10000). Disabling both skips that call.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Reviewed-on: https://forgejo.theladners.com/LabNerd/tv-tracker/pulls/3
Co-authored-by: Brian Ladner <git@theladners.com>
Co-committed-by: Brian Ladner <git@theladners.com>
2026-11-11 14:57:46 -08:00
.forgejo/workflows ci: deploy to Cloudflare Workers on push to main (#2) 2026-11-11 14:42:51 -08:00
.husky chore: unified ESLint config and pre-commit hooks 2026-03-25 22:06:36 -05:00
.vscode updated backlog 2026-03-26 19:54:54 -05:00
apps Remove leftover Docker/nginx deployment files 2026-11-04 03:03:54 +00:00
context chore: update feature history and clear spec 2026-11-03 01:06:01 -06:00
.gitignore chore: fix dev script and node engine, refresh docs 2026-11-03 01:00:38 -06:00
CLAUDE.md ci/forgejo-actions (#1) 2026-11-11 13:58:38 -08:00
eslint.config.mjs chore: unified ESLint config and pre-commit hooks 2026-03-25 22:06:36 -05:00
LICENSE docs: add LICENSE, standardize README headings 2026-11-03 20:40:11 +00:00
package.json docs: add LICENSE, standardize README headings 2026-11-03 20:40:11 +00:00
pnpm-lock.yaml chore: wrangler config improvements 2026-05-31 10:31:21 -05:00
pnpm-workspace.yaml Initial commit 2026-02-19 18:11:13 -06:00
README.md docs: add LICENSE, standardize README headings 2026-11-03 20:40:11 +00:00
TODO.md Clear resolved TODO items 2026-03-05 18:05:19 -06:00
tsconfig.base.json feat: replace Axios API calls with Hono RPC type-safe client 2026-03-26 09:47:05 -05:00
wrangler.jsonc wrangler: disable workers.dev and preview URLs (#3) 2026-11-11 14:57:46 -08:00

TV Tracker

License: MIT

Track the TV shows you follow and see when the next episode airs. Search any show on TVMaze, add it to your list, and see the next and previous episode dates at a glance.

Live demo: tvtracker.labnerd.net. Click Try the demo on the landing page to explore without signing up. It creates a throwaway account pre-loaded with a few shows.

TV Tracker dashboard in card view TV Tracker dashboard in table view

Features

  • Search TVMaze and add shows in one click
  • Next or last episode date for every show, with per-show refresh
  • Card grid or sortable table view
  • Light and dark themes ("Broadcast Noir")
  • Email/password accounts, plus a one-click demo mode

Tech Stack

Layer Tech
API Hono on Cloudflare Workers, Zod validation
Database Cloudflare D1 (SQLite) via Drizzle ORM
UI React 19, React Router 7, MUI v7
API client Hono RPC client, with request and response types inferred from the API's AppType
Auth HttpOnly cookies: 15-minute JWT access token plus a rotating refresh token; bcrypt passwords
Data source TVMaze public API
Tooling TypeScript, pnpm workspaces, Vitest, ESLint, Husky

Engineering Highlights

  • One Worker serves everything. The Worker runs the API under /api/* and serves the built UI as static assets, with a single-page-app fallback so client routes survive a refresh. run_worker_first in wrangler.jsonc keeps API and health requests out of that fallback.
  • Cookie auth, no tokens in JavaScript. The access and refresh tokens are HttpOnly cookies. The refresh token is a random value; only its SHA-256 hash is stored, and it is rotated on every refresh. On a 401 the UI refreshes once and retries.
  • Outbound requests are bounded. TVMaze calls have timeouts and response size caps, and episode links are only followed if they point at api.tvmaze.com.
  • Slow work happens after the response. Episode dates are resolved with ctx.waitUntil, so adding a show returns immediately.
  • Demo mode is built to be disposable. POST /api/auth/demo creates a user on the reserved demo.invalid domain with an unusable password and a seeded show list, capped at 200 live demo users. An hourly cron deletes demo users older than 24 hours (their shows cascade-delete), and registration rejects the reserved domain so a real account can never be swept up.

Local Development

Requires Node 22+ (Wrangler's minimum) and pnpm.

pnpm install

# Local secrets (gitignored). ENVIRONMENT=development is needed so auth cookies work over http.
printf 'JWT_SECRET=%s\nENVIRONMENT=development\n' "$(openssl rand -base64 32)" > .dev.vars

pnpm exec wrangler d1 migrations apply tv-tracker --local

pnpm dev                                           # builds API and UI, then http://localhost:8787

pnpm dev does a full build before starting Wrangler, so restart it to pick up code changes.

Deploying to Cloudflare

pnpm exec wrangler d1 create tv-tracker            # put the returned database_id in wrangler.jsonc
pnpm exec wrangler d1 migrations apply tv-tracker --remote
pnpm exec wrangler secret put JWT_SECRET           # openssl rand -base64 32
pnpm deploy                                        # builds both apps, then wrangler deploy

The cron trigger for demo cleanup is declared in wrangler.jsonc and is registered on deploy.

Commands

Run from the repo root.

pnpm dev                 # build API and UI, run wrangler dev on :8787
pnpm build               # build API and UI
pnpm build:api           # tsc + tsc-alias (output: apps/api/dist/api/src/)
pnpm build:ui
pnpm deploy              # build + wrangler deploy
pnpm lint                # ESLint (API and UI)

pnpm --filter @tv-tracker/api test               # API tests
pnpm --filter @tv-tracker/ui test -- --run       # UI tests

pnpm --filter @tv-tracker/api db:generate        # generate a migration from schema changes
pnpm exec wrangler d1 migrations apply tv-tracker --local

Project Structure

apps/
  api/        Hono API (Cloudflare Worker)
    src/
      routes/           auth, user, admin handlers
      db/               Drizzle schema + query functions
      schemas/          Zod request schemas
      demo/             demo-mode constants and seed shows
      utils/            auth cookies, middleware, background jobs, env helpers
      tvmaze.ts         TVMaze API wrapper
    drizzle/            migration files (commit these)
    tests/
  ui/         React frontend (Vite)
    src/
      apis/             one file per API domain
      contexts/         Auth, Show, Alert, Theme providers
      pages/            route-level components
      components/       shared UI components
      utils/            theme, Hono client, request helpers
    tests/
  shared/     Shared TypeScript types (no build step)
wrangler.jsonc          Worker config: D1 binding, static assets, cron

API overview

POST   /api/auth/register
POST   /api/auth/login
POST   /api/auth/demo             # create and sign in a throwaway demo user
POST   /api/auth/refresh
POST   /api/auth/logout
DELETE /api/auth/deleteUser

GET    /api/user/profile
GET    /api/user/tvshows
GET    /api/user/tvshow/:id
POST   /api/user/tvshow           # add by full TVMaze JSON body
POST   /api/user/tvshow/:id       # add by TVMaze show ID
PATCH  /api/user/tvshow/:id       # refresh from TVMaze
DELETE /api/user/tvshow/:id

GET    /api/admin/users           # admin role only
GET    /health

Protected routes read the accessToken cookie; there is no Authorization header.

Notes

  • Migration files in apps/api/drizzle/ must be committed. Apply them with wrangler d1 migrations apply.
  • There is no rate limiting yet. Demo sign-ups are bounded by the user cap and cleanup, not per-IP limits.
  • Show and episode data comes from TVMaze and is not owned by this project.

License

MIT