A shared TV-show and movie tracker for a small club. Each member maintains four ranked lists (Watching, Awaiting, Recommending, Up Next); the home page surfaces what everyone is watching and exposes a per-member iCalendar feed for upcoming premiere dates.
Live at showpicker.club.
- Product overview & user flows:
docs/PRODUCT.md - Architecture, data model, endpoints:
docs/ARCHITECTURE.md - Apple TV (tvOS) app:
tvos/README.md - iPhone (iOS) app:
ios/README.md
There are native iOS, tvOS, and watchOS apps. They share a ShowPickerCore Swift package (at the repo root) and are opened via the ShowPickerClub.xcworkspace workspace. iOS and tvOS ship as a single universal App Store app (one bundle id, net.patrickturner.showpickerios, running on iPhone + Apple TV).
- Multi-tenant. One deployment, many members. Each member is a slug (
/whitt,/patrick) with their own lists; they sign in with a one-time code (text or email) or Sign in with Apple. - Auto-enriched. OMDB supplies IMDB ratings and canonical titles; TMDB supplies cast, next-season dates, finale dates, series-ended flags, and genres.
- Social. Suggest a show to another member and share a show across lists.
- Vibe.
/vibeprofiles each member's taste across 27 trait dimensions and assigns one of seven cluster identities. - Calendar feed.
webcal://showpicker.club/calendar/<slug>.icskeeps upcoming premieres and finales in Apple Calendar / Google Calendar / Fantastical. - PWA. Installable to home screen.
- Frontend: Static HTML + vanilla JS, no build step. Service worker for PWA support.
- API: Cloudflare Pages Functions (file-system-routed JavaScript handlers).
- Database: Cloudflare D1 (SQLite at the edge).
- Enrichment: OMDB API + TMDB API.
- Vibe trait scoring: Claude API (Sonnet 4.6 with prompt caching), admin-triggered batch only.
- Auth: One-time codes (SMS via Twilio Verify, email via Resend) plus Sign in with Apple; HttpOnly session cookies, 30-day expiry.
Only public/ (static assets) and functions/ (Pages Functions) are deployed. Everything else — schema.sql, docs/, workflow files, backups — stays out of the build output dir so it can't be served.
├── public/ Deployed static assets
│ ├── index.html Landing + per-member SPA
│ ├── vibe.html Member taste profiles
│ ├── reporting.html Admin metrics (auth-gated)
│ ├── setup.html Admin: create new member (secret-gated)
│ ├── url-cleanup.html Admin: fix missing network URLs (secret-gated)
│ ├── vibe-admin.html Admin: batch-score trait vectors (secret-gated)
│ ├── manifest.json PWA manifest
│ ├── sw.js Service worker
│ ├── _headers Security headers (CSP, HSTS, etc.)
│ └── _redirects SPA fallback + legacy slug rewrites
├── functions/
│ ├── api/ All /api/* endpoints
│ ├── auth/ Login, logout, session check
│ ├── calendar/[slug].js Per-member iCalendar feed
│ └── _shared/ Reusable helpers (auth, enrichment, vibe traits, vibe clusters)
├── docs/ Product + architecture documentation
├── schema.sql Starter DB schema (not deployed)
├── wrangler.toml Cloudflare config
└── .github/workflows/
├── deploy.yml Push-to-main → Cloudflare Pages
└── backup.yml Daily D1 dump → Google Drive
Routing is documented in docs/ARCHITECTURE.md.
- A Cloudflare account
- Wrangler CLI
- A free OMDB API key
- A free TMDB API key
- (Optional, for vibe trait scoring) an Anthropic API key
-
Clone and create the D1 database.
git clone https://github.com/turnepf/Shows.git cd Shows wrangler d1 create shows-dbCopy the returned
database_idintowrangler.toml. -
Apply the schema. Note that
schema.sqlis the starter shape; columns and tables added over time (added_by,enriched_at,genres,sessions.last_seen_at, etc.) are documented indocs/ARCHITECTURE.md. New deployments should apply the schema and then any subsequent ALTERs from that doc.wrangler d1 execute shows-db --remote --file=schema.sql
-
Seed at least one member + a contact for login. New members are usually created via the admin
/setuppage once you're deployed; bootstrap by hand the first time. Login is by one-time code, so seed an email and/or phone the member will receive the code at.INSERT INTO members (slug, name, first_name) VALUES ('patrick', 'Patrick Turner', 'Patrick'); INSERT INTO member_emails (email, member_slug, is_primary) VALUES ('patrick@example.com', 'patrick', 1); INSERT INTO member_phones (phone, member_slug, is_primary) VALUES ('+13365550123', 'patrick', 1);
-
Set API key secrets. Use
printf(notecho) so trailing newlines don't break runtime calls.printf "your-omdb-key" | wrangler pages secret put OMDB_API_KEY --project-name shows printf "your-tmdb-key" | wrangler pages secret put TMDB_API_KEY --project-name shows printf "your-tmdb-token" | wrangler pages secret put TMDB_TOKEN --project-name shows # Optional — only if you wire up the corresponding features: printf "sk-ant-..." | wrangler pages secret put ANTHROPIC_API_KEY --project-name shows # /vibe-admin trait scoring printf "your-watchmode" | wrangler pages secret put WATCHMODE_API_KEY --project-name shows # auto URL deep-links printf "ACxxxx" | wrangler pages secret put TWILIO_ACCOUNT_SID --project-name shows # SMS printf "your-twilio-tok" | wrangler pages secret put TWILIO_AUTH_TOKEN --project-name shows printf "+1336..." | wrangler pages secret put TWILIO_PHONE_NUMBER --project-name shows printf "MGxxxx" | wrangler pages secret put TWILIO_MESSAGING_SERVICE_SID --project-name shows
Watchmode deep-link lookups default to the US region. To target another country, set the non-secret
WATCHMODE_REGIONvar (e.g.GB,CA) in the Cloudflare Pages dashboard → Settings → Environment variables.Reviewer / demo login (optional). The app is invite-only with no public sign-up, so App Review needs a way in. Create a throwaway demo member with an email (in
member_emails), then set these two secrets to let that one email log in with a fixed code — no SMS/email round-trip needed:printf "demo@example.com" | wrangler pages secret put DEMO_LOGIN_EMAIL --project-name shows printf "424242" | wrangler pages secret put DEMO_LOGIN_CODE --project-name shows # 6 digits → iOS auto-submits
The bypass (in
functions/auth/login.js) only ever unlocks the configured email and only when both secrets are set; leave them unset to disable it. Put the same email + code in App Store Connect → App Review Information. -
Create the Pages project and do the first deploy.
wrangler pages project create shows wrangler pages deploy public --project-name shows
pages_build_output_dir = "public"inwrangler.tomlensures onlypublic/is uploaded. Functions at the repo root are picked up automatically. Never pass.as the deploy directory — it would publish secrets, backups, and.envfiles. -
(Optional) Add a custom domain via the Cloudflare dashboard → Pages → your project → Custom domains.
- Production deploy is automatic on push to
mainvia.github/workflows/deploy.yml. The workflow runswrangler pages deploy, then probes a few endpoints to confirm secrets aren't leaking and auth gates are still in place. - Daily D1 backup at 03:00 UTC via
.github/workflows/backup.yml. Dumps the full SQL to Google Drive (gdrive:Shows-Backups/), prunes backups older than 30 days, and prunesfailed_loginsrows older than 7 days from D1.
Both workflows require these GitHub Actions secrets:
| Secret | Used by | Purpose |
|---|---|---|
CLOUDFLARE_API_TOKEN |
deploy.yml, backup.yml | Pages:Edit + D1:Edit on the account |
CLOUDFLARE_ACCOUNT_ID |
deploy.yml, backup.yml | The account ID |
RCLONE_CONF |
backup.yml | Full ~/.config/rclone/rclone.conf with gdrive token |
MIT