A subscription streaming home for original short-form stories. 60-second anonymous trial per (browser session, show), then sign-up + Stripe Checkout in one flow.
Production: https://matio.tv
- Next.js 16 App Router · TypeScript · React 19
- Postgres on Neon · Drizzle ORM (
postgres-jsdriver, pooled endpoint) - Clerk 7 (auth, keyless in dev)
- Stripe 23 (Checkout + Customer Portal + webhooks)
- Mux 15 (direct upload + RS256-signed playback)
- Tailwind v4 · shadcn (built on Base UI)
- Vercel hosting
pnpm install
cp .env.example .env.local # fill in real values
pnpm db:migrate # apply schema to your Neon branch
pnpm stripe:setup # create Stripe products + prices
pnpm seed:fake-shows # optional: 35 placeholder shows for layout testing
pnpm devOpen http://localhost:3000.
AGENTS.md lists every env var and where to obtain it; .env.example
mirrors the canonical names.
Read these before changing integrations:
- docs/architecture.md — system diagram, data model, trial pipeline, playback pipeline, route protection, why each decision was made
- docs/services.md — per-service setup (Clerk, Stripe, Mux, Neon, Vercel) and env-var sources
- docs/operations.md — pnpm scripts, migrations, deploy commands, end-to-end test recipes
- docs/gotchas.md — version-specific traps for Next 16, Clerk 7, Stripe SDK 23 (API 2024+), Mux 15, Tailwind v4 + shadcn-on-Base-UI, Drizzle 0.45
CLAUDE.md summarises the rules and conventions agents
follow when working in this repo.
| Command | Purpose |
|---|---|
pnpm dev |
Next dev server with Turbopack |
pnpm typecheck |
tsc --noEmit |
pnpm lint |
ESLint |
pnpm db:generate |
Diff schema → write a new Drizzle migration |
pnpm db:migrate |
Apply pending migrations to DATABASE_URL |
pnpm db:studio |
Drizzle Studio (browser DB GUI) |
pnpm stripe:setup |
Idempotently create Stripe products + prices |
pnpm seed:fake-shows |
Insert 35 demo shows (slug prefix demo-) |
pnpm seed:fake-shows -- --playback-ids=<mux id>[,<id>:<seconds>] |
…and give one demo show a season of playable episodes (signed playback). Ids are arguments, never committed |
pnpm seed:fake-shows -- --reset |
Delete every demo-* show (seasons and episodes cascade) |
pnpm promote-to-admin <email> |
Grant users.role='admin' |
Two steps, and only the second one reaches viewers:
- Merging to
maindeploys staging — a separate Vercel project with its own Neon branch, its own keys and seeded data. - Production (
matio.tv) deploys only from a published GitHub Release..github/workflows/deploy-production.ymlruns in theproductionGitHub Environment, so the job waits for the owner to approve before anything reaches the live site.
Migrations run separately against DATABASE_URL, staging first:
pnpm db:migrateAlways migrate before deploying when a release adds new columns/tables; dropping a column happens only after the deployment without it is live.
The full map — emergency manual deploy, rollback, staging rules — is in the
/devops skill (.claude/skills/devops/SKILL.md).
app/
(public)/ Catalog: / and /shows/[slug]
admin/ Admin panel: shows, episodes, analytics
api/
billing-portal/ Direct redirect to Stripe Customer Portal
playback-token/ Mux RS256 JWT issuer (trial + subscriber)
webhooks/ Clerk / Mux / Stripe webhooks
subscribe/ Checkout form
watch/[showSlug]/ Player + trial paywall
components/
ui/ shadcn primitives (Base UI under the hood)
admin/ Admin-specific (upload widget, status select)
site/ Marketing surfaces (header, footer, posters)
watch/ Player, paywall, overlays
db/ Drizzle client + schemas
drizzle/ Migrations (`pnpm db:migrate`)
lib/ Server-only helpers (auth, trial, Mux, Stripe)
proxy.ts Next 16 middleware (auth gating + role cache)
scripts/ One-off CLI tasks