A production-ready starter for web apps: Next.js 16, React 19, Clerk for authentication, Supabase for Postgres and storage, Tailwind CSS 4 with shadcn/ui, and next-intl for translations. It boots without any credentials and walks you through setup in the browser.
- Auth with Clerk: sign-in, sign-up, social logins, and a full account page (email, password, MFA, sessions) under
/settings/account. Clerk's UI follows the app language. - Supabase with real security: versioned SQL migrations, generated TypeScript types, row level security, and column-level grants so users can never change their own
role. Clerk tokens are trusted by Supabase through the native third-party auth integration, so every query runs as the signed-in user. - Profiles and avatars: a
profilesrow is created on first visit (the Clerk webhook is a backstop and handles deletion), and avatars upload to a Supabase Storage bucket where each user can only write to their own folder. - Server Actions with shared validation: Zod schemas are used by the browser forms and re-checked on the server. Errors come back as translation keys.
- Internationalisation: English and Spanish out of the box, locale from a cookie or the browser, no URL prefixes. A test fails if a translation file is missing a key.
- Admin example:
/adminlists all users, and only works because a database policy allows admins to read every profile. - Privacy-friendly analytics: Google Analytics only loads after the visitor accepts the cookie banner, and the banner only appears if analytics are configured.
- Quality gates: strict TypeScript, validated environment variables, ESLint, Prettier, Vitest, Playwright, pgTAP database tests, and GitHub Actions running all of them.
- SEO and platform basics: metadata, Open Graph image, sitemap, robots, web manifest, security headers, health check endpoint.
Requirements: Node 22.12 or newer (see .nvmrc). Docker is only needed for the local Supabase stack.
npm installnpm run devOpen http://localhost:3000. With no credentials the app redirects to /setup, a wizard that:
- Checks your Clerk keys.
- Checks your Supabase keys.
- Shows you how to connect Clerk to Supabase and create the tables.
- Writes
.env.localfor you, and optionally pushes the project to a new GitHub repo and deploys it on Vercel.
Prefer doing it by hand? Copy .env.example to .env.local, fill it in, and apply the migrations (see below). You can also click "Skip for now" to preview the site without any services.
The setup API only runs in development and only while the app is unconfigured. In production it returns 404.
- In Clerk, open Supabase integration, activate it, and copy your Clerk domain.
- In Supabase, go to Authentication, Sign In / Providers, Third-Party Auth, add Clerk and paste the domain.
- Apply the database schema, either:
- with the CLI:
npx supabase linkthennpm run db:push, or - by pasting
supabase/migrations/*.sqlinto the Supabase SQL editor.
- with the CLI:
- In Clerk, add a webhook to
https://your-domain/api/webhooks/clerkforuser.createdanduser.deleted, and setCLERK_WEBHOOK_SIGNING_SECRET.
To make yourself an admin, run this in the Supabase SQL editor:
update public.profiles set role = 'admin' where id = 'user_...';| Command | What it does |
|---|---|
npm run dev |
Start the dev server |
npm run build / npm start |
Production build and server |
npm run check |
Lint, typecheck and unit tests |
npm run lint / lint:fix |
ESLint |
npm run format |
Prettier, including import and Tailwind class sorting |
npm run typecheck |
Generate route types, then tsc |
npm test |
Vitest unit tests |
npm run test:e2e |
Playwright, against a server on port 3100 |
npm run db:start / db:stop |
Local Supabase stack (Docker) |
npm run db:reset |
Recreate the local database from migrations and seed |
npm run db:diff -- <name> |
Generate a migration from changes made in local Studio |
npm run db:push |
Apply migrations to the linked Supabase project |
npm run db:types |
Regenerate src/lib/supabase/database.types.ts |
npm run test:db |
Run pgTAP tests in supabase/tests |
Migrations in supabase/migrations are the single source of truth. Never change the schema only in the dashboard.
npm run db:startnpm run db:resetAfter changing the schema, regenerate the types with npm run db:types and add a pgTAP test for any new policy in supabase/tests/database. npm run db:start prints local keys you can put in .env.local. To use real Clerk sessions against the local stack, enable [auth.third_party.clerk] in supabase/config.toml.
Three Supabase clients, each for one job:
| Import | Use it in | Runs as |
|---|---|---|
@/lib/supabase/server |
Server Components, Actions, Routes | The signed-in user (RLS) |
@/lib/supabase/client |
Client Components (useSupabase()) |
The signed-in user (RLS) |
@/lib/supabase/admin |
Webhooks, cron, scripts only | Service role, bypasses RLS |
messages/ Translations (en.json is the reference)
supabase/
migrations/ SQL migrations, applied in order
tests/database/ pgTAP tests for RLS and privileges
seed.sql Local seed data
src/
app/
(site)/ Pages with the header and footer
(auth)/ Clerk sign-in and sign-up
settings/ Profile, preferences, account (+ actions.ts)
admin/ Admin-only example
api/ Health check, Clerk webhook, setup wizard API
setup/ Setup wizard
components/ App components; ui/ holds shadcn/ui primitives
i18n/ Locale config and request resolution
lib/
actions/ Server Actions shared across pages
supabase/ Clients and generated types
validation/ Zod schemas shared by client and server
auth.ts requireUserId() for protected pages
profile.ts getCurrentProfile(), cached per request
env.ts Validated environment variables
proxy.ts Clerk middleware and setup-mode routing
tests/e2e/ Playwright specs
- Authorization lives with the data. Every protected page calls
requireUserId(), every Server Action checksauth(), and RLS is the final word. The proxy only routes between setup mode and the app. - Read in Server Components, write in Server Actions. Actions validate with the shared Zod schema and return an
ActionResultinstead of throwing. - No hardcoded copy. Add strings to
messages/en.jsonandmessages/es.json. Validation messages are keys undervalidation. - Use
env, notprocess.env. Add new variables tosrc/env.tsand.env.example. - Add UI primitives with the shadcn CLI:
npx shadcn@latest add <component>. - Files and folders are
kebab-case, componentsPascalCase, constantsSCREAMING_SNAKE_CASE.
- Add the code to
localesandlocaleNamesinsrc/i18n/config.ts. - Copy
messages/en.jsontomessages/<code>.jsonand translate it.npm testtells you if a key is missing. - Add the matching Clerk localization in
src/app/layout.tsx.
npm testruns unit tests for validation, locale negotiation, action results and translation completeness.npm run test:e2estarts the app on port 3100 and runs Playwright on desktop and mobile. Without credentials it tests setup mode. With Clerk keys plusE2E_CLERK_USER_EMAILandE2E_CLERK_USER_PASSWORD(a password user in your Clerk development instance), it also tests the signed-in flows.npm run test:dbchecks that users can only read and change their own data, cannot promote themselves, and cannot write into other users' storage folders.
- Create a production instance in Clerk, add its DNS records, and wait for them to verify.
- Set every variable from
.env.examplein Vercel (Settings, Environment Variables).NEXT_PUBLIC_values are inlined at build time, so redeploy after changing them. - Apply migrations to the production Supabase project (
npm run db:push). - Add the production webhook in Clerk and set
CLERK_WEBHOOK_SIGNING_SECRET. - If you use Google sign-in, add your domain to the OAuth client in Google Cloud Console.
| Symptom | Fix |
|---|---|
| Site shows "This deployment is not configured" | Environment variables are missing; add them and redeploy |
| Clerk fails to load in production | DNS or SSL for clerk.your-domain not ready yet |
| Queries return nothing for signed-in users | Clerk is not added as a third-party provider in Supabase |
| Avatar upload fails | Migrations not applied, so the avatars bucket does not exist |
- Rename
PROJECTinsrc/lib/site.ts, and replacepublic/logo.png,src/app/icon.pngandsrc/app/apple-icon.png. - Delete the setup wizard (
src/app/setup,src/app/api/setup,src/lib/setup) once you no longer need it, and simplifysrc/proxy.ts. - Plug an error tracker into
src/app/error.tsxandsrc/app/global-error.tsx.