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)
- 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.sqle o D1 local foram reconciliados com osstripe_product_idreais para destravar o QA manual de checkout.
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.
- 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.
- Bundles UI/checkout (but keep design extensible).
- Free-pass subscription (but keep schema/checks extensible).
- Livestreams.
- Full admin dashboard (use seeds/import scripts).
- 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 always requires authentication.
- Preview lesson: allow after login.
- Locked lesson: allow only with access grant (or later pass).
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
packages/shareddefines:- Zod schemas for domain models + DTOs
ApiErrorshape + 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.
- Next.js (App Router)
- shadcn/ui components (Tailwind + Radix primitives)
- Calls API over HTTP (no Next backend/route handlers)
- Hono (Cloudflare Workers)
- Better Auth for OAuth (Google + GitHub in MVP)
- Stripe SDK for Checkout + webhooks
- Cloudflare Stream token minting
- D1 via Drizzle
- pnpm
- Turborepo pipelines:
dev,lint,typecheck,test,build - ESLint across repo (single shared config)
- TypeScript
strict: true(exceptions must be explicit)
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
On lesson page:
- show poster + “Play” button
- on Play click:
- capture
play_intentin PostHog - if logged out → auth modal
- if logged in:
- if preview → request Stream token → play
- if locked → show “Buy course” modal
- capture
Key requirement: even preview lessons require login to play.
MVP OAuth providers:
- GitHub
Session model:
- cookie-based session on the API domain
- web uses
fetch(..., { credentials: "include" })
MVP: course-only purchases.
Stripe mapping simplification
- Store only
stripeProductIdon the course record. - At checkout time, API retrieves Stripe Product and uses its
default_pricefor Checkout Session line items.- Operational requirement: each sellable Course Product in Stripe must have
default_priceset.
- Operational requirement: each sellable Course Product in Stripe must have
Checkout flow
- Web calls API:
POST /checkout/session { courseId } - API creates Checkout Session and returns
{ url } - Web redirects user to Stripe-hosted checkout URL
- Stripe webhook confirms payment → API creates access grants
Webhook is the source of truth for granting access.
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.
- 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.
Minimum events:
play_intent(most important)auth_start,auth_successcheckout_startpurchase_success(emitted when webhook confirmed OR on return page after confirmed state)play_blocked_not_entitledplay_startlesson_complete- optional throttled:
lesson_progress
Identity:
- anonymous browsing allowed
- on login: identify user and link prior anonymous session
- store watched seconds and completion
- course page shows “Continue where you left off”
- lesson page supports resume timestamp (optional v1.1)
users
- (managed by auth) +
idused across domain
courses
id, slug, title, description, language, coverImagepriceCents, currencystripeProductId(unique)publishedAt
lessons
id, courseId, slug, title, summary, orderisPreviewstreamUiddurationSecondspublishedAt
categories
id, slug, name, description?
course_categories
courseId, categoryId
purchases
id, userIdstripeCheckoutSessionId(unique)status(paid/refunded/etc.)createdAt
purchase_items
purchaseIdstripeProductIdquantity
access_grants
id, userIdkind(course|pass)courseIdnullable (required when kind=course)startsAtendsAtnullable (required when kind=pass)sourcePurchaseIdnullable
lesson_progress
userId, lessonIdwatchedSecondscompletedAtnullable
Names are illustrative. Exact schemas live in shared.
GET /coursesGET /courses/:slugGET /courses/:slug/lessonsGET /lessons/:id(or slug-based variant)GET /categoriesGET /categories/:slug
GET /me(user + grants summary)POST /lessons/:lessonId/playback-tokenPOST /checkout/sessionPOST /progress(upsert progress)GET /progress?courseId=...(optional)
POST /webhooks/stripe
canWatchLesson(user, lesson):
- if not authenticated → deny
- if lesson.isPreview → allow
- if has active pass (future) → allow
- if has
coursegrant for lesson.courseId → allow - else deny
- Course cards, lesson list, category chips
- Captive player component
- Auth modal
- Purchase modal
- Skeletons/loading states
- server-rendered metadata: title/description per page
- OpenGraph tags per course/lesson
- sitemap + robots
- (optional v1.1) JSON-LD Course schema
- Production:
master - Preview: PR branches (+ local dev is the main dev workflow)
No staging environment.
- 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)
- 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.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.1 Define shared Zod schemas: Course, Lesson, Category, AccessGrant, Progress
- M2.2 Define DTO schemas for API endpoints (requests/responses)
- M2.3 Define
ApiErrorschema + error codes - M2.4 Implement pure helper
canWatchLesson(grants, lesson)(+ future pass support) - M2.5 (Optional) typed
apiClientwrapper for web consuming shared DTOs
- 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
/healthand basic logging
- M4.1 Integrate Better Auth into API
- M4.2 Configure OAuth providers: Google + GitHub
- M4.3 Implement
GET /mereturning user + access summary - M4.4 Web: auth modal + login/logout UX (pure client + redirects to API)
- 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.1 Set up Tailwind + shadcn/ui in
apps/web - M6.2 Build
/cursospage (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.1 API: implement
POST /lessons/:lessonId/playback-token - M7.2 API: enforce
canWatchLessonrules (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_startafter successful start (deferred to M9 analytics)
- M8.1 Add
stripeProductIdto 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_successwhen entitlement confirmed
- 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.1 API:
POST /progressupsert watched seconds - M10.2 API:
GET /progressor embed progress in course response (auth only) - M10.3 Web: “Continue” on course page
- M10.4 Web: lesson completion event + UI marker
- 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)
- 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 buildin CI.
- Bundles: add mapping “bundle productId → courseIds”, fulfillment creates multiple course grants.
- Pass: subscription that creates
access_grants(kind=pass, endsAt=...),canWatchLessonalready supports it. - Mobile app: reuse API + shared DTOs (no UI package needed yet).