Every serious Next.js project I start needs the same fifteen things before the actual product exists: authentication, layouts, permissions, environment validation, an API client, i18n, UI primitives, SEO metadata, tests, linting, formatting, Docker, CI, error tracking, analytics.
None of that is the product. All of it has to exist before the product can ship.
I kept rebuilding this foundation project after project, and each time I rebuilt it slightly differently, which meant relearning my own past decisions instead of improving on them. So I wrote down what "doing it right" actually means for a Next.js app in 2026, and then built that answer into an open-source boilerplate, Next Elite, so the decision only has to happen once.
This post is that answer. It is not a features list. For each practice I'll explain the problem it solves, where developers disagree about it, and then show concretely how Next-Elite implements it, distinguishing clearly between "this is a 2026 best practice" and "this is a choice I made in my own boilerplate."
If you never touch Next-Elite, the checklist at the end still works as a reference for starting a serious Next.js project in 2026.
Table of Contents
- What "production-ready" actually means in 2026
- Start with architecture, not components
- Frontend-first and API-driven architecture
- Server Components vs Client Components
- Authentication is not authorization
- RBAC without scattering permission logic everywhere
- TypeScript should enforce architecture, not just describe it
- Type-safe internationalization
- UI architecture with Tailwind + shadcn/ui
- Data fetching: don't turn everything into React state
- Environment variables should fail safely
- SEO should be part of the architecture
- Performance should be a default, not a final optimization
- Testing strategy
- Code quality is an engineering system
- Observability from day one
- Docker and deployment should be boring
- What I intentionally did NOT include
- The trade-offs
- The actual boilerplate: Next Elite
- Final checklist: my 2026 Next.js project checklist
1. What "production-ready" actually means in 2026
"Production-ready" gets used as a synonym for "has TypeScript, Tailwind, and an auth library." That's not what it means. A project with all three of those can still fall over the first time two developers touch the same file, or the first time someone forgets to set an environment variable in a new deploy target.
In practical terms, production readiness in 2026 means:
- Maintainability: a new developer can find where a feature lives without a guided tour.
- Type safety that extends past props: config, permissions, translations, and API responses are typed, not just component inputs.
- Predictable architecture: the same kind of problem is always solved the same way.
- Security boundaries that live on the server: the UI can hide a button; only the server can actually deny a request.
- Testing at more than one layer: unit, component, and end-to-end each catch a different class of bug.
- Observability before the incident: error tracking and health checks exist before you need them, not after the first outage.
- A deployment story that doesn't force a rewrite: whether that's Vercel or your own Docker host.
- Performance and SEO as defaults, not a pre-launch checklist item.
None of these require exotic tools. Most of them require deciding early and enforcing the decision consistently, which is a cultural problem as much as a technical one.
2. Start with architecture, not components
The default Next.js tutorial structure looks like this:
src/
components/
hooks/
utils/
app/
This works until it doesn't. Once a project has more than a handful of screens, "components" stops meaning anything: it holds unrelated UI for auth, billing, and settings side by side, and touching one feature means opening files scattered across four folders. Notably, Next.js's own documentation doesn't mandate a single structure; the framework explicitly supports colocating by route, splitting by feature, or keeping a flat top-level layout, and leaves the choice to the team. There is no "correct" Next.js folder structure handed down by the framework, which is exactly why so many teams land on the same fix independently: group code by feature, not by file type.
Next-Elite structures its domain logic under src/features/, where each feature owns its own hooks, schemas, server helpers, and types:
src/features/
├── auth/
│ ├── lib/ # BetterAuth server + auth-client singletons
│ ├── server/ # Server-only helpers (getCurrentUser)
│ ├── hooks/ # Auth provider + useAuth hook
│ ├── rbac/ # permissions, roles, can, require
│ ├── schemas/ # Zod login + register schemas
│ └── types.ts
├── i18n/ # next-intl config (routing, request, locale actions, RTL hook)
└── site/ # site.config.json, its schema, server-only base URL, locale helpers
Shared, feature-agnostic UI still lives centrally, under src/components/ (ui/ primitives, shared/ app pieces, layout/ shell, icons/), because forcing genuinely reusable building blocks into a feature folder just relocates the mess instead of solving it. The rule Next-Elite follows is: if it's about one feature, it lives in that feature's folder; if it's used by every feature, it's shared.
Demo code gets the same treatment: the landing page, the component showcase, and the demo accounts all live in one src/_demo(DELETE)/ folder, and npm run demo:remove deletes it and rewrites every file that referenced it. CI runs that removal on every push, so "delete the demo" never turns into an afternoon of chasing broken imports.
When this is worth it: SaaS apps with multiple roles, dashboards with several independent domains (billing, users, settings), or any project a second developer will touch in six months.
When it's over-engineering: a landing page, a five-screen MVP, or a prototype you'll throw away in a month. Feature folders for a project with one feature is just extra typing.
3. Frontend-first and API-driven architecture
This is the architectural decision Next-Elite is built around, and it's the one most likely to get pushback, so it deserves the most honest treatment.
Most Next.js boilerplates assume the framework owns the whole stack: Prisma or Drizzle, a Postgres instance, an ORM schema tightly coupled to the UI. That's a completely reasonable choice for a small product where Next.js genuinely is the backend. It is a much worse fit the moment you already have a backend team, an existing service in Go or Laravel, or a plan to eventually ship a mobile client against the same API.
Next-Elite deliberately does not own a database. It's built to sit on top of whatever you already have, REST, GraphQL, or a BFF layer, and treats TanStack Query as the seam between the frontend and that backend. The trade-off is explicit:
| Frontend owns the DB (Next.js as full stack) | Frontend is API-driven (Next-Elite's approach) | |
|---|---|---|
| Best for | Solo builder, single client, fastest path to a working app | Existing backend, multiple clients, a team that owns the API separately |
| Cost | Fewer moving parts, one deploy | An API contract to define and maintain |
| Risk | Frontend and backend rewrite together if you ever split them | None, but you're doing more upfront wiring |
| Mobile/second client later | Usually means duplicating backend logic | Already speaks the same API |
Neither option is "more architecturally correct." The mistake is picking the database-coupled path by default and discovering the coupling problem only after the product needs a second client or a separate backend team. Next-Elite's stance is: decide this on purpose, and if you decide you want the database bundled in, it isn't the right starting point for you, several other well-known Next.js starters (ones that ship Prisma, Postgres, and multi-tenancy out of the box) solve that problem well and are worth using instead. (For the backend side, I'm also building a companion template, Next Elite Server, with NestJS, BetterAuth, Drizzle ORM, and PostgreSQL. It's coming soon.)
4. Server Components vs Client Components
Recommended practice: default to Server Components for everything, and only add 'use client' at the smallest possible boundary, a button, a form, a toggle, not an entire page. Next.js 16's App Router still renders Server Components first; the client boundary is where interactivity, browser APIs, or local state actually live. This isn't a style preference so much as a bundle-size decision: every client component is JavaScript the browser has to download, parse, and hydrate, and the newer primitives React 19.2 introduced (View Transitions, useEffectEvent, <Activity>, shipped as part of the Next.js 16 line) all assume this server-first default rather than replacing it.
How Next-Elite handles it: the request lifecycle is intentionally linear. A page renders as a Server Component first, requireUser() / requirePermission() check the session there, translations are resolved server-side via next-intl, and only the interactive parts (forms, the theme toggle, the query-backed lists) cross into 'use client':
flowchart TB
User([User]) --> Page
subgraph Server["Server (runs first)"]
Page[Page / Layout]
Guard["requireUser() / requirePermission()"]
Page --> Guard
Guard -->|reads session + role| Auth[BetterAuth]
end
subgraph Client["Client (runs in browser)"]
Hook["useQuery (TanStack Query)"]
Hook -->|fetch| Api["Your backend API"]
end
Page -->|sends HTML| User
Page -.->|interactive parts| Hook
The practical rule Next-Elite's structure encodes: if a component needs useState, an event handler, or a browser API, it's a client component. If it just needs data and permission, it stays on the server. The boundary also matters for what crosses it: server-only values are passed down as props instead of imported into client code. For example, the "Google sign-in enabled" flag is read from the validated env on the server and handed to the auth provider, so the env validation library never ends up in the browser bundle.
5. Authentication is not authorization
These get conflated constantly, and the conflation is where security bugs come from.
- Authentication answers: who are you?
- Authorization answers: what are you allowed to do, now that I know who you are?
A login form is authentication. A dashboard that only admins can see is authorization. Treating "the user is logged in" as sufficient to gate an admin page is a common and avoidable mistake.
There's also a sharper, more recent lesson here. In 2025, a critical Next.js vulnerability (CVE-2025-29927) let attackers send a specific header and skip middleware entirely, bypassing any authentication checks that lived only in middleware. The deeper lesson wasn't "patch your dependencies" (although do that too), it was architectural: middleware is not a security boundary. It's an edge layer meant for routing and response shaping, not the last line of defense. Real authorization checks have to live in your Server Components, Route Handlers, and Server Actions, where they can't be skipped by a crafted request.
Recommended practice: authenticate however fits your product (email/password, OAuth, passkeys), but authorize on the server, close to the data, every time, not once in middleware and never again.
How Next-Elite handles it: authentication is BetterAuth, wired for email/password and Google OAuth through /api/auth/* route handlers, with admin emails configurable via the server-only AUTH_ADMIN_EMAILS variable. Authorization is separate and server-side, and the project has no middleware at all, so there's no edge layer to bypass:
// Server Component example
import { requirePermission } from '@/features/auth/rbac/require';
import { getTranslations } from 'next-intl/server';
const AdminDashboardPage = async () => {
const [, t] = await Promise.all([
requirePermission('dashboard.view:admin'),
getTranslations('dashboard.admin'),
]);
return <h1>{t('title')}</h1>;
};
export default AdminDashboardPage;
requirePermission runs in the Server Component itself and redirects to /login (no session) or /unauthorized (no permission) before any protected content renders. One limitation to know before you deploy: with no database configured, BetterAuth falls back to its built-in in-memory store. That's ideal for the demo and local development, but users and sessions are lost on restart and aren't shared between server instances, so production needs a real database adapter (or auth pointed at your own backend). The project's production checklist calls this out explicitly.
6. RBAC without scattering permission logic everywhere
Recommended practice: roles and permissions should be defined in one place, checked through a small set of functions, and never re-implemented as ad hoc if (user.role === 'admin') checks scattered through components. Scattered checks are how a permission gets forgotten on the one page nobody remembered to update.
It's also worth being honest that RBAC is not always the right amount of complexity. A single-tenant internal tool with one kind of user needs an authentication check, full stop. RBAC earns its cost when a product has genuinely different classes of users with different capabilities, not by default.
How Next-Elite handles it: RBAC is deliberately simple, two roles (user, admin) with permission strings, checked through requireUser() and requirePermission() on the server, plus Next.js parallel routes so role-specific layouts don't need runtime branching in the render tree at all. Both roles share the same URLs, and the layout mounts the right slot:
src/app/(protected)/
├── @admin/ # Admin slots (dashboard, analytics, profile, settings)
├── @user/ # User slots (dashboard, analytics, profile, settings)
├── unauthorized/ # Shown when a role lacks permission
└── layout.tsx # Picks slot based on permissions
Adding a role is a three-step, explicit process: extend the UserRole union in permissions.ts, map its permissions in ROLE_PERMISSIONS in the same file, and optionally add a parallel route slot. It intentionally does not attempt multi-tenant, hierarchical, or resource-scoped permissions; for complex multi-tenant permission graphs, that logic belongs on your main API, not bolted onto a frontend starter. The UI can still hide a disabled button for a role that lacks a permission (useAuth().hasPermission() exists for exactly that), which is good UX, but the enforcement that matters happens in requirePermission, on the server, where a hidden button can't be bypassed by editing the DOM.
7. TypeScript should enforce architecture, not just describe it
Recommended practice: TypeScript's value in 2026 isn't "the compiler catches typos." It's using the type system to make invalid states hard to represent at all, typed config, typed environment variables, typed permission strings, typed translation keys, so an entire category of runtime bug turns into a compile-time error instead.
How Next-Elite handles it: this shows up in four concrete places rather than as a slogan:
-
Environment variables are validated and typed through T3 Env (
src/libs/env.ts), so a malformed value, like anNEXT_PUBLIC_APP_URLthat isn't a URL or aBETTER_AUTH_SECRETshorter than 32 characters, fails loudly at startup instead of surfacing as a broken feature later. -
Translation keys are type-checked through
next-intl;t('dashboard.title')only compiles if that key exists inmessages/en.json, so a typo'd key fails the build instead of silently rendering nothing. -
RBAC permissions are a typed union (
UserRoleandAuthPermissioninpermissions.ts), sorequirePermission('dashbord.view:admin'), misspelled, is a type error, not a bug that ships and gets discovered by a user with too much access. -
Site config has a Zod schema that defines its type, and a unit test validates
site.config.jsonagainst it, so the config is typed in the editor and checked in CI without shipping the validator to the browser.
TypeScript 7 in strict mode (with noUncheckedIndexedAccess) across the codebase is table stakes; the part that actually prevents bugs is using types to encode business rules (valid roles, valid locales, valid config shape), not just typing function signatures.
8. Type-safe internationalization
Recommended practice: i18n bolted on at the end of a project usually means hardcoded strings scattered through components, a hunt-and-replace refactor, and a translation key system with no compile-time safety, so a typo'd key just renders blank in production. It also usually means nobody thought about RTL languages until a customer in an Arabic-speaking market asks why the layout is broken. RTL isn't a CSS afterthought, it changes how you think about "start" and "end" instead of "left" and "right" throughout the component tree, which is much cheaper to design in from the start than to retrofit.
How Next-Elite handles it: next-intl provides cookie-based locale switching (a NEXT_LOCALE cookie set by a Server Action, no /en/ or /es/ URL prefix) across six languages, including Arabic with RTL support, and translation keys are type-checked against messages/en.json at compile time. A unit test also checks that every locale file has exactly the same keys as English, so a missing translation fails CI. Layout code uses logical utilities (ms-*/me-*, ps-*/pe-*, border-e) instead of left/right, so the same components work in both directions. Adding a language is three steps: add the locale to site.config.json, create messages/<locale>.json, and the runtime and types pick it up automatically.
The trade-off is explicit: cookie-based locale means simpler routing, but it has two costs. First, if you need path-prefixed URLs for per-locale SEO indexing (/es/dashboard ranking separately from /en/dashboard), that requires a custom router setup Next-Elite doesn't provide out of the box. Second, because the server has to read the cookie to know the language, public pages render on every request instead of being statically cached at the CDN. For an app that lives mostly behind a login, that's the right call; for a large multilingual marketing site, it's a real limitation to know about before you start.
9. UI architecture with Tailwind + shadcn/ui
Recommended practice: treat shadcn/ui as a foundation you own and edit, not a component library you import and never touch. Its actual value in 2026 isn't the number of components available, it's that the code lives in your repo, built on accessible Radix primitives with Tailwind and CVA for variants, so you can change a button's behavior without fighting a black-box dependency's API surface. The second half of the practice is a design system underneath it: colors, radii, and spacing come from tokens, never from hard-coded hex values, so dark mode and rebranding are a token change instead of a find-and-replace.
How Next-Elite handles it: the boilerplate ships 50+ of these primitives and custom components under src/components/ui/, including stat cards, an area chart, a sortable and paginated data table, a searchable combobox, and a calendar/date picker, plus a live /ui-components showcase page so you can see what exists before building a new one from scratch:
Every color and radius comes from light and dark tokens in src/app/styles/tokens.css, mapped to Tailwind utilities. The number isn't the point; the point is that composition and accessibility come for free because they're built on Radix, and because the components are just code in your repo, extending one (a new variant) doesn't mean waiting on an upstream release. Domain-specific UI (the auth forms, the dashboard shell) lives separately under src/components/auth/ and src/components/layout/, so the line between "generic primitive" and "feature-specific composition" stays visible in the folder structure itself.
10. Data fetching: don't turn everything into React state
Recommended practice: Server Components and TanStack Query solve different problems, and the 2026 consensus (backed by both the Next.js docs and the wider React ecosystem) is that they're complementary, not competing. Server Components are the right tool for the initial, server-known render, a dashboard's first paint, a product list. TanStack Query is the right tool for anything that needs client-side behavior after that: pagination, optimistic mutations, background refetching, stale-while-revalidate caching, infinite scroll. Reaching for useEffect plus manual useState for server data in 2026 means rebuilding caching, retries, and race-condition handling that TanStack Query already solved. The inverse mistake also happens: wrapping genuinely static, server-known data in a client-side query hook just adds a network waterfall and a loading spinner where a Server Component would have sent the HTML already rendered.
How Next-Elite handles it: QueryClientProvider is pre-configured in src/app/providers.tsx with sensible defaults (a 60-second staleTime, 5-minute gcTime, one retry, no refetch on window focus), ready to wire useQuery / useMutation against your REST, GraphQL, or BFF endpoints. It's deliberately not mandatory scaffolding around every fetch; server-renderable data still belongs in the Server Component that renders it.
11. Environment variables should fail safely
Recommended practice: a misconfigured environment variable should be a loud error, not a silent undefined that surfaces as a broken feature in production three weeks later. This means validating both server-only and public (NEXT_PUBLIC_*) variables against a schema, and being deliberate about which is which, since a secret accidentally marked public ships straight to the browser bundle.
How Next-Elite handles it: variables are validated by src/libs/env.ts via T3 Env, and that module is only imported by server code, so the validation library itself doesn't ship to the browser. A couple of specific decisions worth calling out because they're not the only valid choice: BETTER_AUTH_URL is optional and derives from VERCEL_URL in production rather than requiring manual configuration on every deploy, and a missing BETTER_AUTH_SECRET logs a warning instead of crashing the build, a deliberate softness so local development and CI don't grind to a halt over a secret you'll set before the real deploy. SKIP_ENV_VALIDATION=true exists specifically for CI and Docker build steps where secrets genuinely aren't available yet at build time.
12. SEO should be part of the architecture
Recommended practice: metadata, Open Graph tags, a sitemap, robots.txt, and a web manifest shouldn't be a checklist item the week before launch, scattered across whatever files happened to need them at the time. Centralizing them means updating your domain or title once, not hunting through six files hoping you didn't miss one.
How Next-Elite handles it: src/features/site/site.config.json is the single source of truth:
{
"appName": "Next Elite",
"domain": "https://yourdomain.com",
"tagline": "Frontend-first, API-driven, batteries included.",
"title": "Next Elite - Production-Ready SaaS Boilerplate",
"description": "Frontend-first Next.js 16.4 + React 19 boilerplate with i18n, RBAC and BetterAuth."
}
The root layout's metadata and JSON-LD structured data, sitemap.ts, robots.ts, and manifest.ts all read from this one file (with NEXT_PUBLIC_APP_URL able to override the domain), along with the locale configuration, so localized routes and PWA metadata stay in sync with the rest of the i18n setup instead of drifting from it. There's also a /llms.txt file, a machine-readable summary for AI crawlers and assistants, which is a small 2026 addition that costs nothing and helps AI tools describe your product accurately.
13. Performance should be a default, not a final optimization
Recommended practice: the practical levers in 2026 are the same ones they've been for a few years, rendering on the server by default, minimizing the client JavaScript boundary, using next/image correctly (setting priority and sizes deliberately rather than accepting defaults everywhere), and measuring instead of guessing. What's changed is the tooling underneath: Turbopack graduated to stable as the default bundler in Next.js 16, and the 16.x line has continued shipping real, measured startup and rendering improvements release over release. None of that replaces measuring your specific app; bundle analysis and a Lighthouse run tell you where your app actually spends its budget, which is never quite the generic advice.
One tooling detail that catches people out: @next/bundle-analyzer only works with webpack, so under Turbopack it silently does nothing. Use Next's built-in Turbopack analyzer instead (npx next experimental-analyze).
How Next-Elite handles it: I ran exactly that analysis on Next-Elite and it found three things that were on every page without being needed there: the Sentry browser SDK (even when no DSN was set), Zod and the env validator (pulled into the browser through the auth provider and site config), and the ApexCharts library (loaded as soon as any chart rendered, even below the fold). The fixes were small and boring:
- Sentry now loads lazily, and only when
NEXT_PUBLIC_SENTRY_DSNis set, including inside the error boundaries. - Server-only values are passed to client components as props, and the site config is validated in tests instead of in the browser.
- Charts load their library only when they scroll into view.
- Only the font subset the app actually uses is loaded.
Together that cut first-load JavaScript by 21–24% per page (the home page went from 425 KB to 336 KB gzipped), and the live demo's total transfer roughly halved, from 906 KB to 462 KB.
The project's Lighthouse report shows 100 across Performance, Accessibility, Best Practices, and SEO, and 3/3 on Agentic Browsing:
That number describes the starting point; it's not a guarantee that survives you adding a heavy client-side library without thinking about it. That's exactly why the analyzer step belongs in your workflow, not just in a launch checklist.
14. Testing strategy
Recommended practice: unit and component tests catch logic bugs and rendering regressions fast, cheaply, and in isolation. End-to-end tests catch the thing unit tests structurally can't, whether the whole system actually works together, but they're slower and more expensive to maintain, which means writing them for the handful of flows that actually matter (login, the core happy path) rather than trying to E2E everything. Setting up this infrastructure after a project is already painful to test is a common regret; it's cheaper to have it from the first feature.
How Next-Elite handles it: Vitest and React Testing Library cover unit and component tests (config/vitest.config.ts), split into a fast node project for .test.ts logic and a jsdom project for .test.tsx components. Tests live in src/tests/, mirroring the src/ structure, with a renderWithProviders helper (@/tests/render) for components that need the app's context, i18n, theme, auth, React Query, so testing a component that reads the current locale doesn't mean hand-wiring four providers per test file. Playwright covers end-to-end, including the home page, the health endpoint, and the security headers, running on port 6767 against next dev locally (all browsers) and against a production next start build in CI (Chromium only, for speed). npm run check is the single CI gate: typecheck, lint, dead-code detection, and tests, in that order, so a broken type or an unused export fails the build before a flaky E2E test would even get a chance to run.
15. Code quality is an engineering system
Recommended practice: linting and formatting stop being cosmetic once a team grows past one person; they're what keeps "the way we do things" from depending on everyone remembering the rules. The honest 2026 state of the tooling landscape is a genuine trade-off, not a clean win for either side. Rust-based linters like Oxlint are dramatically faster than ESLint, independent 2026 benchmarks put the gap at roughly 50 to 100 times on large codebases, but ESLint still has by far the larger plugin ecosystem, meaning teams with heavy plugin dependencies (custom accessibility rules, framework-specific configs) often run both rather than fully switching. For a new project without that legacy plugin baggage, going Oxlint-first is a reasonable, increasingly common choice; for an existing large codebase with deep ESLint plugin investment, a hybrid or gradual migration is the more honest recommendation.
How Next-Elite handles it: Oxlint and Oxfmt handle linting and formatting, chosen specifically because the project has no legacy ESLint plugin dependency to migrate away from, it's a new codebase, so there was nothing to preserve by staying on the slower tool. Knip catches dead code and unused dependencies. Dependencies are pinned to exact versions (.npmrc sets save-exact=true) and kept current by Renovate and Dependabot, so an update is always a reviewed pull request, never a surprise in a fresh install. Lefthook wires all of it into git hooks: a pre-commit hook runs Oxlint and Oxfmt, a commit-msg hook enforces Conventional Commits via Commitlint, and a pre-push hook runs the full npm run check gate before code ever reaches a PR, which catches problems at the cheapest possible point, before CI, not after.
In 2026 there's a new member of the team the rules have to reach: AI coding agents. A linter can't tell an agent "use the existing card component instead of writing a new one." So Next-Elite ships its conventions in a form agents read: an AGENTS.md for tools like Cursor and Codex, and a project-standards skill for Claude Code. They tell the agent to reuse existing components first, add a variant instead of duplicating a component, use design tokens only, keep public pages as Server Components, and ask before making structural changes. That's the same "the way we do things" problem, extended to contributors that don't attend code review.
16. Observability from day one
Recommended practice: "it works on my machine" is not a monitoring strategy, and neither is waiting for a user to report a bug in a support ticket. Error tracking and a basic health endpoint cost almost nothing to set up and are the difference between finding a production issue in minutes versus discovering it days later from a support ticket. The 2026 addition: observability shouldn't cost every visitor bundle size. Load your error tracking SDK lazily so it never competes with the first paint.
How Next-Elite handles it: Sentry is wired for both client and server instrumentation (instrumentation.ts, instrumentation-client.ts), and both error boundaries report to it. The browser SDK loads lazily, and only when a DSN is configured, so projects that don't use Sentry ship none of it. Vercel Analytics is included for basic page analytics and renders only when the app is actually deployed on Vercel, so self-hosted deploys don't make a failing request on every page. A GET /api/health endpoint exists specifically so a load balancer, Dokploy, or your own uptime monitor has something concrete to poll. None of this replaces a real incident-response process for a team that needs one; it's the minimum that should exist before you need it, not a substitute for a mature observability stack at scale.
17. Docker and deployment should be boring
Recommended practice: deployment infrastructure shouldn't force an architectural rewrite six months in. Standalone output, multi-arch container builds, a real health check, and security headers set in code (not in some dashboard nobody remembers configuring) are what let you move a project from Vercel to a VPS, or from a single instance to several, without touching application code.
How Next-Elite handles it: the Dockerfile is a multi-stage build on a Google Distroless base image (gcr.io/distroless/nodejs22-debian12), so the runtime image contains no shell or package manager. It uses Next.js's standalone output with a built-in HEALTHCHECK against /api/health, and Buildx support means one command produces images for both linux/amd64 and linux/arm64, useful for self-hosting on ARM (Oracle Cloud's free tier, AWS Graviton, a Raspberry Pi). One detail that trips people up: NEXT_PUBLIC_* values are inlined into the JavaScript at build time, so they're passed as Docker build args, while server secrets like BETTER_AUTH_SECRET are read at runtime.
Security headers live in next.config.mjs: a strict Content Security Policy, HSTS, X-Frame-Options: DENY, nosniff, Referrer-Policy, and Permissions-Policy, with E2E tests checking that they're actually sent. One practical lesson from running the live demo behind Cloudflare: a strict CSP also blocks the scripts your host injects (Cloudflare Web Analytics, in my case), so allow-list them deliberately instead of loosening the whole policy.
It deploys to Vercel with one click, or to Dokploy as a self-hosted alternative, using the same Dockerfile and health check either way. The point isn't a preference for one platform, it's that the same container-based deployment story works whether you're on a $0 Vercel hobby plan or your own hardware.
18. What I intentionally did NOT include
A good boilerplate is defined as much by what it refuses to bundle as by what it ships. Next-Elite intentionally does not include:
- A database or ORM. No Prisma, no Drizzle, no Postgres. This is the direct consequence of the API-driven architecture decision in section 3, not an oversight.
- Payments or billing. Stripe, subscriptions, and usage metering are product-specific decisions that vary too much between projects to standardize in a starter.
- Queues or Redis. Background jobs and caching infrastructure depend entirely on what your backend already provides.
- A specific backend framework. Next-Elite doesn't assume Go, Node, or Laravel behind it, because assuming any one of them would break the "drop it on any backend" promise.
- Multi-tenant or hierarchical permission graphs. The RBAC system covers two roles well; it doesn't try to be a general-purpose authorization engine.
- A wired-up password reset email flow. The reset page and form are there, but sending the email is your backend's job, so the form is ready to connect rather than tied to one email provider.
Every one of these is a real feature other boilerplates in the ecosystem provide well, some of them, like ixartz's Next.js Boilerplate, ship a database, multi-tenancy, and a full auth/roles system out of the box, and paid options like ShipFast or MakerKit bundle Stripe, transactional email, and enterprise compliance features for a one-time fee. Building that same foundation from scratch has been estimated at somewhere in the $7,500 to $12,000 range in developer time, which is exactly why those paid options exist and are worth it for the right project. If you want any of what's on this list bundled in, one of those is very likely the better starting point than Next-Elite. That's not a hedge, it's the honest answer to "why doesn't this have X."
19. The trade-offs
None of this architecture is universally correct, and presenting it that way would be dishonest.
Next-Elite is probably overkill for:
- Single-page landing sites
- Tiny prototypes and weekend experiments
- Projects where Next.js is genuinely the entire backend and will stay that way
It's a stronger fit for:
- SaaS applications with more than one user role
- Admin panels, ERPs, dashboards, and internal tools
- Multilingual products, especially ones that need RTL support
- Products backed by an existing REST, GraphQL, or BFF API
- Teams (or solo developers) who've noticed they're rebuilding the same auth-and-RBAC foundation across projects
If you're not sure which camp you're in: if you can describe your project without the words "roles," "dashboard," or "multiple languages," you probably don't need this, and that's a legitimate outcome, not a failure to be convinced.
20. The actual boilerplate: Next Elite
I built Next Elite because I kept answering the same architectural questions from scratch on every new SaaS project, and I wanted the answer to compound instead of resetting. It's MIT-licensed and free to use as a template.
What you get, in one pass:
- Next.js 16.4 (App Router, Turbopack, Server Actions) + React 19.3 + TypeScript 7
- Feature-based architecture under
src/features/ - BetterAuth (email/password + Google OAuth) with permission-based RBAC, split-pane auth UI, server-side guards, parallel-route dashboard slots
- next-intl across 6 languages including RTL, cookie-based, type-checked translation keys
- 50+ shadcn/ui primitives and custom components on Tailwind CSS v4 and Radix UI, with light/dark design tokens
- TanStack Query pre-configured for your REST/GraphQL/BFF API
- Sentry (lazy-loaded) + Vercel Analytics + a
/api/healthendpoint - Security headers (CSP, HSTS, frame and content-type protection) set in code
- Vitest + React Testing Library + Playwright
- Oxlint + Oxfmt + Knip + Lefthook + Commitlint, with pinned dependencies kept current by Renovate and Dependabot
-
AGENTS.mdand aproject-standardsskill so AI coding agents follow the project's conventions - Standalone Distroless Docker build, multi-arch (amd64/arm64), Dokploy-ready, one-click Vercel deploy
- SEO metadata, JSON-LD, dynamic sitemap,
robots.txt, PWA manifest, andllms.txtfrom one config file - All demo code in one folder, removable with
npm run demo:remove
Who should use it: teams and solo developers building a role-based SaaS, admin panel, or dashboard product against an existing or planned API, especially if multi-language support is on the roadmap.
Who shouldn't: anyone who needs a bundled database on day one, or whose project is small enough that this is genuinely more structure than the problem needs. Use create-next-app for that, or the official Next.js SaaS Starter if you want the framework team's own opinionated take.
Quick start
git clone https://github.com/salmanshahriar/Next-Elite.git
cd Next-Elite
npm install
cp .env.example .env
npm run dev
Open http://localhost:6767. You'll land on a working app with auth, RBAC-gated dashboards, six languages, and the rest of this list already wired together, not a scaffold you still have to assemble. When you're ready to build your own product, run npm run demo:remove and npm run check.
Live Demo · GitHub Repo · Use This Template · Deploy on Vercel
21. Final checklist: my 2026 Next.js project checklist
Whether or not you use Next-Elite, this is the list I'd check before calling a Next.js project production-ready:
Architecture
- ✅ Code is grouped by feature, with genuinely shared UI kept in one place
- ✅ Decided on purpose whether Next.js owns the database or consumes an API
- ✅ Demo or example code is isolated and removable
Rendering & data
- ✅ Pages and layouts are Server Components;
'use client'only at small leaf boundaries - ✅ Server-only values are passed to client components as props, not imported
- ✅ Server-known data renders on the server; client data uses TanStack Query, not
useEffect+useState
Security
- ✅ Authorization runs in Server Components, Route Handlers, and Server Actions, never only in middleware
- ✅ Roles and permissions are defined in one typed place and checked through a few helpers
- ✅ Auth has a real database adapter before multi-instance production
- ✅ Security headers (CSP, HSTS, frame protection) are set in code and tested
Type safety & config
- ✅ Environment variables are validated, with server and public variables clearly separated
- ✅ Translation keys, permissions, and config are typed, not just component props
i18n & UI
- ✅ Every user-facing string goes through i18n, and every locale has the same keys
- ✅ Layouts use logical start/end spacing so RTL works
- ✅ Colors and radii come from design tokens, with light and dark values
- ✅ Every data view has loading, empty, and error states
SEO & performance
- ✅ Metadata, sitemap, robots, manifest, and JSON-LD come from one config
- ✅ Bundle analyzed with the Turbopack analyzer; heavy libraries load lazily
- ✅ Lighthouse checked on the production build
Quality & delivery
- ✅ One
checkcommand runs typecheck, lint, dead-code detection, and tests - ✅ Git hooks run the cheap checks before code leaves the machine
- ✅ Dependencies are pinned and updated through reviewed pull requests
- ✅ Unit/component tests plus E2E for the flows that matter
- ✅ AI coding agents get the project's conventions in
AGENTS.md(or a skill) - ✅ Error tracking, a health endpoint, and a container build with a health check exist before launch




Top comments (0)