xtravaganzamusical.com · system architecture
A public, world-reachable landing page with a double opt-in email list for a Broadway production — engineered to survive a press spike without paging its sole operator, cheap to run, and built on a React + Node + Express + TypeScript stack fronted entirely by Cloudflare.
Built to be found in 2026 — by Google and by the AI answer engines that increasingly sit between people and the web. Good crawlers are welcomed and fed clean structured data; malicious ones are turned away at the edge.
<title> + meta description, rel=canonical, and robots: index, follow, max-image-preview:large, max-snippet:-1 — explicitly inviting full snippets and large image previews.summary_large_image (1500×375 banner.jpg) so shares on iMessage, Slack, X, Facebook, and LinkedIn render a rich card.TheaterEvent, server-prerendered into the homepage <head> and nonce-injected at build (CSP-safe). Current: models the engagement as one event — run dates May 7 – Jun 13, 2027, eventStatus: Scheduled, eventAttendanceMode: Offline, venue (PerformingArtsTheater + address), creative team (Person + role), producers, and a ticket Offer → Baltimore Center Stage. Already eligible for Google event rich results, event listings, and knowledge panels.subEvent showtimes with date-time + timezone, and a priced Offer with price / validFrom. Tracked deliberately — the run is currently a date range, not individual showtimes.robots.txt allows all by default and explicitly allowlists the AI/LLM fleet: GPTBot · OAI-SearchBot · ChatGPT-User · ClaudeBot · Claude-SearchBot · PerplexityBot · Google-Extended · Applebot-Extended · CCBot · meta-externalagent · Amazonbot · Bytespider · YouBot · Diffbot — alongside Googlebot/Bingbot. References the sitemap.llms.txtsitemap.xml (weekly, priority 1.0). robots.txt / sitemap.xml / llms.txt are cached 24h for crawler re-checks; HTML is served max-age=0, must-revalidate so content edits surface immediately.One language end to end (TypeScript), one package manager (pnpm workspaces), and a deliberately small surface: a static frontend, a single long-running API, and one Postgres table. No Next.js, no Vercel, no Prisma.
Frontend — apps/web
Backend — apps/api
Shared & data — packages/shared
subscribers table
Build & delivery
Two vendors do the heavy lifting — Cloudflare for everything at the edge, Railway for compute and data — with focused third parties for email, monitoring, and source.
Proxied DNS, Pages hosting, global CDN + DDoS, WAF, Access (Zero Trust), R2 object storage, and Web Analytics (RUM beacon). The single public front door.
Always-hot Express container (Pro) and the managed Postgres plugin. Auto-deploys from main.
Transactional sends (Pro, 50k/mo, unlimited daily) and Audiences/Broadcasts for marketing. React Email templates.
Repository, Actions CI on every push, and the nightly backup workflow — dispatched by the API's in-app scheduler (the digest + backup timing moved in-process after GitHub's schedule: cron proved unreliable).
External synthetic monitoring of the web root and the API health endpoint, with email alerting.
Frontend + backend error tracking is planned but not yet wired in; errors currently surface via structured pino logs on Railway.
Embedded audio engine for the opening-number player, sandboxed via CSP frame-src + delegated Permissions-Policy.
Not yet confirmed. Inbound MX may be owned by M365 via a separate IT resource; if so, Resend outbound coexists through a merged SPF record + independent DKIM selectors. Pending confirmation of who runs inbound mail.
The running pieces and what each one is responsible for.
The marketing page + signup form + /confirm route. Pure static assets on Pages; a strict CSP and a per-build script nonce.
Subscribe, confirm, unsubscribe, health, CSP-report, and the Resend webhook. RFC 7807 problem+json errors; no stack traces in prod.
/admin UI + /v1/admin/* — subscriber search, a paginated & sortable table, a 7-day site-traffic panel, CSV export, and a manual digest trigger. Server-rendered (no client JS); gated by Cloudflare Access and re-verified in-app.
Two Postgres tables: subscribers (status lifecycle) and digest_runs (a watermark so the daily digest never skips or double-counts). Verification tokens are stateless HMACs — no token table to clean up.
The API's in-app scheduler (scheduler.ts) runs the digest at 14:00 UTC — new confirmed signups (windowed on confirmed_at, watermarked so none are missed), a 7-day traffic panel, and a CSV, emailed to the producers.
The scheduler dispatches a GitHub workflow at 04:00 UTC: pg_dump → AES-256 → Cloudflare R2, with a heartbeat ping on success (dead-man's switch). Gated on BACKUPS_ENABLED.
Defense in depth: every request crosses the Cloudflare edge first, the API re-checks what the edge asserts, and secrets fail the boot if they're wrong.
/.git, /.env, /wp-login.php, /.terraform…) with a 403. Plus IP and ASN access rules (a known bulletproof scanner network is banned)./admin + /v1/admin/* — email one-time-PIN allowlist for humans, a service token for programmatic access. The API independently verifies the Access JWT (issuer + AUD) and accepts a bearer ADMIN_API_TOKEN — two locks, not one./assets, and every other asset directory (/media, /fonts, /partners, /team, the style-map captures) follows the same rule by convention — a content change always takes a new filename (-a2, -sx1, -2…), never a reuse. So all of them serve max-age=31536000, immutable: repeat visits pull the 9MB hero loop, the poster, and the photography from disk, not the network. HTML stays max-age=0, must-revalidate. (Range-served video barely caches under must-revalidate — 206 partials skip the HTTP cache — which is why the loop was re-fetched every visit before this rule.)unsafe-inline scripts), X-Frame-Options: DENY, X-Content-Type-Options, Referrer-Policy, and a scoped Permissions-Policy. CSP violations report to /v1/csp-report.CF-Connecting-IP (survives an edge misconfig). CSV export is hardened against spreadsheet formula injection. A server-side Origin allowlist guards /v1/subscribe + /v1/confirm beyond the edge WAF, and confirm is single-use (idempotent — a replayed token re-sends nothing).VERIFY_TOKEN_SECRET — verify tokens expire in 48h, unsubscribe tokens don't expire. Raw source IPs are never persisted; the schema reserves an ip_hash column for optional HMAC-hashed abuse triage..env.production → dashboard secrets → gitignored .env.local). zod hard-fails the boot in prod if a base URL is http/localhost or a key is missing. Postgres superuser password rotated.Most traffic never touches the origin — Cloudflare serves the static show from the edge — and the parts that do are watched.
/ (200) and /v1/healthz (asserts $.ok == true) every 5 min and emails on failure. Cloudflare zone analytics + a RUM beacon cover traffic (surfaced in the producer digest and /admin); errors surface via pino logs on Railway today (Sentry planned).?filmdebug=1 to any page and a self-contained overlay reports the hero film’s pipeline live — src attached, readyState/networkState, paused/time/buffered, media errors, the last play() rejection by name (NotAllowedError = Low Power Mode / autoplay policy), Save-Data, reduced-motion, recent JS errors, UA. Built for the “works on my laptop, dead on the designer’s phone” loop: one screenshot of the box replaces a tethered Web Inspector session. CSP-safe (no external scripts).Delivery already scales to a worldwide audience for free; the only paths that meter are the API write and the email send, both of which have wide headroom.
The subscriber list is the irreplaceable asset, so it has an understudy off-site, encrypted, and rehearsable.
pg_dump (Postgres 18, absolute path) → AES-256-encrypted (openssl, pbkdf2) → Cloudflare R2. Gated on the BACKUPS_ENABLED repo variable.BACKUP_ENCRYPTION_KEY is held separately from the R2 access keys — a leaked R2 read token can't decrypt subscriber emails.infra/db/restore-drill.sh pulls a dump, decrypts, restores into a fresh Postgres, and validates row counts. Run and verified before launch; re-run quarterly. RTO target < 2h.A roll-your-own double opt-in list on top of Resend — consent-correct, deliverability-aware, and ready to graduate into broadcasts without a second opt-in.
subscribe (status pending) → HMAC verify email → confirm (status confirmed) → welcome email. Statuses: pending · confirmed · unsubscribed · bounced.List-Unsubscribe header arrives with the planned Resend Broadcasts path.confirmed_at so late confirmers are never dropped).confirmed rows into a Resend Audience for announcements — no re-opt-in (the double opt-in is the consent record), with an unsubscribe webhook keeping both sides in sync.p=quarantine → tighten to p=reject after clean reports.