Release model
- Pull requests must pass two independent gates: GitHub Actions runs
npm ci+npm test, while Cloudflare Workers Builds uploads a non-production preview version - Production auto-deploys from
mainvia Cloudflare Workers Builds; a manualwrangler deployis a break-glass path, not the normal release workflow - No build step; worker and static assets are deployed as-is
- Custom domain:
pub.ihnyc-rc.org - Admin UI at
/admin/*behind Cloudflare Access (Zero Trust)
Prerequisites
- Node.js v18+
- Wrangler CLI (
npm install -g wrangler && wrangler login) - Access to the Cloudflare account that owns the worker
Secrets and variables
Required secrets
Set once per environment via wrangler secret put <NAME>:
| Secret | Purpose |
|---|---|
PUB_ADMIN_TOKEN | Admin API auth token (also gates Short.io QR sync). Renamed from TENDER_QR_SYNC_TOKEN; the worker reads PUB_ADMIN_TOKEN. |
NOW_PLAYING_INGEST_TOKEN | Bearer token for Pi song-ID pipeline |
CF_ACCESS_POLICY_API_TOKEN | Used by the Access page to update the dedicated delegated-operator policy |
Optional secrets
| Secret | Purpose |
|---|---|
NOTION_API_KEY | Notion integration token (events source, still used for events reads) |
SHORT_IO_API_KEY | Short.io API for tender QR code short links |
| Provider and relay credentials | Optional integrations for Notion, Short.io, email, Q-SYS, lighting, and TV control |
Non-secret vars in wrangler.toml
| Var | Purpose |
|---|---|
TV_BUILD_ID | Bump on every user-facing change; triggers TV self-reload |
PUBLIC_SITE_URL | https://pub.ihnyc-rc.org |
CF_ACCESS_TEAM_DOMAIN, CF_ACCESS_AUD | JWT issuer and audience used for every delegated Pub operator |
| Access policy identifiers | Dedicated delegated-operator policy bridge; public identifiers, never the API token |
NOTION_DATABASE_ID | Events DB |
NOTION_ANNOUNCEMENTS_DATABASE_ID | Announcements DB (legacy, being migrated to D1) |
NOTION_PUB_TENDERS_DATABASE_ID | Pub Tenders DB (legacy, tenders now in D1) |
SHORT_IO_DOMAIN | Domain for Short.io QR links |
SETLIST_LEAD_SURFACE_ENABLED | Feature flag for Top Songs surface |
SETLIST_VIBE_COMPASS_ENABLED | Feature flag for Vibe Compass |
SETLIST_PERSONA_ENABLED | Feature flag for Persona AI surface |
SETLIST_STORIES_ENABLED | Feature flag for Stories AI surface |
SPORTS_ENABLED | Live sports schedule (keyless ESPN) → /api/sports/live + cron refresh of the sports KV cache |
NOW_SHOWING_ENABLED | ”Now Showing” fusion (/api/now-showing) — fuses the Pi content verdict with the ESPN schedule to name a live game |
SCREENINGS_ENABLED | Movie-night auto “Now Showing” + /screen archive. Values: off | shadow | on |
FEEDBACK_ENABLED | Patron feedback / suggestion box (/feedback + /api/feedback); admin read at /admin/feedback is always on |
PUB_SCHEDULER_V2 | Pub-scheduler run state-machine engine. Dormant — unset until the v2 cutover; flipping it activates the new transition engine |
Bindings in wrangler.toml
| Binding | Type | Purpose |
|---|---|---|
CONTENT_DB | D1 | Pub scheduler data (tenders, shifts, assignments, submissions, audit) |
SETLIST_DB | D1 | Song-ID data, setlist analytics, theme presets, API health |
IMAGE_MIRROR_BUCKET | R2 | Image mirror for TV/announcement/tender images |
EVENT_MEDIA_BUCKET | R2 | Event slideshow media |
TUTORIALS_DATA | KV | Admin-edited tutorial content blobs |
TV_PAIRING | Durable Object | TvPairingCoordinator for slot state |
DJ_MODE | Durable Object | DJ Mode queue state |
AI | Workers AI | Persona/Stories AI inference |
D1 migrations
Migrations are plain SQL files applied manually. Two databases:
CONTENT_DB (pub scheduler)
wrangler d1 execute CONTENT_DB --remote --file=./migrations/content/<file>.sqlLatest numbered migration: 0053_pub_supervisors_fairness_report.sql. Recent additions include email-log attachments (0052) and the per-supervisor fairness-report setting (0053).
SETLIST_DB (song-ID, analytics, theme, API health)
wrangler d1 execute SETLIST_DB --remote --file=./migrations/<file>.sqlLatest: 0061_pub_qsys_snapshots.sql. Migrations 0053–0061 add quiet-hours state, TV control, scoped Pub roles, delegated Access sync, Q-SYS control discovery and curation, Spotify Jam scheduling, DMX command results, and audio snapshots. Two different 0058 files exist; use the repository’s recorded migration order rather than assuming the number is unique.
Always apply migrations before deploying code that depends on new tables/columns. Check CLAUDE.md in the repo for the full ordered migration list.
Deploy
Production deploys automatically when a change lands on main via Cloudflare Workers Builds. Before merge, the GitHub Actions test check and Cloudflare preview build must both pass. GitHub Actions is test-only and has no deploy credentials; Cloudflare owns preview uploads and the production deploy.
Because main can go live immediately after merge, required D1 migrations must either be applied before merging or remain backward-compatible with both the old and new Worker. The canonical release, verification, and rollback procedure lives in the Pub repository’s docs/DEPLOYMENT.md.
Bump TV_BUILD_ID in wrangler.toml (in the same change) if TV-facing assets changed; TVs self-reload within 60 seconds when it changes.
A manual wrangler deploy is reserved for break-glass/one-off situations and must be recorded in the PR or incident — merging a green PR to main is the normal path.
Local development
# Create .dev.vars with local secrets
echo "PUB_ADMIN_TOKEN=<token>" > .dev.vars
# Start local dev server
wrangler devLocal dev serves at http://localhost:8787. Workers AI (env.AI) is not available in local mode; use wrangler dev --remote for AI surface testing (bills Neurons).
Monitoring
| Signal | Where to look |
|---|---|
| Worker errors | Cloudflare Dashboard > Workers & Pages > Logs |
| TV health | /admin/tv-health — live heartbeat, build match, memory, JS errors per screen |
| API dependencies | /admin/api-health — Notion, LRCLib, Short.io, Deezer, Formbricks, Workers AI, ESPN (keyless live-sports scoreboard), TMDB (movie posters, keyless Deezer fallback) |
| Scheduler state | /admin/pub-schedule — current week’s wizard step and submission feed |
| Email delivery | /admin/pub-setup > Email Log |
| Song-ID pipeline | ssh pi@<pi-tailnet-hostname> 'systemctl is-active ptp4l-eth0 aes67-daemon pi-capture' |
| Setlist rollups | Check /api/setlist/<surface> for fresh computedAt timestamps |
TV build version
TV screens self-reload when TV_BUILD_ID changes. Bump it on every deploy that touches public/tv/, public/bar-tv-right/, or any TV-facing asset:
TV_BUILD_ID = "YYYY-MM-DD-NN" # e.g. 2026-06-07-01The worker injects this into TV HTML as window.__TV_BUILD_ID__. TVs poll /api/tv-build every 60 seconds and reload when the value changes.
Cloudflare Access setup
The admin UI is gated by a Pub-specific Cloudflare Access application. It is not an RC Console application.
- Full administrators remain on the standing full-admin policy and hold
*. - Delegated operators are kept in a dedicated email-only policy managed from
/admin/access. - The Worker verifies the Access JWT, then reads scoped permissions from
pub_admin_roles. - Removing an operator revokes Pub permissions and then removes the delegated Access entry.
Add a delegated operator in /admin/access, not by editing RC Console or hand-editing the main Access policy. If the policy sync fails after the D1 role changes, the Access page shows the pending retry.
Troubleshooting
TVs show “Some TVs need attention”
The hub shows amber only for genuinely missing TVs (no heartbeat for 30+ minutes). If a TV is borderline stale (5-30 min), the full TV Health page shows details. Common causes:
- TV went to sleep or lost network
- Cloudflare Workers had a brief outage
- TV browser crashed (check JS errors on the health card)
If the slot is an intentionally parked QA device, open /admin/slots and turn off Monitor for that slot. Its real state remains visible, but it no longer contributes to warning counts or offline-alert email.
Scheduler email not sending
- Check the This Week wizard — is Step 4 (Send) active?
- Confirm all shifts are Approved (Draft shifts block sending)
- Check
/admin/pub-setup> Email Log for send failures - Verify
PUB_ADMIN_TOKENis set as a Cloudflare secret
Setlist surfaces show stale data
The cron dispatcher picks ONE due surface per tick. If a surface hasn’t updated:
- Check its
surface_config.enabled = 1in SETLIST_DB - Check its cadence (e.g., Persona/Stories run weekly, Top Songs runs hourly)
- Check
/admin/api-health— Deezer degraded blocks Vibe Compass enrichment
Tutorial edits not saving
- Verify
TUTORIALS_DATAKV namespace is bound inwrangler.toml - Check the Access session and the operator’s
tutorials.editpermission - Preview a history version before restoring it
- Check browser console for 401/403/503 errors
Audio controls are missing or locked
- Confirm the Pi and Q-SYS Core are reachable on the System page.
- Refresh discovery on
/admin/audio. - Confirm the control is writable and included in an operator group.
- Check the group and control lock state.
- Capture a snapshot before changing several controls.
Song-ID pipeline (Pi-side)
The pipeline runs on a Raspberry Pi reached through its approved private tailnet hostname. Three systemd services:
| Service | What |
|---|---|
ptp4l-eth0.service | PTP slave syncing to Q-SYS Core |
aes67-daemon.service | RAVENNA kernel module + ALSA capture |
pi-capture.service | Fingerprint + POST to worker |
All enabled on boot with dependency ordering. See pi-capture-ops for the full runbook.