Pottery course site with lifetime video and PDF access
Beginner-friendly course site where students pay once to permanently unlock pottery videos and PDF handouts, behind login and private file storage.
nextjstypescriptsupabasestripepostgresvitest
Built 1 time, downloaded 0 times.
Install from your terminal
Run this in your project folder. It signs you in, shows what changes and asks before it overwrites a file.
npx claude-lab add pottery-course-site-with-lifetime-video-and-pdf-access-65fd19CLAUDE.md# Pottery course site
## Purpose
This is a paid, one-time-purchase course site for pottery classes. Students pay once through Stripe and keep lifetime access to the course videos and PDF handouts. The owner is the only admin. Read SPEC.md for what to build and PLAN.md for the current phase.
## Stack
- Next.js with the App Router and TypeScript
- Supabase for login, Postgres and private file storage
- Stripe Checkout for the one-time payment
- Mux or Bunny Stream for video
These are defaults. The owner may change them in SPEC.md before Phase 1 starts. If SPEC.md names a different tool, follow SPEC.md and tell the owner.
## Commands
- `npm run dev` starts the local site at http://localhost:3000
- `npm run lint` checks code style
- `npm run typecheck` checks TypeScript types
- `npm run build` makes a production build
- `npm test` runs the Vitest tests
- `npm run verify` runs all of the above
Run `npm run verify` before saying any task is done. If it fails, fix the first error and run it again.
## Secrets and keys
- Never commit `.env` files or any real key.
- Use obvious placeholders in examples, such as YOUR_STRIPE_KEY or YOUR_SUPABASE_URL.
- The Supabase service-role key stays on the server. Never import it into client components or send it to the browser.
- Real values belong in `.env.local`, which the owner creates. Do not ask the owner to paste keys into chat.
## Access rules
- A lesson video or PDF may only be served after checking for a matching row in `purchases` for the signed-in user.
- Never expose raw storage URLs, storage paths or video IDs to people who have not bought the course.
- Access checks run on the server, on every lesson and PDF route.
- Denied requests return 403.
## Stripe rules
- Grant access only from the verified webhook event `checkout.session.completed`.
- Never grant access from the success redirect page, because the URL can be visited without paying.
- Verify the webhook signature before doing anything with the event.
- Handle repeated webhook events safely. The same event may arrive more than once.
- Keep Stripe in test mode until Phase 5 is finished.
## Code conventions
- Use server components by default. Add client components only for interactive parts.
- Keep functions small and named for what they do.
- Put each feature in its own folder under `src/features/`.
- Use a database row-level security policy on every new table.
- Store PDFs only in the private storage bucket, never in a public one.
## Working style
- The owner is new to coding. After each change, explain what you did in two or three plain sentences.
- Ask before adding any new dependency. Name the package and say why it is needed.
- Ask before changing `.claude/settings.json` or any hooks.
- Mark finished checklist items in PLAN.md as you complete them.
## Review
@REVIEW.md
Use the same checklist for code review and before merging any change. Anything that fails `npm run verify` is a blocker.
## Gotchas
- Supabase row-level security blocks reads by default. If a query returns nothing, check the policy before changing code.
- Stripe test and live keys look alike. Confirm which mode `.env.local` uses before running a purchase test.
- Video IDs are not secret, but they must never reach a visitor who has not paid. Serve playback through a signed, short-lived URL.
.claude/agents/test-runner.md---
name: test-runner
description: Runs lint, typecheck, tests and build and reports only failures with file and line.
model: haiku
tools:
- Bash
- Read
maxTurns: 8
---
You run the project's checks and report results. You do not fix anything.
Run these commands in order from the project root:
1. `npm run lint`
2. `npm run typecheck`
3. `npm test`
4. `npm run build`
Rules:
- Never edit, create, or delete files.
- If a step fails, record it and continue to the next step so all failures are visible in one pass.
- Read a file only when needed to confirm the file and line of an error.
- Return a short list with one entry per failing check. Each entry has the check name, the file and line if known, and the first error line. No extra commentary.
- If every check passes, reply with one line: all checks passed.
Keep your final reply under 15 lines.
.claude/settings.json{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Bash(rm -rf *)",
"Bash(git push --force *)",
"Bash(npx supabase db reset *)"
],
"ask": [
"Bash(git push *)",
"Bash(npm publish *)",
"Bash(npx supabase *)"
]
}
}
.claude/skills/add-course-content/SKILL.md---
name: add-course-content
description: Use when adding courses, lessons, videos or PDFs to the pottery site, so the content is stored safely through the admin code path and stays hidden until published.
---
# Add course content
Follow these steps in order. Keep notes short.
## 1. Ask first
Before editing any file, ask the owner for:
- Course title
- Price (in pounds or dollars, as set in SPEC.md)
- Lesson order (for example, lesson 1 first)
- Video ID for each lesson, from the video host (Mux or Bunny Stream)
- PDF file names to upload
Do not guess values. Wait for the answers.
## 2. Add rows through the admin code path
- Insert courses, lessons and resources only through the admin code in `src/features/admin/`.
- Never hand-edit the database or run raw SQL inserts.
- Use the existing server actions or functions. Do not add a new path to write rows.
## 3. Handle PDFs safely
- Upload PDFs only to the private storage bucket.
- Never make a bucket or file public.
- Store the storage path, not a public URL.
- Never show raw storage paths or URLs in client code.
## 4. Handle videos
- Store only the video ID the owner gives you.
- Never place the video ID in a public page or a client component for a locked lesson.
## 5. Verify
1. Run the verify skill (`npm run verify`) and report the result.
2. If the change touches access rules, also run `npm test -- access`.
3. Check that the new lesson is hidden from anonymous users. A signed-out visit to its URL should redirect to login or return 403, and the video and PDF must not appear.
If a check fails, fix it before going on.
## 6. Tell the owner what to click next
End with short, plain steps, for example:
1. Open the admin page at `/admin/courses`.
2. Find the new course and check the title and price.
3. Click "Publish" when you are ready. Until then, students cannot see it.
Keep the message to five lines or fewer.
.claude/skills/verify/SKILL.md---
name: verify
description: Use after any code change, and before saying a task is done, to run this project's lint, typecheck, test and build checks and report the results.
---
# Verify
Use `npm run verify` by default. It runs all four checks in the order below.
## Steps in order
1. `npm run lint`
2. `npm run typecheck`
3. `npm test`
4. `npm run build`
Stop at the first failure. Fix it, then run the checks again from the start.
If you run the steps one by one instead of `npm run verify`, keep the same order.
## Payment and access changes
If the change touches Stripe, purchases, webhooks, or lesson and PDF access, also run:
```
npm test -- access
```
Report its result on its own line.
## UI changes
1. Start the app with `npm run dev`.
2. Open each page you changed and note the URL you checked.
3. If you did not open a browser, say so plainly: "Browser check not done."
Do not claim a page works from the code alone.
## Rules
- Never edit `.env` files or keys to make a check pass.
- Never skip a failing step or mark it as passing.
- Do not say a task is done until every step shows PASS.
## Report format
One line per step, using PASS or FAIL. On FAIL, add only the first error line, with the file and line number if the tool gives them.
```
lint: PASS
typecheck: PASS
test: FAIL - src/features/access/check.test.ts:24 expected 403, received 200
build: not run (stopped at test)
```
Then give one plain-language sentence on what to fix next.
INSTALL.md# Install and run the pottery course site
## What Claude Code is
Claude Code is an AI assistant that works in a terminal window.
It reads and edits the files in the folder where you start it, and it runs commands for you.
## Step 1: Install Node.js
1. Go to nodejs.org and install the LTS version.
2. Open a terminal and run `node --version`.
3. You should see a version number. If you do not, restart the terminal and try again.
## Step 2: Install Claude Code
1. Use the official installer described in the Claude Code docs.
2. Run `claude` once in a terminal.
3. Sign in when asked.
## Step 3: Create your accounts
Create free accounts for these services:
- Supabase (login and database)
- Stripe, in test mode (payments)
- Mux or Bunny Stream (video hosting)
- A hosting provider such as Vercel (later, for launch)
Never paste keys into the chat.
Put them in `.env.local` using the names in `.env.example`.
## Step 4: Set up the project folder
1. Create a folder for the site.
2. Copy in CLAUDE.md, SPEC.md, PLAN.md, REVIEW.md, INSTALL.md and the `.claude` folder.
3. Create a file named `.env.local` in the project.
4. Add placeholder names such as `NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL`.
5. Put real values only in `.env.local`.
## Step 5: Fill in the spec
Open SPEC.md and answer the open questions:
- Exact prices
- Business name
- Video host choice (Mux or Bunny Stream)
- Refund wording
- Currency
## Step 6: Install plugins
Open a terminal in the project folder and run:
```
claude plugin marketplace add anthropics/skills
```
Then start Claude Code in the same folder:
```
claude
```
Inside Claude Code, run these four commands one at a time:
```
/plugin install code-review@claude-plugins-official
/plugin install commit-commands@claude-plugins-official
/plugin install security-guidance@claude-plugins-official
/plugin install frontend-design@anthropic-agent-skills
```
## Step 7: Start the first phase
1. Start Claude Code in the project folder with `claude`.
2. Paste the kickoff prompt from the plan.
3. Wait for Claude to report that Phase 1 is done.
## Step 8: Run each phase
For every phase after Phase 1:
1. Open PLAN.md.
2. Read the manual check listed for the phase you just finished, and do it in your browser.
3. If the check passes, copy the fenced `/goal` prompt for the next phase.
4. Paste it into Claude Code.
Do not skip a manual check.
If a check fails, tell Claude what you saw in plain words.
## Helpful habits
- `/clear` between unrelated tasks, so old context does not slow Claude down.
- `/compact` when a session gets long.
- `/usage` shows how many tokens you have used.
- `/goal clear` stops a run that is going in circles.
## If something breaks
1. Run `/doctor` inside Claude Code.
2. Describe the error in plain words, including what you clicked and what you expected.
3. Do not paste secret values into the chat, even when an error mentions them.
## Warnings
- Keep Stripe in test mode until Phase 5 is finished.
- Switch to live keys only after a test purchase works from start to finish.
- Never share your `.env.local` file with anyone, and never commit it.
PLAN.md# Build plan
Work the phases in order. For each phase:
1. Start `claude` in this folder.
2. Copy the fenced `/goal` block for that phase and paste it.
3. When Claude reports done, run the manual check yourself in a browser.
4. Only then start the next phase.
## Phase 1: Foundation
**Goal:** A Next.js site runs locally, shows placeholder courses, and passes verify.
Tasks:
- [ ] Create the Next.js app (App Router, TypeScript) in this folder.
- [ ] Add Tailwind CSS.
- [ ] Set up lint, typecheck, Vitest and the `verify` script.
- [ ] Home page lists the three placeholder courses from SPEC.md.
- [ ] `npm run verify` passes.
Manual check: Run `npm run dev`, open https://example.com and see three course cards. Narrow the window to phone width and check they still fit.
```
/goal Phase 1 of PLAN.md is complete: a Next.js (App Router, TypeScript) app in this folder runs with `npm run dev`, the home page lists the three placeholder courses from SPEC.md, and `npm run verify` exits 0. Check: run `npm run verify` and show its exit code and the last 5 lines of output. Run `npm run dev` and show the HTTP status from `curl -sI http://localhost:3000`, which must be 200. Constraints: follow CLAUDE.md; read SPEC.md and PLAN.md first; no authentication, payments, video or storage yet; do not create or commit any .env file or real key; do not change .claude/settings.json; add only the Phase 1 dependencies (Tailwind, Vitest); tick the Phase 1 checkboxes in PLAN.md; explain the change in three plain sentences. Turn bound: stop after 30 turns or when both checks pass, whichever comes first. If you stop early, name the check that still fails and why.
```
What you will see: a simple home page with three pottery course cards. Nothing is for sale or locked yet.
## Phase 2: Accounts
**Goal:** Students can sign in with a magic link and sign out. Anonymous visitors to My Courses are sent to sign in.
Tasks:
- [ ] Add the Supabase packages `@supabase/supabase-js` and `@supabase/ssr`.
- [ ] Create `.env.example` with placeholders such as `NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL`.
- [ ] Build the magic-link sign-in and sign-out pages.
- [ ] Create the profiles table with an `is_admin` flag and row-level security.
- [ ] Build the protected My Courses page and a test for the redirect.
Before you start: create a Supabase project and put its values in `.env.local`. Never paste keys into chat.
Manual check: Enter your email, open the link, and see My Courses. Sign out. Open `/my-courses` in a private window and confirm you are sent to sign in.
```
/goal Phase 2 of PLAN.md is complete: Supabase magic-link sign-in and sign-out work, the profiles table has an is_admin flag with row-level security, and My Courses redirects anonymous visitors to sign-in. Check: run `npm run verify` and `npm test -- auth`, and show both exit codes. Then run `curl -sI http://localhost:3000/my-courses` and show the status line and the Location header; the status must be 307 or 302 and Location must point at the sign-in page. Constraints: follow CLAUDE.md; read SPEC.md and PLAN.md first; use only the placeholder names in .env.example and never write real keys; add only the Phase 2 Supabase packages, and ask before adding anything else; do not add Stripe or storage yet; tick the Phase 2 checkboxes in PLAN.md; explain the change in three plain sentences. Turn bound: stop after 30 turns or when all checks pass, whichever comes first. If you stop early, name the check that still fails and why.
```
What you will see: a sign-in page that emails you a link. After signing in you land on an empty My Courses page. Signed out, the page sends you back to sign in.
## Phase 3: Catalog and admin
**Goal:** The owner can create courses and lessons. A course appears publicly only when it is published.
Tasks:
- [ ] Create the courses and lessons tables with row-level security.
- [ ] Build the course detail page showing title, description and price.
- [ ] Build an owner-only admin page to create a course.
- [ ] Build an owner-only admin page to add lessons (title, order, video ID).
- [ ] Add a test that an unpublished course is hidden from anonymous visitors.
Manual check: Sign in as the admin, create a course, and leave it unpublished. Check that it is missing from the home page in a private window. Publish it and check it appears.
```
/goal Phase 3 of PLAN.md is complete: courses and lessons tables have row-level security, the owner-only admin pages create courses and add lessons, and an unpublished course is hidden from anonymous visitors. Check: run `npm run verify` and `npm test -- catalog`, and show both exit codes. Then show the HTTP status from `curl -sI http://localhost:3000/courses/UNPUBLISHED_COURSE_ID`, where UNPUBLISHED_COURSE_ID is the id of a course you created with published set to false; it must be 404. Constraints: follow CLAUDE.md; read SPEC.md and PLAN.md first; never hand-edit the database, insert rows through the admin code path; do not add payments, video or storage uploads yet; do not change .claude/settings.json; ask before adding any new dependency; tick the Phase 3 checkboxes in PLAN.md; explain the change in three plain sentences. Turn bound: stop after 30 turns or when all checks pass, whichever comes first. If you stop early, name the check that still fails and why.
```
What you will see: an admin page where you add a course and its lessons. Courses show on the public home page only after you publish them.
## Phase 4: Payments and access
**Goal:** A Stripe test-mode purchase unlocks the course. An unpaid user gets a 403 for the same lesson URL.
Tasks:
- [ ] Add the `stripe` package and start a Stripe Checkout session from the course page (test keys only).
- [ ] Build a webhook route that verifies the Stripe signature.
- [ ] On `checkout.session.completed`, write one purchases row. Repeated events must not create duplicates.
- [ ] Check purchases before serving any lesson or PDF route.
- [ ] `npm test -- access` covers the allowed and denied cases.
Before you start: keep Stripe in test mode and put the test keys in `.env.local`.
Manual check: Buy the course with test card +00 000 000 000 4242. My Courses should show it. Sign in with a second test account and open the same lesson URL; you should get a 403.
```
/goal Phase 4 of PLAN.md is complete: a Stripe test-mode Checkout session is created, the webhook verifies the Stripe signature and writes one purchases row on checkout.session.completed, and lesson and PDF routes check purchases. Check: run `npm test -- access` and show the pass and fail counts with exit code 0. Run `npm run verify` and show its exit code. Then show the HTTP status for a lesson URL requested without a session, which must be 401 or 403, and show that a POST to the webhook route without a valid Stripe-Signature header returns 400. Constraints: follow CLAUDE.md; read SPEC.md and PLAN.md first; use Stripe test keys only and never write real keys; grant access only from the verified webhook event, never from the success page; add only the stripe package; ask before adding anything else; do not change .claude/settings.json; tick the Phase 4 checkboxes in PLAN.md; explain the change in three plain sentences. Turn bound: stop after 30 turns or when all checks pass, whichever comes first. If you stop early, name the check that still fails and why.
```
What you will see: after a test payment, the course appears on My Courses. A second account gets a "forbidden" page for the same lesson URL.
## Phase 5: Media and launch
**Goal:** Video plays only for paid students. PDFs use short-lived links. The site is deployed and the deploy checklist is complete.
Tasks:
- [ ] Add signed playback for videos from the chosen host (Mux or Bunny Stream).
- [ ] Serve PDFs through signed URLs that expire within 5 minutes.
- [ ] Add friendly error pages for 403, 404 and 500.
- [ ] Add a privacy page covering data use and how to request deletion by email.
- [ ] Complete the deploy checklist:
- [ ] Environment variables set in the hosting provider.
- [ ] Supabase redirect URL set to the live site.
- [ ] Stripe webhook endpoint set to the live site, still in test mode.
- [ ] One full test purchase works on the live site.
- [ ] Owner approves switching to live Stripe keys. Only then switch.
Manual check: On your phone, play a lesson video as a paid student and confirm a stranger cannot. Open a PDF link and check it expires. Read the privacy page.
```
/goal Phase 5 of PLAN.md is complete: lesson videos use signed playback, PDFs use signed URLs that expire within 5 minutes, 403, 404 and 500 pages exist, the privacy page is published, and the deploy checklist in PLAN.md Phase 5 has every box ticked except the live Stripe switch, which waits for owner approval. Check: run `npm run verify` and `npm test -- access`, and show both exit codes. Then run `curl -sI` on the deployed home page URL that you record in the deploy notes, and show the HTTP status, which must be 200. Constraints: follow CLAUDE.md; read SPEC.md and PLAN.md first; never expose raw storage URLs or video IDs to non-owners; never print or commit keys; add only the video host's documented package; ask before adding anything else; do not change .claude/settings.json; do not switch Stripe to live keys without owner approval; tick the Phase 5 checkboxes in PLAN.md; explain the change in three plain sentences. Turn bound: stop after 30 turns or when all checks pass, whichever comes first. If you stop early, name the check that still fails and why.
```
What you will see: a working site online. Students who paid can watch and download. Everyone else sees a locked page, and the privacy page explains how data is used.
REVIEW.md# Review checklist
Use this list for `/code-review` and before merging any change.
CLAUDE.md imports this file, so Claude reads it in every session.
## Security
- [ ] No secrets in code, tests, docs or logs. Examples use placeholders like YOUR_STRIPE_KEY.
- [ ] No .env file is committed or pasted into chat.
- [ ] Every lesson and PDF route checks a purchases row for the signed-in user.
- [ ] The service-role key is used only on the server and never reaches the browser.
- [ ] The Stripe webhook verifies the signature before it does anything else.
- [ ] Raw storage URLs and video IDs are never sent to people who do not own the course.
## Payments
- [ ] Access is granted only from the verified checkout.session.completed event.
- [ ] The success redirect page never grants access.
- [ ] Repeated webhook events are handled safely. A second event must not create a second purchase.
## Data
- [ ] Row-level security is enabled on every new table.
- [ ] No raw storage paths appear in client code.
- [ ] PDFs stay in the private storage bucket.
- [ ] Rows are inserted through the admin code path, not by hand-editing the database.
## Tests
- [ ] New access rules have one test for the allowed case and one for the denied case.
- [ ] `npm test -- access` passes for any payment or access change.
## Verify
- [ ] `npm run verify` passes.
- [ ] Anything that fails verify is a blocker. Do not merge it.
## Plain-language summary
- [ ] The change is explained in two sentences that a non-coder can follow.
SPEC.md# Pottery course site: spec
Owner-editable. Claude reads this before building. Answer the open questions at the end before Phase 1 is done.
## Who it is for
- Students: adults, mostly complete beginners, who buy pottery classes.
- Owner: the only admin. Adds courses, lessons and PDFs.
## Core flow
1. Browse the course list.
2. Open a course to see its description and price.
3. Pay once with Stripe Checkout.
4. Land on My Courses.
5. Watch lesson videos and download PDFs.
Access never expires. One payment unlocks a course for life.
## Accounts
- Email magic link through Supabase. The app never stores passwords.
- Admin is a flag on the profiles table. Only admins can open the admin pages.
## Data model (sketch)
- profiles: id (matches the auth user), is_admin.
- courses: id, title, description, price_cents, published.
- lessons: id, course_id, order, title, video_id, kind.
- resources: id, lesson_id or course_id, storage_path. PDFs live in private storage.
- purchases: id, user_id, course_id, stripe_session_id, created_at.
- Unique on user_id plus course_id, so one purchase per student per course.
- Every table has row-level security turned on.
## Admin pages (owner only)
- Create a course: title, description, price, published on or off.
- Upload PDFs to the private storage bucket.
- Add a lesson: title, order, and the video ID pasted from the video host.
## Access rules
- A lesson video or PDF is served only after a purchases row exists for the signed-in user.
- Video and PDF links are short-lived signed links. Raw storage paths and video IDs never reach non-owners.
- An unpaid signed-in user who opens a lesson or PDF URL gets a 403.
- Access is granted only from the verified Stripe webhook event checkout.session.completed, never from the success page.
## Out of scope for v1
- Refunds UI, coupons, certificates, comments, subscriptions, multiple currencies, mobile app.
## Non-functional
- Pages load in under 2 seconds on a phone.
- Every page is usable on a small screen.
- No student data is sold or shared.
- Students can ask to delete their account by emailing the owner.
## Placeholder courses (Phase 1 only)
- Wheel basics
- Hand building
- Glazing and firing
Replace these with real courses in Phase 3.
## Acceptance checklist
- [ ] Home page lists the course list.
- [ ] A visitor can sign in with a magic link and sign out.
- [ ] An anonymous visitor opening My Courses is sent to sign in.
- [ ] Owner can create a course; it shows publicly only when published.
- [ ] Owner can add lessons and upload PDFs; PDFs have no public URL.
- [ ] A Stripe test-mode purchase unlocks the course on My Courses.
- [ ] An unpaid signed-in user gets a 403 on a lesson or PDF URL.
- [ ] A paid student can play a lesson video; other visitors cannot.
- [ ] Course and lesson pages work at 375px wide.
- [ ] Privacy page explains data use and how to request deletion.
- [ ] The deploy checklist in PLAN.md Phase 5 is complete.
## Open questions for the owner
- Exact price for each course.
- Video host: Mux or Bunny Stream.
- Refund policy wording.
- Business name.
- Currency to charge in.