Production-Ready, Security-Hardened Next.js 16 Starter
Full authentication, database flexibility, and modern tooling. Clone it, run one command to choose your database, and start building.
Spin up a secure, full-auth Next.js backend in under 60 seconds — with your choice of database.
Docs: https://nextforge.talhaahmad.me
No boilerplate. No repeated setup. No security gaps.
No leftover scaffold code. No vendor lock-in.
- 🔐 Complete authentication (email + Google OAuth)
- 🛡️ Security-first setup (XSS, SQLi/NoSQLi protection, rate limiting)
- 🗄️ Choose your database: MongoDB, PostgreSQL (Supabase), or Firebase (Firestore)
- 📧 Email verification + password reset flows
- 🧱 Clean, production-ready architecture
All with one command:
node setup.mjs→ Pick your stack. Ship.
- Developers tired of rebuilding auth and security every project
- Indie hackers shipping SaaS quickly
- Agencies handling multiple client backends
- Anyone who wants a secure default starting point
Most starters give you structure.
This gives you a production-ready backend system:
- OTPs are hashed (never stored in plaintext)
- Timing-safe comparisons prevent side-channel attacks
- MongoDB inputs sanitized against operator injection
- Parameterized queries for PostgreSQL
- Rate limits separated for auth and OTP endpoints
Security is not optional — it’s the default.
- Login / registration flows
- Email verification system
- Password reset logic
- Rate limiting
- Input validation
- Basic security protections
- ⚡ Next.js 16 with App Router and Turbopack
- 🔐 NextAuth v5 — Credentials + Google OAuth, JWT sessions
- 🗄️ Database choice — MongoDB (Mongoose), Supabase (PostgreSQL), or Firebase (Firestore) via interactive setup
- 📧 Full auth flow — Register, login, email verification, forgot/reset password
- 🌐 Boilerplate landing page — Useful
/homepage with quick auth/dashboard CTAs - 🚪 Dashboard logout control — Starter dashboard includes a working logout button wired to the active auth backend
- 🔑 OAuth UI included — Google sign-in buttons on both login and register forms
- 🔄 OAuth user sync included — Google sign-in now creates or updates the matching backend user in MongoDB, Supabase, and Firebase
- 🛡️ Security-hardened — bcrypt (cost 12), CSPRNG OTPs, timing-safe comparison, CSP headers, HSTS
- 🚫 Rate limiting — Upstash Redis, separate limits for auth and OTP endpoints
- 📨 Transactional email — Resend integration with HTML templates
- ✅ Zod validation — All inputs validated on the server before touching the database
- 🔒 NoSQL injection protection — Operator stripping for MongoDB, parameterized queries for Supabase
- 🎨 Tailwind CSS v4 — Dark mode, CSS variables, responsive design
- 🍞 Toast notification system — Accessible, animated, auto-dismissing
- 🧱 Reusable UI components — Button, Input (with icons), Modal, Loader, Toast
- 📡 Route protection —
proxy.tsmiddleware with configurable public/protected routes
git clone https://github.com/Talhaahmad9/nextforge.git my-project
cd my-projectnode setup.mjs┌──────────────────────────────────────────┐
│ NextForge — Database Setup │
└──────────────────────────────────────────┘
Which database backend do you want to use?
1) MongoDB (Mongoose)
2) Supabase (PostgreSQL)
3) Firebase (Firestore)
Enter 1, 2, or 3: _
The script will:
- Copy the correct database files into place
- Remove the unused variant's dependencies from
package.json - Generate a
.env.local.examplewith only the env vars you need - Delete
_variants/andsetup.mjsitself (clean slate)
npm installcp .env.local.example .env.localFill in .env.local — see the Environment Variables section below.
Open your Supabase project → SQL Editor → paste and run lib/db/schema.sql.
npm run devOpen http://localhost:3000.
├── _variants/ # Database variant files (deleted after setup)
│ ├── mongodb/ # Mongoose-based auth + OTP logic
│ ├── supabase/ # Supabase-based auth + OTP logic + SQL schema
│ └── firebase/ # Firebase-based auth + OTP logic
│
├── actions/
│ ├── auth.ts # loginAction, registerAction, verifyEmailAction, logoutAction
│ └── email.ts # sendPasswordResetOTPAction, resetPasswordAction, resendVerificationOTPAction
│
├── app/
│ ├── (auth)/
│ │ ├── login/ # /login
│ │ ├── register/ # /register
│ │ ├── verify-email/ # /verify-email
│ │ ├── forgot-password/ # /forgot-password
│ │ └── reset-password/ # /reset-password
│ ├── dashboard/ # /dashboard (protected route)
│ ├── api/auth/[...nextauth]/ # NextAuth handler
│ ├── error.tsx # Global error boundary
│ ├── not-found.tsx # 404 page
│ ├── layout.tsx # Root layout
│ └── globals.css # Tailwind + CSS variable design tokens
│
├── components/
│ ├── auth/ # Form components for each auth page
│ ├── nav/ # Navbar, MobileMenu, NavLinks, ThemeToggle
│ ├── footer/ # Footer
│ └── ui/ # Button, Input, Modal, Loader, Toast
│
├── hooks/
│ └── useToast.tsx # Toast state + provider
│
├── lib/
│ ├── auth.ts # NextAuth configuration
│ ├── db/
│ │ ├── mongo.ts # Mongoose connection (MongoDB variant)
│ │ ├── models/ # User + OTP Mongoose models
│ │ └── supabase.ts # Supabase client — publishable + secret key (Supabase variant)
│ ├── email.ts # Resend email templates + sender
│ ├── ratelimit.ts # Upstash Redis rate limiters
│ ├── sanitize.ts # XSS escaping + (MongoDB) NoSQL injection stripping
│ ├── tokens.ts # OTP generation, hashing, verification
│ └── validate.ts # Zod schemas for all forms
│
├── proxy.ts # Next.js 16 middleware (route protection + security headers)
├── setup.mjs # Interactive database setup script
└── types/
├── index.ts # ActionState, shared types
└── next-auth.d.ts # NextAuth session type extensions
- User submits name, email, password
- Server: Zod validation → sanitize → rate limit → check duplicate → bcrypt hash → create user → generate OTP → send verification email
- User is redirected to
/verify-email
- User submits the 6-digit OTP from their email
- Server: validate → find valid (unused, unexpired) OTP → timing-safe hash comparison → mark used → mark user verified
- User submits email + password
- NextAuth Credentials provider: look up user →
bcrypt.compare→ return user object → JWT minted - JWT stored as HTTP-only cookie; session available via
auth()oruseSession() - Optional Google OAuth flow is available directly from login/register UI buttons
- Dashboard logout button posts to the shared server action and redirects back to
/login
- Handled by NextAuth's Google provider
- UI button is included on both
/loginand/register - First Google sign-in creates or updates the matching backend user record automatically
- JWT/session state is rehydrated from the database so
id,role, and verified status match the canonical user record - No additional setup beyond
AUTH_GOOGLE_ID/AUTH_GOOGLE_SECRET
- User submits email → server checks if user exists (generic response to prevent email enumeration) → generates OTP → sends reset email
- User submits email + OTP + new password → validate OTP → mark used → bcrypt hash new password → update user
| Layer | Implementation |
|---|---|
| Password hashing | bcrypt, cost factor 12 |
| OTP generation | crypto.randomInt (CSPRNG), 6-digit |
| OTP storage | SHA-256 hash stored, plaintext never persisted |
| OTP verification | crypto.timingSafeEqual — constant-time comparison |
| OTP expiry | 10 minutes, checked at query time |
| NoSQL injection | stripMongoOperators strips $ / . keys from user input (MongoDB) |
| SQL injection | Supabase JS client uses parameterized queries |
| XSS | escapeHTML applied to all user-supplied string inputs |
| Rate limiting | Upstash Redis sliding window — 5 requests/15 min auth, 3 requests/10 min OTP |
| Session | JWT strategy, AUTH_SECRET required in all environments |
| Route protection | proxy.ts middleware — unauthenticated → /login, authenticated + verified → allowed |
| Security headers | CSP, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, HSTS (production) |
| Email enumeration | Password reset and OTP resend always return the same generic response |
- Reset-password and email-verification flows now validate
emaildirectly in Zod schemas. - Protected route guard now blocks both unauthenticated users and authenticated-but-unverified users.
- Password reset OTP send now surfaces provider send failures instead of silently returning success.
- Removed
mongoose-sanitizeplugin usage in favor of explicit input sanitization utilities.
After running setup.mjs, your .env.local.example will contain only the variables relevant to your chosen database.
# NextAuth — generate with: openssl rand -base64 32
AUTH_SECRET=
# Google OAuth (optional — remove Google provider from lib/auth.ts if unused)
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
# Resend (transactional email)
RESEND_API_KEY=
RESEND_FROM_EMAIL=no-reply@yourdomain.com
# Upstash Redis (rate limiting)
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
# Public app config
NEXT_PUBLIC_APP_NAME=MyApp
NEXT_PUBLIC_APP_URL=https://yourdomain.com# Full Atlas connection string
MONGODB_URI=mongodb+srv://user:pass@cluster.mongodb.net/mydb?retryWrites=true&w=majorityNEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...
SUPABASE_SECRET_KEY=sb_secret_... # ⚠️ server-only — never expose to the browserFIREBASE_PROJECT_ID=my-project-id
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxx@my-project-id.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"- MongoDB: use a valid Atlas URI, and ensure your network/IP access allows your machine.
- Supabase: run
lib/db/schema.sqlbefore testing auth flows; use new keys only (NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,SUPABASE_SECRET_KEY). - Firebase: use Admin service account values (not web SDK config), include full PEM wrappers in
FIREBASE_PRIVATE_KEY, create Firestore(default), and create required composite index when prompted. - Google OAuth: after Google is configured, first sign-in will create or update the matching backend user record for the selected database variant.
Detailed guides:
- MongoDB: https://nextforge.talhahmad.me/documentation/database/mongodb
- Supabase: https://nextforge.talhahmad.me/documentation/database/supabase
- Firebase: https://nextforge.talhahmad.me/documentation/database/firebase
The scaffold ships with three production-ready database backends. The auth logic, OTP flow, and security model are identical — only the data access layer differs.
- Mongoose models with pre-save bcrypt hook (
UserModel,OTPModel) - TTL index on
OTPModel.expiresAt— MongoDB auto-deletes expired OTPs stripMongoOperatorsstrips$and.keys from all user input before queries- Google OAuth upserts the user into MongoDB and marks the account verified for protected-route compatibility
- Two tables:
usersandotps(schema atlib/db/schema.sql) - Supabase JS client with publishable key (browser-safe) and secret key (server-only)
.gt('expires_at', ...)for expiry checking — equivalent to MongoDB's$gt- Optional pg_cron job for periodic OTP cleanup (see
schema.sqlfor instructions) bcrypt.comparein server actions — passwords are never handled by Supabase Auth- Google OAuth upserts the user row by email;
passwordremains nullable for OAuth-only users
- Two collections:
usersandotps - Server-side only integration using
firebase-admin - NextAuth Credentials integration checking against Firestore documents
- Expiry checking done in-memory on the server after querying
bcrypt.comparein server actions — passwords are never handled by native Firebase Auth- Google OAuth creates or updates the matching
usersdocument and reuses the same session shape as credentials login
# Clone the scaffold as your new project
git clone https://github.com/Talhaahmad9/nextforge.git my-new-project
cd my-new-project
# Pick your database — this is a one-time operation
node setup.mjs
# Install (setup.mjs already patched package.json)
npm install
# Set env vars
cp .env.local.example .env.local
# Edit .env.local
# Point to your own repo
git remote set-url origin https://github.com/YourUsername/my-new-project.git
git add -A && git commit -m "chore: init from scaffold"
git push -u origin main
# Start building
npm run devAfter setup.mjs runs, the project is a completely standard Next.js app — no scaffold-specific code remains.
Edit proxy.ts:
const PUBLIC_ROUTES = ["/", "/login", "/register", "/forgot-password"];
const AUTH_ROUTES = ["/login", "/register"]; // redirect to /dashboard if already logged inAdd it to the providers array in lib/auth.ts:
import GitHub from "next-auth/providers/github";
// ...
providers: [
Credentials({ ... }),
Google({ ... }),
GitHub({ clientId: process.env.AUTH_GITHUB_ID, clientSecret: process.env.AUTH_GITHUB_SECRET }),
],In lib/tokens.ts:
export const OTP_EXPIRY_MINUTES = 10; // change thisIn lib/ratelimit.ts:
export const authRatelimit = new Ratelimit({
limiter: Ratelimit.slidingWindow(5, "15 m"), // 5 requests per 15 minutes
});The role field ("user" | "admin") is already on the user model and propagated through the JWT. Use it in server actions:
const session = await auth();
if (session?.user.role !== "admin") {
return { success: false, error: "Forbidden." };
}| Category | Technology |
|---|---|
| Framework | Next.js 16 |
| Auth | NextAuth v5 |
| Database (pick one) | MongoDB Atlas + Mongoose · Supabase · Firebase |
| Resend | |
| Rate Limiting | Upstash Redis |
| Validation | Zod |
| Styling | Tailwind CSS v4 |
| Icons | Lucide React |
| Password Hashing | bcryptjs |
| Language | TypeScript |
npm run dev # Start dev server (Turbopack)
npm run build # Production build
npm run start # Start production server
npm run lint # ESLint
node setup.mjs # Database variant setup (run once, after cloning)MIT — use freely in personal and commercial projects.