Skip to content

Instantly share code, notes, and snippets.

@rafaelquintanilha
Created March 29, 2026 16:00
Show Gist options
  • Select an option

  • Save rafaelquintanilha/0c29ebadc33dfd5bdd686df3f4e93d4b to your computer and use it in GitHub Desktop.

Select an option

Save rafaelquintanilha/0c29ebadc33dfd5bdd686df3f4e93d4b to your computer and use it in GitHub Desktop.
Programe.ai final spec

Code Capital — Unified PRD (Web + API)

Repo branch: master (production)
Monorepo: Turborepo + pnpm
Type system: End-to-end TypeScript (shared schemas + DTOs)
Analytics: PostHog (from day 1)
Payments: Stripe Checkout (hosted)
Video: Cloudflare Stream (signed playback)
Backend: Hono on Cloudflare Workers + D1 (SQLite)
Frontend: Next.js on Vercel (SSR/SEO UI only; no Next Route Handlers)


0) Progress Tracker (keep updated)

  • Last completed milestone: M8.1–M8.8 Stripe Checkout integration ✅ (M8.9 deferred to M9)
  • Next up: M9 Analytics (PostHog end-to-end)
  • Latest update (2026-03-25): M8 completo (exceto M8.9). Seed com stripeProductId real do Stripe. POST /checkout/session cria Checkout Session via Stripe REST API (CF Workers, sem SDK Node). Webhook /webhooks/stripe verifica assinatura via Web Crypto API, cria purchase + purchase_items + access_grant com idempotência. BlockedOverlay funcional com botão de compra → redirect ao Stripe. CheckoutSuccessBanner exibe confirmação pós-compra e atualiza grants via refetchAccess(). packages/db/drizzle/seed.sql e o D1 local foram reconciliados com os stripe_product_id reais para destravar o QA manual de checkout.

1) Summary

Code Capital is a SEO-first course marketplace for Rafael Quintanilha (QuantBrasil / Code Capital Substack). Courses consist of pre-recorded lessons hosted on Cloudflare Stream. Lesson pages are public for SEO, but playback is gated via a “captive player”: a click on Play triggers login, then entitlement checks, then short-lived Stream token issuance.

MVP sells individual courses only. The codebase must be bundle-ready and subscription-pass ready without heavy refactors.


2) Goals (MVP)

  • Public, indexable course and lesson pages with strong SEO.
  • Captive playback funnel: Play click → login → (preview OR purchase) → watch.
  • Secure playback: Stream videos require signed tokens minted by API.
  • Stripe Checkout purchase flow for courses.
  • Access grants created via Stripe webhooks (source of truth).
  • PostHog captures key funnel events from day 1.
  • All code TypeScript, linted, and typechecked consistently.

Non-goals (MVP)

  • Bundles UI/checkout (but keep design extensible).
  • Free-pass subscription (but keep schema/checks extensible).
  • Livestreams.
  • Full admin dashboard (use seeds/import scripts).

3) Users & Permissions

Roles

  • Visitor: browse public pages, cannot watch.
  • Authenticated (free): can watch preview lessons.
  • Authenticated + Course Access: can watch all lessons in purchased course.
  • Admin (internal): manage catalog via scripts (MVP).

Playback rules (hard rules)

  • Playback always requires authentication.
  • Preview lesson: allow after login.
  • Locked lesson: allow only with access grant (or later pass).

4) Architecture (Monorepo)

code-capital/ apps/ web/ # Next.js (Vercel) - UI + SSR only api/ # Hono (Cloudflare Workers) - auth, catalog, playback tokens, webhooks packages/ shared/ # Zod schemas + DTOs + pure helpers (no secrets, no DB) db/ # Drizzle schema + migrations (used by api) turbo.json pnpm-workspace.yaml

TypeScript End-to-End Contract

  • packages/shared defines:
    • Zod schemas for domain models + DTOs
    • ApiError shape + error codes
    • pure helper: canWatchLesson(...) (API enforces, web uses for UI)
  • API validates input/output with Zod (runtime safety).
  • Web uses shared DTO types + (optional) typed API client wrapper.

5) Tech Stack (Implementation)

Frontend (apps/web)

  • Next.js (App Router)
  • shadcn/ui components (Tailwind + Radix primitives)
  • Calls API over HTTP (no Next backend/route handlers)

Backend (apps/api)

  • Hono (Cloudflare Workers)
  • Better Auth for OAuth (Google + GitHub in MVP)
  • Stripe SDK for Checkout + webhooks
  • Cloudflare Stream token minting
  • D1 via Drizzle

Tooling

  • pnpm
  • Turborepo pipelines: dev, lint, typecheck, test, build
  • ESLint across repo (single shared config)
  • TypeScript strict: true (exceptions must be explicit)

6) Product Requirements

6.1 Catalog (SEO-first)

Public pages (indexable HTML):

  • /cursos — list all published courses
  • /cursos/[courseSlug] — course detail + lesson list
  • /cursos/[courseSlug]/aulas/[lessonSlug] — lesson detail + captive player
  • /categorias/[categorySlug] — category SEO landing (MVP included)

Course includes

  • title, description, cover image
  • language (pt-BR initially)
  • category tags
  • published status
  • price display (from DB)
  • purchase CTA

Lesson includes

  • title, summary
  • order within course
  • preview flag (isPreview)
  • Stream UID
  • published status

6.2 Captive Playback Funnel

On lesson page:

  • show poster + “Play” button
  • on Play click:
    1. capture play_intent in PostHog
    2. if logged out → auth modal
    3. if logged in:
      • if preview → request Stream token → play
      • if locked → show “Buy course” modal

Key requirement: even preview lessons require login to play.


6.3 Authentication

MVP OAuth providers:

  • Google
  • GitHub

Session model:

  • cookie-based session on the API domain
  • web uses fetch(..., { credentials: "include" })

6.4 Payments (Stripe Checkout)

MVP: course-only purchases.

Stripe mapping simplification

  • Store only stripeProductId on the course record.
  • At checkout time, API retrieves Stripe Product and uses its default_price for Checkout Session line items.
    • Operational requirement: each sellable Course Product in Stripe must have default_price set.

Checkout flow

  1. Web calls API: POST /checkout/session { courseId }
  2. API creates Checkout Session and returns { url }
  3. Web redirects user to Stripe-hosted checkout URL
  4. Stripe webhook confirms payment → API creates access grants

Webhook is the source of truth for granting access.


6.5 Access Model (updated)

We separate purchase history from access grants.

  • Purchases: what was paid for (auditing / reconciliation).
  • Access grants: what the user can watch (used by entitlement checks).

Access grant kinds (MVP + future-proof):

  • course (per-course)
  • pass (time-limited all access) — not shipped, but supported in schema/check logic

Bundles are not a grant type. Future bundle purchase fulfillment will simply create multiple course grants.


6.6 Video Security (Cloudflare Stream)

  • Videos require signed playback.
  • API endpoint POST /lessons/:lessonId/playback-token:
    • checks session + access rules
    • returns short-lived Stream token

Web embeds Stream player using the token, never raw UIDs alone.


6.7 Analytics (PostHog)

Minimum events:

  • play_intent (most important)
  • auth_start, auth_success
  • checkout_start
  • purchase_success (emitted when webhook confirmed OR on return page after confirmed state)
  • play_blocked_not_entitled
  • play_start
  • lesson_complete
  • optional throttled: lesson_progress

Identity:

  • anonymous browsing allowed
  • on login: identify user and link prior anonymous session

6.8 Progress Tracking

  • store watched seconds and completion
  • course page shows “Continue where you left off”
  • lesson page supports resume timestamp (optional v1.1)

7) Data Model (D1 / Drizzle)

Tables (MVP)

users

  • (managed by auth) + id used across domain

courses

  • id, slug, title, description, language, coverImage
  • priceCents, currency
  • stripeProductId (unique)
  • publishedAt

lessons

  • id, courseId, slug, title, summary, order
  • isPreview
  • streamUid
  • durationSeconds
  • publishedAt

categories

  • id, slug, name, description?

course_categories

  • courseId, categoryId

purchases

  • id, userId
  • stripeCheckoutSessionId (unique)
  • status (paid/refunded/etc.)
  • createdAt

purchase_items

  • purchaseId
  • stripeProductId
  • quantity

access_grants

  • id, userId
  • kind (course | pass)
  • courseId nullable (required when kind=course)
  • startsAt
  • endsAt nullable (required when kind=pass)
  • sourcePurchaseId nullable

lesson_progress

  • userId, lessonId
  • watchedSeconds
  • completedAt nullable

8) API (Hono) — Contract Overview (DTOs live in packages/shared)

Names are illustrative. Exact schemas live in shared.

Public

  • GET /courses
  • GET /courses/:slug
  • GET /courses/:slug/lessons
  • GET /lessons/:id (or slug-based variant)
  • GET /categories
  • GET /categories/:slug

Authenticated

  • GET /me (user + grants summary)
  • POST /lessons/:lessonId/playback-token
  • POST /checkout/session
  • POST /progress (upsert progress)
  • GET /progress?courseId=... (optional)

Webhooks (no auth)

  • POST /webhooks/stripe

Access check logic

canWatchLesson(user, lesson):

  • if not authenticated → deny
  • if lesson.isPreview → allow
  • if has active pass (future) → allow
  • if has course grant for lesson.courseId → allow
  • else deny

9) Web App Requirements (Next.js / shadcn)

UI primitives (MVP)

  • Course cards, lesson list, category chips
  • Captive player component
  • Auth modal
  • Purchase modal
  • Skeletons/loading states

SEO Requirements

  • server-rendered metadata: title/description per page
  • OpenGraph tags per course/lesson
  • sitemap + robots
  • (optional v1.1) JSON-LD Course schema

10) Environments & Deploy

  • Production: master
  • Preview: PR branches (+ local dev is the main dev workflow)

No staging environment.


11) Non-Functional Requirements

  • Type safety: strict TS, DTO validation at API boundary
  • Security:
    • signed Stream playback only
    • Stripe webhook signature verification
    • rate limit playback-token endpoint
  • Performance:
    • cache public catalog responses at CDN/API layer where safe
    • avoid extra roundtrips on lesson pages (batch loaders if needed)

12) Milestones & Tasks (checkbox-driven)

M0 — Repo foundation (Turbo + TS + lint) ✅

  • M0.1 Initialize Turborepo monorepo with pnpm
  • M0.2 Create apps: apps/web, apps/api
  • M0.3 Create packages: packages/shared, packages/db
  • M0.4 Add root TypeScript config(s) (strict) and package references
  • M0.5 Add ESLint (single shared config) + scripts: lint, typecheck
  • M0.6 Configure Turbo pipeline: dev, build, lint, typecheck, test
  • M0.7 Add environment variable conventions + .env.example (web/api separate)
  • M0.8 Add base README with local dev commands and architecture note

M1 — Database & migrations (D1 + Drizzle) ✅

  • M1.1 Implement Drizzle schema for MVP tables (courses, lessons, categories, grants, purchases, progress)
  • M1.2 Add migration tooling + scripts (db:generate, db:migrate, db:seed)
  • M1.3 Seed script: initial courses/lessons/categories from a JSON seed file
  • M1.4 Document local D1 workflow (wrangler + drizzle)
  • M1.5 Expandir seed de catálogo com múltiplos cursos/aulas e capas reais locais para validação visual do web

M2 — Shared contracts (Zod + DTOs + pure logic) ✅

  • M2.1 Define shared Zod schemas: Course, Lesson, Category, AccessGrant, Progress
  • M2.2 Define DTO schemas for API endpoints (requests/responses)
  • M2.3 Define ApiError schema + error codes
  • M2.4 Implement pure helper canWatchLesson(grants, lesson) (+ future pass support)
  • M2.5 (Optional) typed apiClient wrapper for web consuming shared DTOs

M3 — API foundation (Hono + D1 binding + error handling) ✅

  • M3.1 Bootstrap Hono worker app with routing and consistent error format
  • M3.2 Wire D1 + Drizzle connection via Worker bindings
  • M3.3 Implement CORS for Vercel web origin (credentials included)
  • M3.4 Add rate limiting middleware for sensitive endpoints (deferred to Cloudflare native)
  • M3.5 Implement /health and basic logging

M4 — Auth (Better Auth + OAuth) ✅

  • M4.1 Integrate Better Auth into API
  • M4.2 Configure OAuth providers: Google + GitHub
  • M4.3 Implement GET /me returning user + access summary
  • M4.4 Web: auth modal + login/logout UX (pure client + redirects to API)

M5 — Catalog APIs (courses/lessons/categories) ✅

  • M5.1 GET /courses (published only)
  • M5.2 GET /courses/:slug (+ include categories)
  • M5.3 GET /courses/:slug/lessons (ordered)
  • M5.4 GET /categories, GET /categories/:slug (+ courses)
  • M5.5 Ensure DTO validation + types from packages/shared

M6 — Web pages (Next + shadcn/ui + SEO baseline)

  • M6.1 Set up Tailwind + shadcn/ui in apps/web
  • M6.2 Build /cursos page (SSR fetch from API)
  • M6.3 Build course detail page with lesson list
  • M6.4 Build lesson page with captive player shell (poster + play)
  • M6.5 Build category listing + category page
  • M6.6 Add metadata per page (title/description/OG)

M7 — Video playback (Stream tokens + gating)

  • M7.1 API: implement POST /lessons/:lessonId/playback-token
  • M7.2 API: enforce canWatchLesson rules (preview vs course grants vs future pass)
  • M7.3 Web: implement captive player flow (play → auth → token → player)
  • M7.4 Web: handle "blocked" state (purchase modal trigger)
  • M7.5 Web: emit PostHog play_start after successful start (deferred to M9 analytics)

M8 — Payments (Stripe Checkout + webhook fulfillment)

  • M8.1 Add stripeProductId to courses (seed + schema)
  • M8.2 API: POST /checkout/session { courseId }
  • M8.3 API: fetch Stripe Product default_price and create Checkout Session → return url
  • M8.4 Web: purchase modal + redirect to Checkout URL
  • M8.5 API: Stripe webhook /webhooks/stripe (verify signature)
  • M8.6 Webhook: create purchase + purchase_items
  • M8.7 Webhook: create access_grants(kind=course, courseId=...)
  • M8.8 Web: post-purchase return handling (refresh /me, unlock playback)
  • M8.9 Emit PostHog purchase_success when entitlement confirmed

M9 — Analytics (PostHog end-to-end)

  • M9.1 Initialize PostHog in web (env-based)
  • M9.2 Implement required events (play_intent/auth/checkout/blocked/complete)
  • M9.3 Identify user on login and link anonymous history (alias strategy)
  • M9.4 Add basic dashboard notes (funnel steps) in docs

M10 — Progress tracking

  • M10.1 API: POST /progress upsert watched seconds
  • M10.2 API: GET /progress or embed progress in course response (auth only)
  • M10.3 Web: “Continue” on course page
  • M10.4 Web: lesson completion event + UI marker

M11 — SEO polish + hardening + launch checklist

  • M11.1 Generate sitemap.xml (courses + lessons + categories)
  • M11.2 robots.txt
  • M11.3 Improve OG images pattern (static or generated)
  • M11.4 Tighten caching headers for public catalog endpoints
  • M11.5 Add integration tests for: token-gating, webhook grant, access rules
  • M11.6 Security review checklist (webhook verification, token expiry, rate limits)
  • M11.7 Deployment docs (Vercel + Cloudflare Worker env vars)

13) Acceptance Criteria (MVP “done”)

  • Anonymous users can browse and Google can index course/lesson/category pages.
  • Clicking Play always captures play_intent.
  • Preview lesson: user must login, then can watch.
  • Locked lesson: user must purchase course, then can watch.
  • Stream playback cannot be started without API-issued signed token.
  • Stripe webhook reliably grants course access.
  • PostHog shows a functioning funnel: play_intent → auth_success → checkout_start → purchase_success → play_start.
  • Repo passes: pnpm lint + pnpm typecheck + pnpm build in CI.

14) Future Extensions (explicitly not MVP)

  • Bundles: add mapping “bundle productId → courseIds”, fulfillment creates multiple course grants.
  • Pass: subscription that creates access_grants(kind=pass, endsAt=...), canWatchLesson already supports it.
  • Mobile app: reuse API + shared DTOs (no UI package needed yet).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment