Ship/Now

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

SymptomFile
Signed in, then signed out againproxy.ts, then auth/callback/route.ts
Magic link opens and does nothingSupabase email templates, see Authentication
Paid, but nothing happenedapi/webhooks/stripe/route.ts and the Stripe dashboard's event log
Wrong price chargedapi/checkout/route.ts. The price is read from the database, never the request
Copy shows {name} literallymessages/*.json, see Internationalisation
Colour token does nothingapp/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 rowsupabase/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.