Start here · 3 min
Project structure
Where everything lives and, more usefully, where to look when a specific thing is broken.
src/
app/
api/checkout/route.ts opens a Stripe Checkout session
api/webhooks/stripe/route.ts fulfilment: the only place a sale becomes real
auth/callback/route.ts turns a magic link into a session cookie
app/page.tsx the signed-in dashboard
admin/page.tsx behind ADMIN_EMAILS
blog/ MDX posts
sign-in/page.tsx
layout.tsx fonts, locale, theme, providers
globals.css every design token
sitemap.ts robots.ts icon.tsx opengraph-image.tsx
components/
ui/ Card, Field: the two you will reuse
SignInForm SignOutButton LocaleSwitcher CopyField Logo Icons
lib/
auth.ts currentUser, requireUser
supabase/server.ts the two server clients
supabase/client.ts the browser client
stripe.ts email.ts blog.ts i18n.ts i18n.server.ts
paraglide/ generated. Never edit, never review
messages/
en.json fr.json the source of truth for copy
content/blog/ posts as files
supabase/schema.sql tables, policies, triggers
scripts/setup.mjs scripts/db-push.mjs
proxy.ts runs on every request
Where to look when
| Symptom | File |
|---|---|
| Signed in, then signed out again | proxy.ts, then auth/callback/route.ts |
| Magic link opens and does nothing | Supabase email templates, see Authentication |
| Paid, but nothing happened | api/webhooks/stripe/route.ts and the Stripe dashboard's event log |
| Wrong price charged | api/checkout/route.ts. The price is read from the database, never the request |
Copy shows {name} literally | messages/*.json, see Internationalisation |
| Colour token does nothing | app/globals.css. A token has to be declared in @theme before Tailwind will emit a class for it |
| A user can read another user's row | supabase/schema.sql. It is a policy, not application code |
proxy.ts
Next.js 16 renamed middleware. The file is proxy.ts at the root and it exports
proxy, not middleware. If you are copying a snippet from a blog post written
before the rename, that is why it does nothing.
It runs on every request and does one job: refresh the auth cookie. Server Components cannot write cookies, so without it a session expires mid-visit and the user is signed out halfway through checkout.
src/paraglide
Generated by the message compiler. It is committed so a clean clone builds without running the compiler first, but you never edit it and there is no point reading it in a diff. If you see three hundred changed files there, it is because you added one message.
The two Supabase clients
This distinction is the one that bites people, so it is worth learning early.
supabaseServer() // acts as the signed-in user. RLS applies. Use this by default.
supabaseAdmin() // acts as the service role. RLS does not apply.supabaseAdmin() exists for the webhook, which has no user and has to write
rows that no user is allowed to write. Reaching for it because a query returned
nothing is how you ship an app where every user can read every row. If
supabaseServer() returns nothing, the policy is the bug.
Something wrong or missing on this page? Tell us.