Single Next.js 16 process for both frontend and API. Auth (register / login / verify email / forgot+reset password / profile) wired end-to-end with HttpOnly cookie sessions, Prisma + Postgres, and Inngest for the email queue.
- Web + API: Next.js 16 (App Router) · React 19 · TypeScript
- UI: Tailwind v4 · shadcn/ui (new-york, gray) · lucide-react · react-hook-form + zod
- HTTP client: axios with relative
/apibase, HttpOnly cookie credentials - Database: Prisma + PostgreSQL
- Auth: HttpOnly cookie · JWT (HS256,
jose) · bcryptjs (12 rounds) - Background jobs: Inngest (email queue)
- Email: nodemailer + handlebars
- Node.js ≥ 20
- PostgreSQL running locally (or in Docker)
- Two terminals (one for Next, one for Inngest)
# 1. Install
npm install
# 2. Configure env
cp .env.example .env
# edit .env:
# - DATABASE_URL → your local Postgres
# - JWT_SECRET → any random string ≥ 32 characters
# - APP_URL → http://localhost:3000
# 3. Apply database schema
npx prisma migrate dev --name initTwo processes, two terminals:
# Terminal 1 — web + API
npm run dev# Terminal 2 — Inngest dev server (runs the email worker)
npx inngest-cli@latest devOpen:
- App → http://localhost:3000
- Inngest dashboard → http://localhost:8288 (see events fire, retry, etc.)
- Prisma Studio →
npm run prisma:studio
SMTP_URL is empty by default. The transport detects this and logs the rendered email body to the console instead of sending. You'll see the verify-email / reset-password / welcome-email content directly in your terminal whenever a flow triggers an email — no Mailtrap or SMTP setup needed.
To test against a real inbox, set SMTP_URL=smtp://user:pass@host:port (e.g. a Mailtrap sandbox URL) in .env.
| Task | Command |
|---|---|
| Update schema → migration | npx prisma migrate dev --name <name> |
| Inspect DB | npm run prisma:studio |
| Regenerate Prisma client | npm run prisma:generate |
| Add shadcn primitive | npx shadcn@latest add <name> |
| Typecheck | npx tsc --noEmit |
| Lint | npx eslint |
Set these on your hosting platform — never commit production secrets to git.
| Var | Required | Notes |
|---|---|---|
NODE_ENV |
yes | production |
APP_URL |
yes | Public origin, e.g. https://app.example.com |
DATABASE_URL |
yes | Postgres connection string. Use a connection pooler (PgBouncer / Neon / Supabase) for serverless. |
JWT_SECRET |
yes | Long random string (≥32 chars). Rotating this invalidates every active session. |
AUTH_COOKIE_NAME |
no | Default auth_token |
AUTH_COOKIE_MAX_AGE_SECONDS |
no | Default 604800 (7 days) |
SMTP_URL |
yes (prod) | Real SMTP for outbound mail |
MAIL_FROM_NAME / MAIL_FROM_EMAIL |
yes (prod) | Sender identity |
INNGEST_EVENT_KEY |
yes (prod, if using Inngest Cloud) | From Inngest dashboard |
INNGEST_SIGNING_KEY |
yes (prod, if using Inngest Cloud) | From Inngest dashboard |
VERIFY_EMAIL_EXPIRY_MINUTES |
no | Default 15 |
RESET_PASSWORD_EXPIRY_MINUTES |
no | Default 15 |
MAX_VERIFICATION_SEND_COUNT |
no | Default 5 |
The cookie is automatically set with Secure: true when NODE_ENV=production — make sure your prod URL is HTTPS.
npm install --omit=dev
npm run prisma:generate
npm run build
npm run start # serves on $PORT (default 3000)Run database migrations as a separate step before rolling out new code:
npx prisma migrate deployTwo options. Pick one:
-
Create an app at https://app.inngest.com.
-
Copy the Event Key and Signing Key into your env (
INNGEST_EVENT_KEY,INNGEST_SIGNING_KEY). -
Register your function endpoint with Inngest:
curl -X PUT https://<your-domain>/api/inngest
(or use the "Sync" button in the Inngest dashboard.)
That's it — the SDK posts events to Inngest Cloud, and Inngest invokes your /api/inngest endpoint to run functions. Free tier: 50K runs/month.
Run the open-source Inngest binary on your own infra (Docker, Kubernetes, bare metal — see https://github.com/inngest/inngest). Then point your app at it:
INNGEST_BASE_URL=https://inngest.your-infra.example.com
INNGEST_EVENT_KEY=<key you set on the self-hosted instance>
INNGEST_SIGNING_KEY=<key you set on the self-hosted instance>Caveat: the OSS binary is newer than the cloud version. Fine for low/medium traffic; load-test before betting heavily on it.
| Target | Notes |
|---|---|
| Vercel | Works out of the box. Use Inngest Cloud (Option A). Use a connection pooler for DATABASE_URL since each serverless invocation can open a new connection. |
| Docker | Standard next start container. npm run build then ship the whole project (or use Next's standalone output). Run prisma migrate deploy from a separate one-shot job before rolling out. |
| Node.js server (Render / Fly / Railway / your own VM) | Same as Docker. Add a health check on /. |
| Static export | Not supported — this app needs route handlers, cookies, and Prisma. |
-
JWT_SECRETis a fresh, long random value (not the dev one) -
APP_URLis HTTPS -
DATABASE_URLpoints to your prod Postgres, with pooling if serverless -
SMTP_URLset to real outbound SMTP (otherwise emails are silently logged, never sent) -
INNGEST_*keys set (Cloud) orINNGEST_BASE_URLset (self-host) - Inngest endpoint synced (Cloud only —
curl -X PUT https://<domain>/api/inngest) -
prisma migrate deployrun against prod DB - HTTPS works end-to-end so the Secure cookie attribute doesn't break login
See AGENTS.md for the full folder-by-folder breakdown, API contract, and conventions.
MIT.