A production-ready Next.js 16 starter template with passwordless authentication, internationalization, Azure Cosmos DB, and a modern UI stack.
- Passwordless Auth — OTP + magic link login with session-based security
- Internationalization — English and Dutch out of the box via next-intl
- Azure Cosmos DB — Typed collections for users, sessions, and verification tokens
- Dark Mode — Theme switching with next-themes
- Modern UI — HeroUI components, Tailwind CSS v4, Framer Motion animations
- Turbopack — Fast dev and build with Next.js Turbopack
- Docker Ready — Multi-stage Dockerfile with standalone output
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, React 19) |
| Language | TypeScript (strict mode) |
| Styling | Tailwind CSS v4, HeroUI, Framer Motion |
| Auth | Passwordless OTP + magic links, session cookies |
| Database | Azure Cosmos DB |
| Resend | |
| i18n | next-intl (en, nl) |
| Icons | Lucide React, Iconify |
| Package Manager | pnpm |
- Node.js 20+
- pnpm 10.22+
- Azure Cosmos DB account (or emulator)
- Resend account (for sending emails)
Create a .env.local file in the project root:
LOG_LEVEL=DEBUG | INFO | WARN | ERROR
# Database
NEXT_PRIVATE_COSMOS_DB_URI=your-cosmos-db-uri
NEXT_PRIVATE_COSMOS_DB_KEY=your-cosmos-db-key
NEXT_PRIVATE_COSMOS_DB_NAME=your-database-name
# Email (Resend)
NEXT_PRIVATE_RESEND_API_KEY=your-resend-api-key
EMAIL_FROM=noreply@yourdomain.com
# App
NEXT_PUBLIC_APP_URL=http://localhost:3000pnpm install
pnpm devOpen http://localhost:3000.
pnpm build # Production build with Turbopack
pnpm start # Start production server
pnpm lint # Run ESLintsrc/
├── app/
│ ├── [locale]/ # i18n pages (en, nl)
│ │ ├── page.tsx # Landing page
│ │ ├── landing-page.tsx
│ │ └── layout.tsx # Root layout with providers
│ └── api/auth/ # Auth API routes
│ ├── send-otp/
│ ├── verify-otp/
│ ├── resend-otp/
│ ├── magic-link/
│ ├── session/
│ └── signout/
├── components/
│ ├── auth/ # LoginModal, UserDropdown, OTPInput
│ ├── ui/ # LanguageSwitcher, LanguageModal
│ ├── navbar.tsx
│ ├── footer.tsx
│ ├── theme-switcher.tsx
│ └── providers.tsx # Theme, HeroUI, Auth, i18n providers
├── contexts/
│ └── auth-context.tsx # AuthProvider + useAuth() hook
├── i18n/
│ ├── routing.ts # next-intl routing config
│ └── request.ts # next-intl request config
├── lib/
│ ├── actions/ # Server actions (session, user, email)
│ ├── auth/ # Auth helpers (validateRequest, requireAuth)
│ ├── helper/ # Logger, rate limiting, request context
│ ├── db.ts # Cosmos DB client + containers
│ └── utils.ts # cn() utility
├── proxy.ts # i18n locale detection proxy (Next.js 16)
├── styles/
│ └── globals.css # Tailwind config, CSS utilities
└── types/ # TypeScript type definitions
messages/
├── en.json # English translations
└── nl.json # Dutch translations
- User enters email in the login modal
- Server generates a 6-digit OTP and magic link, sends via Resend
- User enters OTP or clicks the magic link
- Server verifies the token, creates/finds user, issues a session cookie
- Session token is SHA256-hashed, base32-encoded, stored as httpOnly secure cookie
- Sessions expire after 30 days with auto-refresh at the 50% mark
- Client polls
/api/auth/sessionevery 5 minutes to stay in sync
- Locales:
en(default, no URL prefix) andnl(prefixed/nl/) - Translation files in
messages/en.jsonandmessages/nl.json - Use routing helpers from
@/i18n/routing(notnext/navigation) - Proxy (
src/proxy.ts) handles locale detection and routing
Resend is used to send OTP codes and magic link emails.
- Create an account at resend.com
- Go to API Keys in the dashboard and click Create API Key
- Give it a name (e.g.
acme-web), select Sending access, and choose your verified domain (or use the defaultonboarding@resend.devfor testing) - Copy the generated key — this is your
NEXT_PRIVATE_RESEND_API_KEY - Go to Domains and add your domain (e.g.
yourdomain.com). Follow the DNS verification steps (add the MX, SPF, and DKIM records Resend provides) - Once verified, set
EMAIL_FROMto your sender address (e.g.noreply@yourdomain.com)
NEXT_PRIVATE_RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxx
EMAIL_FROM=noreply@yourdomain.comFor local development, you can use
onboarding@resend.devasEMAIL_FROMwithout domain verification — emails will be sent to the Resend dashboard only.
Azure Cosmos DB is the database for users, sessions, and verification tokens.
- Go to the Azure Portal and create a new Azure Cosmos DB for NoSQL account
- Once provisioned, go to Keys in the left sidebar
- Copy the URI and PRIMARY KEY — these are your
NEXT_PRIVATE_COSMOS_DB_URIandNEXT_PRIVATE_COSMOS_DB_KEY - Go to Data Explorer and create a new database (e.g.
acme-db) — this is yourNEXT_PRIVATE_COSMOS_DB_NAME - Inside the database, create three containers with these exact names and partition keys:
| Container | Partition Key |
|---|---|
users |
/email |
sessions |
/userId |
verificationTokens |
/userId |
NEXT_PRIVATE_COSMOS_DB_URI=https://your-account.documents.azure.com:443/
NEXT_PRIVATE_COSMOS_DB_KEY=your-primary-key-here
NEXT_PRIVATE_COSMOS_DB_NAME=acme-db- Download and install the Azure Cosmos DB Emulator
- Start the emulator — it runs on
https://localhost:8081by default - Open the emulator's Data Explorer at
https://localhost:8081/_explorer/index.html - The emulator uses a well-known key for local development:
NEXT_PRIVATE_COSMOS_DB_URI=https://localhost:8081
NEXT_PRIVATE_COSMOS_DB_KEY=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
NEXT_PRIVATE_COSMOS_DB_NAME=acme-db- Create the database and three containers (same names and partition keys as above) via the Data Explorer
| Container | Partition Key | Purpose |
|---|---|---|
users |
/email |
User accounts and profile metadata |
sessions |
/userId |
Active auth sessions (30-day expiry) |
verificationTokens |
/userId |
OTP codes and magic link tokens |
Partition keys are critical for query performance. Lookups by
idinstead of the partition key require cross-partition queries, which are slower and more expensive.
docker compose up --buildMulti-stage build targeting Node 20-alpine with standalone output. Runs as non-root nextjs user on port 3000. Docker Compose allocates 4 CPU / 4-8 GB RAM.
Required environment variables must be set in the host environment or a .env file for Docker Compose to pass them to the container.
MIT