TeachRepo is MIT-licensed. Fork the template, wire up Supabase + Stripe, and deploy anywhere Node runs β Vercel, Railway, Fly.io, a plain VPS. No platform fees. No lock-in.
The full source is in the openclaw-workspace monorepo. The web app lives at startup-91-lean-launch-plan-repo-to-course-git-native-paywall/apps/web/.
# Fork on GitHub first, then:
git clone https://github.com/YOUR_ORG/YOUR_FORK.git
cd YOUR_FORK/startup-91-lean-launch-plan-repo-to-course-git-native-paywall/apps/web
npm installstartup-*/ directories. The teachrepo web app is entirely self-contained in apps/web/ with its own package.json.Create a new project at supabase.com. Then apply the schema:
# In the Supabase SQL editor, run both files in order:
# 1. supabase/schema.sql β tables, indexes, enums
# 2. supabase/rls.sql β row-level security policiesGrab your credentials from Project Settings β API:
NEXT_PUBLIC_SUPABASE_URL β Project URL (e.g. https://xxxx.supabase.co)NEXT_PUBLIC_SUPABASE_ANON_KEY β public anon keySUPABASE_SERVICE_ROLE_KEY β secret service role key (never expose client-side)SUPABASE_SERVICE_ROLE_KEY bypasses RLS. Keep it server-only. Never put it in NEXT_PUBLIC_* env vars.Authentication β Email β Enable email confirmations. For development you can disable email confirmation to skip the verify step.
Create a public bucket named course-assets for cover images and attachments. The app falls back gracefully if the bucket is absent.
Log in to dashboard.stripe.com. In test mode, collect your keys:
STRIPE_SECRET_KEY β starts with sk_test_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY β starts with pk_test_STRIPE_WEBHOOK_SECRET β generated when you create a webhook endpointIn Stripe Dashboard β Developers β Webhooks β Add endpoint:
# Endpoint URL:
https://YOUR_DOMAIN/api/webhooks/stripe
# Events to listen for:
checkout.session.completed
invoice.payment_succeeded
customer.subscription.updated
customer.subscription.deletedCopy the Signing secret (starts with whsec_) into STRIPE_WEBHOOK_SECRET.
Course prices are set per-course in the dashboard β no hard-coded Stripe price IDs needed. The import pipeline creates a Stripe Price on first publish.
stripe listen --forward-to localhost:3000/api/webhooks/stripeCopy the example env file and fill in your values:
cp .env.example .env.localMinimum required variables:
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhb...
SUPABASE_SERVICE_ROLE_KEY=eyJhb...
# Stripe
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# App
NEXT_PUBLIC_APP_URL=http://localhost:3000 # or https://yourdomain.com in prod
# GitHub (for repo-based imports)
GITHUB_TOKEN=ghp_... # GitHub PAT with repo read scope.env.example. The app boots without them β features degrade gracefully.npm run dev
# β http://localhost:3000
# Type-check:
npm run type-check
# Build for production:
npm run build
npm startThe first time you visit /auth/signup, create your founder account. The first signup auto-receives admin privileges if FOUNDER_EMAIL is set in env.
# From apps/web/
npx vercel --prod
# Follow prompts: link to your Vercel project, select framework = Next.js
# Set env vars when prompted, or add them in the Vercel dashboardstartup-91-lean-launch-plan-repo-to-course-git-native-paywall/apps/webvercel domains add yourdomain.com
# Then set the A record / CNAME in your DNS provider per Vercel's instructionsUpdate NEXT_PUBLIC_APP_URL to your custom domain and redeploy.
Add this workflow to your fork for automatic deploy-on-push:
# .github/workflows/deploy.yml
name: Deploy to Vercel
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
jobs:
deploy:
runs-on: ubuntu-latest
defaults:
run:
working-directory: startup-91-lean-launch-plan-repo-to-course-git-native-paywall/apps/web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: |
startup-91-lean-launch-plan-repo-to-course-git-native-paywall/apps/web/package-lock.json
- run: npm ci
- name: Type-check
run: npm run type-check
- name: Install Vercel CLI
run: npm install -g vercel@latest
- name: Pull Vercel env
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy
if: github.ref == 'refs/heads/main'
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}| Secret name | Where to get it |
|---|---|
| VERCEL_TOKEN | Vercel β Account Settings β Tokens |
| VERCEL_ORG_ID | vercel.com/account β General β Your ID, or .vercel/project.json |
| VERCEL_PROJECT_ID | .vercel/project.json after first vercel link |
| SUPABASE_SERVICE_ROLE_KEY | Supabase β Project Settings β API |
| STRIPE_SECRET_KEY | Stripe Dashboard β Developers β API keys |
| STRIPE_WEBHOOK_SECRET | Stripe Dashboard β Developers β Webhooks |
Go to your GitHub repo β Settings β Secrets and variables β Actions β New repository secret.
The CLI lets you push course updates directly from your local repo:
npm install -g @teachrepo/cli
# Authenticate (generates an API token for your account)
teachrepo login
# In your course repo:
teachrepo push --url https://yourdomain.com
# Push a specific branch as a version
teachrepo push --branch v1.2 --label "v1.2"See the full CLI Reference for all commands.
TeachRepo is a standard Next.js app. Any platform that runs Node 18+ works.
# railway.toml (place in apps/web/)
[build]
builder = "nixpacks"
buildCommand = "npm run build"
[deploy]
startCommand = "npm start"
healthcheckPath = "/api/health"
restartPolicyType = "on_failure"# From apps/web/:
fly launch --name teachrepo-app --region iad
fly secrets set NEXT_PUBLIC_SUPABASE_URL=... STRIPE_SECRET_KEY=... # etc.
fly deployFROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]output: 'standalone' to next.config.js to enable the minimal Docker image (Next.js docs: output: standalone).| Aspect | Self-hosted | Hosted (Creator plan) |
|---|---|---|
| Cost | Infra costs only (Vercel/Supabase free tiers = $0/mo) | $29/mo managed |
| Course limit | Unlimited (you own the DB) | β Unlimited (plan unlocked) |
| Lesson limit | Unlimited | β Unlimited |
| Custom domain | Yes β configure in Vercel | Yes β one-click in dashboard |
| Stripe account | Your own | Your own (Connect) |
| Platform fee | 0% | 0% direct, 10% marketplace |
| Marketplace listing | No (self-hosted not indexed) | Yes |
| Updates | Manual β merge upstream PRs | Automatic |
| Devops burden | You manage Supabase, Vercel, webhooks | Managed for you |
| Data ownership | Full β your Supabase project | Full β export anytime |
| AI quiz generation | Unlimited (bring your own key) | Unlimited (included) |
| Support | GitHub Issues / community | Priority email support |
Add the upstream remote and merge new releases:
git remote add upstream https://github.com/ErlisK/openclaw-workspace.git
# Check for new commits:
git fetch upstream
# Merge latest:
git merge upstream/main
# Or cherry-pick specific commits:
git cherry-pick <commit-sha>β Stripe webhook returns 400 "No signatures found"
β Make sure STRIPE_WEBHOOK_SECRET matches the signing secret for the endpoint in your Stripe dashboard. For local dev, use the Stripe CLI to forward events.
β Supabase RLS blocking all reads
β Check that you applied supabase/rls.sql. If testing, temporarily disable RLS on a table via the Supabase dashboard to isolate the issue.
β Build fails with "SUPABASE_SERVICE_ROLE_KEY not found"
β Add the secret to Vercel env vars (Dashboard β Project β Settings β Environment Variables) or to your .env.local file.
β GitHub import returns 404
β Make sure GITHUB_TOKEN has repo read scope and the repo is not private (or the token has access to private repos).
β Vercel deploy fails with "Build Error"
β Run npm run build locally first. The most common cause is a missing env var β check the build log for "undefined" values.