Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Supabase — get from project settings > API
NEXT_PUBLIC_SUPABASE_URL=https://[project-ref].supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ... # Never expose to client — server-only

# Stripe — get from dashboard.stripe.com/apikeys
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_... # Get from Stripe dashboard after registering webhook

# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Production: NEXT_PUBLIC_APP_URL=https://techne.institute
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
38 changes: 38 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# env files
.env*
!.env.local.example

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
115 changes: 115 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Techne Institute

A Next.js web application for Techne Institute — an AI/technology learning center in Boulder, CO.
Students enroll in cohorts, pay via Stripe, and access session recordings in a protected members area.

## Stack

- **Next.js 15** (App Router, TypeScript)
- **Supabase** — Postgres database + auth (magic link, no passwords)
- **Stripe** — one-time payments, promo codes for sliding scale
- **Vercel** — hosting + CI/CD

## Design Tokens

```css
--void: #08080a; /* deepest dark */
--ink: #0f0f12; /* hero background */
--charcoal: #1a1a1f; /* body text */
--graphite: #2a2a30; /* secondary text */
--parchment: #f7f5f0; /* body background */
--cream: #ebe7df; /* card backgrounds */
--bone: #d8d3c8; /* borders */
--stone: #9a958a; /* muted text, labels */
--ember: #c2512a; /* primary accent */
--ember-dim: #8a3a1f; /* ember hover state */
--font-display: Cormorant, serif
--font-body: Source Serif 4, serif
--font-mono: IBM Plex Mono, monospace
```

All fonts loaded via `next/font/google` as CSS variables in `app/layout.tsx`.

## Routes

| Route | Type | Auth |
|---|---|---|
| `/` | Static | Public |
| `/programs` | Server component (DB query) | Public |
| `/enroll/[cohort]` | Server component | Public |
| `/enroll/[cohort]/success` | Server component | Public |
| `/signin` | Client component | Public |
| `/auth/callback` | API route | Public |
| `/cohort` | Server component | Protected |
| `/cohort/profile` | Client component | Protected |
| `/admin` | Server component | Protected (admin only) |
| `/writing` | Static | Public |
| `/api/checkout` | API route | Public |
| `/api/webhooks/stripe` | API route | Stripe signature |

## Environment Variables

See `.env.local.example`. Required:
- `NEXT_PUBLIC_SUPABASE_URL`
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
- `SUPABASE_SERVICE_ROLE_KEY` (server-only — never expose to client)
- `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`
- `STRIPE_SECRET_KEY`
- `STRIPE_WEBHOOK_SECRET`
- `NEXT_PUBLIC_APP_URL`

## Database

Migrations in `supabase/migrations/`. Run in Supabase SQL Editor in order:
1. `001_initial_schema.sql` — tables
2. `002_rls_policies.sql` — row-level security
3. `003_triggers.sql` — auto-create profile on signup

Seed data in `supabase/seed.sql` — creates Cohort 2 record.

## Critical Patterns

### Post-Payment Auth Flow
After Stripe checkout, the student has no Supabase session. Never redirect to `/cohort` directly from Stripe.
Always redirect to `/enroll/[cohort]/success?session_id=...` which:
1. Retrieves the Stripe session to get the student's email
2. Sends a magic link to that email
3. Shows "Check your email" message

### Stripe Webhook Idempotency
The webhook handler (`/api/webhooks/stripe`) checks for an existing `stripe_session_id` before inserting.
Listens to `checkout.session.completed` (not `payment_intent.succeeded`) to cover 100% promo codes.

### Admin Role
Set `profiles.is_admin = true` via Supabase SQL editor:
```sql
update public.profiles set is_admin = true
where id = (select id from auth.users where email = 'ag@unforced.org');
```

### Supabase Clients
- `lib/supabase/client.ts` — browser client (use in `'use client'` components)
- `lib/supabase/server.ts` → `createClient()` — server client with cookie handling
- `lib/supabase/server.ts` → `createAdminClient()` — service role client (webhook + post-payment only)

## Local Development

```bash
npm install
cp .env.local.example .env.local
# Fill in .env.local with Supabase + Stripe keys
npm run dev
```

For Stripe webhooks locally:
```bash
stripe listen --forward-to localhost:3000/api/webhooks/stripe
```

## Key Files

- `middleware.ts` — protects `/cohort` and `/admin` routes
- `app/globals.css` — all styles (no Tailwind)
- `app/layout.tsx` — fonts + Nav + Footer
- `app/api/webhooks/stripe/route.ts` — enrollment on payment
- `app/enroll/[cohort]/success/page.tsx` — post-payment magic link send
13 changes: 13 additions & 0 deletions app/_components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default function Footer() {
return (
<footer className="site-footer">
<p>
Techne Institute &bull; Part of{' '}
<a href="https://regenhub.xyz" target="_blank" rel="noopener noreferrer">
RegenHub
</a>{' '}
&bull; Boulder, Colorado &bull; 2026
</p>
</footer>
)
}
29 changes: 29 additions & 0 deletions app/_components/Nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client'

import Link from 'next/link'

export default function Nav() {
return (
<nav className="site-nav">
<div className="nav-inner">
<Link href="/" className="nav-logo">
TECHNE
</Link>
<ul className="nav-links">
<li>
<Link href="/programs">Programs</Link>
</li>
<li>
<Link href="/writing">Writing</Link>
</li>
<li>
<Link href="/cohort">Cohort</Link>
</li>
</ul>
<Link href="/signin" className="nav-signin">
Sign In
</Link>
</div>
</nav>
)
}
Loading