← All brand pages

xtravaganzamusical.com · system architecture

XTRAVAGANZAMUSICAL.comArchitecture

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.

LIVE in production web xtravaganzamusical.com api api.xtravaganzamusical.com venue Baltimore Center Stage · May 2027 monorepo pnpm workspaces

Request & data flow

XTRAVAGANZA system architecture diagram Visitors reach the Cloudflare edge, which serves the static Cloudflare Pages site and proxies the API to the Express service on Railway, which talks to Postgres and Resend. GitHub Actions runs CI and crons; nightly Postgres dumps go to Cloudflare R2; UptimeRobot probes the health endpoint; Sentry error tracking is planned. Worldwide audience browsers · phones · press HTTPS Cloudflare edge proxied DNS · the only public front door Global CDN WAF DDoS Rate limit Access (Zero Trust) IP / ASN block cached static /v1/* · /admin Cloudflare Pages React 18 · Vite SSG · static strict CSP · HSTS · _headers Railway — Express API Node 20 · TypeScript · Drizzle · zod single region · always-hot SQL (pool 10) email API Railway Postgres subscribers table HMAC tokens · no raw IPs Resend transactional (Pro 50k/mo) + audiences / broadcasts OPERATIONS & AUTOMATION GitHub Actions CI: typecheck · lint · test + nightly backup workflow Cloudflare R2 nightly pg_dump AES-256 encrypted UptimeRobot 5-min checks · email alert web 200 · api ok:true Sentry error tracking (planned) in-app scheduler → dispatches backup workflow (04:00) /v1/healthz
control / compute plane static & managed services / data request & data flow

Discoverability — SEO, LLMs & bots

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.

On-page SEO
Descriptive <title> + meta description, rel=canonical, and robots: index, follow, max-image-preview:large, max-snippet:-1 — explicitly inviting full snippets and large image previews.
Social cards
Full Open Graph + Twitter summary_large_image (1500×375 banner.jpg) so shares on iMessage, Slack, X, Facebook, and LinkedIn render a rich card.
Structured data
JSON-LD 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.

Planned enrichment (awaiting the production calendar & on-sale data): per-performance 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.
Crawl directives
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.txt
The 2026 convention: a hand-written markdown brief (quick facts, creative team, synopsis, ticket link) that AI search engines read directly — so answers about the show are accurate and on-message rather than scraped guesses.
Sitemap & freshness
sitemap.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.
Bot policy at the edge
Cloudflare AI Crawl Control set to allow crawlers (discoverability over gatekeeping), while the WAF 403s malicious scanners (recon paths + banned ASNs). Good bots in, bad bots out — the directives and the firewall pull the same direction.

Tech stack

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

React 18TypeScriptVite Tailwind v4SSG prerenderReact Email → Cloudflare Pages

Backend — apps/api

Node 20Express 4TypeScript Drizzle ORMzod (boot + I/O)pino logs jose (JWT)→ Railway

Shared & data — packages/shared

zod schemasHMAC token helpers Postgres 18single subscribers table

Build & delivery

pnpm workspacesTurboGitHub Actions CI Dockerfile / Nixpacksmigrations on container start

Providers

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.

edge · dns · security

Cloudflare

Proxied DNS, Pages hosting, global CDN + DDoS, WAF, Access (Zero Trust), R2 object storage, and Web Analytics (RUM beacon). The single public front door.

compute · database

Railway

Always-hot Express container (Pro) and the managed Postgres plugin. Auto-deploys from main.

email

Resend

Transactional sends (Pro, 50k/mo, unlimited daily) and Audiences/Broadcasts for marketing. React Email templates.

source · ci/cd · backups

GitHub

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).

uptime

UptimeRobot

External synthetic monitoring of the web root and the API health endpoint, with email alerting.

errors · planned

Sentry — planned

Frontend + backend error tracking is planned but not yet wired in; errors currently surface via structured pino logs on Railway.

media

SoundCloud

Embedded audio engine for the opening-number player, sandboxed via CSP frame-src + delegated Permissions-Policy.

inbound mail · TBD

Microsoft 365 — TBD

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.

Services

The running pieces and what each one is responsible for.

public · static

Web

The marketing page + signup form + /confirm route. Pure static assets on Pages; a strict CSP and a per-build script nonce.

public · /v1/*

API

Subscribe, confirm, unsubscribe, health, CSP-report, and the Resend webhook. RFC 7807 problem+json errors; no stack traces in prod.

private · behind Access

Admin

/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.

data

Database

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.

scheduled · in-app

Daily digest

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.

scheduled · in-app

Nightly backup

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.

Security

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.

Edge WAF
Custom rule blocks recon/secret-scanner paths (/.git, /.env, /wp-login.php, /.terraform…) with a 403. Plus IP and ASN access rules (a known bulletproof scanner network is banned).
Admin access
Cloudflare Access (Zero Trust) fronts /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.
Caching
One contract, two enforcers: Vite fingerprints /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.)
Headers
HSTS (preload), strict CSP with a per-build nonce (no 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.
Input & abuse
Every input validated with zod. Role-address rejection, per-email cooldown, a hidden honeypot field, and app-level rate limiting keyed on 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).
Tokens & PII
Stateless HMAC tokens signed with 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.
Secrets
Layered env (committed non-secret .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.

Reliability & uptime

Most traffic never touches the origin — Cloudflare serves the static show from the edge — and the parts that do are watched.

Edge absorption
Static site cached globally on Cloudflare; a Reddit/HN spike is served from the edge. "Under Attack Mode" is a one-toggle emergency brake.
Always-hot API
Railway Pro keeps the Express container warm (no cold start), with a healthcheck path and restart policy.
Monitoring
UptimeRobot probes / (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).
Field diagnostics
Append ?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).
Heartbeats
The scheduled digest and nightly backup each ping an UptimeRobot heartbeat on success — a dead-man's switch that alerts if a run is silently missed, not just if a probe fails.
Fewer moving parts
Stateless tokens mean no token table to corrupt; structured pino logs stream to Railway; errors return problem+json, never a stack trace.

Scalability

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.

Frontend
Cloudflare Pages = global CDN, unlimited bandwidth. 100 or 1,000,000 visitors load the page from the edge at $0. Scaling reach needs no plan change.
API
Stateless Express — horizontally scalable on Railway. Signup-only write workload; Postgres pool capped at 10 (one always-hot instance, so no connection proxy needed yet). Migration path if single-region is outgrown: Fly.io or ECS, not a rewrite.
Email
Resend Pro: 50k/mo transactional, unlimited daily — a press spike won't get throttled. ~25k signups/month of headroom before the monthly cap (≈2 emails per signup).
Cost
Runs on paid tiers for headroom — Railway Pro + Resend Pro + UptimeRobot (paid), Cloudflare free — on the order of $45–55/mo, and largely flat as the audience grows because Cloudflare absorbs traffic spikes at $0. Abuse is bounded by the WAF + app rate limits.

Backups

The subscriber list is the irreplaceable asset, so it has an understudy off-site, encrypted, and rehearsable.

Nightly dump
GitHub Actions runs pg_dump (Postgres 18, absolute path) → AES-256-encrypted (openssl, pbkdf2) → Cloudflare R2. Gated on the BACKUPS_ENABLED repo variable.
Key separation
BACKUP_ENCRYPTION_KEY is held separately from the R2 access keys — a leaked R2 read token can't decrypt subscriber emails.
Restore drill
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.
Secondary
Railway's automatic daily snapshots back the off-site dumps as a second line.

List management

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.

Double opt-in
subscribe (status pending) → HMAC verify email → confirm (status confirmed) → welcome email. Statuses: pending · confirmed · unsubscribed · bounced.
Unsubscribe
A signed one-click unsubscribe link in the email, with a server-rendered result page (CAN-SPAM / CASL). The one-click List-Unsubscribe header arrives with the planned Resend Broadcasts path.
Reputation
Resend bounce/complaint webhook (svix-signed) flips rows to bounced/unsubscribed — handled on day one so a flood can't poison sender reputation.
Producer digest
Daily email of new confirmed signups + a full CSV to the producer recipients, sent by the API's in-app scheduler at 14:00 UTC (watermarked on confirmed_at so late confirmers are never dropped).
Broadcasts (planned)
Sync 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.
Sender domain
Resend DKIM selectors + an SPF record (merged with whatever owns inbound mail — TBD), DMARC at p=quarantine → tighten to p=reject after clean reports.