DEV Community

Cover image for Next.js Best Practices in 2026: The Boilerplate I Built So I Stop Repeating Myself
Salman Shahriar
Salman Shahriar

Posted on Edited on

Next.js Best Practices in 2026: The Boilerplate I Built So I Stop Repeating Myself

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

  1. What "production-ready" actually means in 2026
  2. Start with architecture, not components
  3. Frontend-first and API-driven architecture
  4. Server Components vs Client Components
  5. Authentication is not authorization
  6. RBAC without scattering permission logic everywhere
  7. TypeScript should enforce architecture, not just describe it
  8. Type-safe internationalization
  9. UI architecture with Tailwind + shadcn/ui
  10. Data fetching: don't turn everything into React state
  11. Environment variables should fail safely
  12. SEO should be part of the architecture
  13. Performance should be a default, not a final optimization
  14. Testing strategy
  15. Code quality is an engineering system
  16. Observability from day one
  17. Docker and deployment should be boring
  18. What I intentionally did NOT include
  19. The trade-offs

20. The actual boilerplate: Next Elite

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, and enforced so they don't quietly regress.

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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 where rendering on the server actually buys you something (SEO, a fast first paint of server-known data), and add 'use client' at the smallest boundary that needs interactivity, browser APIs, or local state. 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.

The nuance worth being honest about: "server-first" doesn't mean "server-everything." A page behind a login doesn't need SEO, and if all of its data comes from a client-side API cache anyway, rendering it on the server mostly adds complexity.

How Next-Elite handles it: it splits the decision by route group instead of by habit. Public pages are Server Components for SEO. Auth pages are a server shell with client forms. The protected app checks the session once on the server, in its layout, and then renders its pages on the client, where they fetch from your API:

flowchart LR
    subgraph Public["(public) · SEO"]
        direction TB
        P["Server Components<br/>metadata · sitemap · OG"]
    end

    subgraph Auth["(auth) · sign in"]
        direction TB
        F["Client forms<br/>Better Auth loaded on submit"]
    end

    subgraph App["(protected) · app"]
        direction TB
        G["Layout: requireUser()<br/>one server check"] --> R{role}
        R -->|admin| A["@admin pages"]
        R -->|user| U["@user pages"]
    end

    API[("Your API<br/>REST · GraphQL · BFF")]

    Public --> Auth --> App
    A & U -->|TanStack Query| API

The practical rule the structure encodes: if the page needs SEO or server-known data for first paint, it's a Server Component. If it lives behind the login and talks to your API, it can be a client component, as long as the session check happened on the server first. 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 where they can't be skipped by a crafted request: your Server Components, Route Handlers, Server Actions, and, for an API-driven frontend, your backend API.

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. The project has no middleware at all, so there's no edge layer to bypass. The protected layout runs requireUser() on the server: no session means a redirect to /login before any protected content renders, and the user's role decides which pages are rendered at all.

// src/app/(protected)/layout.tsx (simplified)
const ProtectedLayout = async ({ admin, user, children }) => {
  const currentUser = await requireUser(); // redirects to /login without a session
  const isAdmin = hasPermission(currentUser.permissions, 'dashboard.view:admin');

  return (
    <AuthProvider initialUser={currentUser}>
      <QueryProvider>{isAdmin ? admin : user}</QueryProvider>
    </AuthProvider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Because Next-Elite has no database, the data itself lives behind your API, and that's where authorization of data access belongs: your API should check the session on every request, not trust the frontend. The frontend's job is to never render what a user shouldn't see; the API's job is to never return it.

Two smaller details worth copying: the BetterAuth client is only downloaded when someone actually signs in, signs up, or signs out, and the current user comes from the server instead of client-side session polling. 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, plus Next.js parallel routes so role-specific pages don't need runtime branching in the render tree at all. Both roles share the same URLs, and the layout mounts the right slot on the server:

src/app/(protected)/
  ├── @admin/               # Admin slots (dashboard, analytics, profile, settings)
  ├── @user/                # User slots (dashboard, analytics, profile, settings)
  └── layout.tsx            # requireUser() once, then picks the slot by role
Enter fullscreen mode Exit fullscreen mode

Because the slot is chosen on the server, a regular user is never sent the admin pages in the first place; there's nothing to unhide by editing the DOM. Inside a page, useAuth().hasPermission() handles finer UI decisions, like hiding a button a role can't use, which is good UX but not the security layer. 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.


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 five 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 an NEXT_PUBLIC_APP_URL that isn't a URL or a BETTER_AUTH_SECRET shorter 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 in messages/en.json, so a typo'd key fails the build instead of silently rendering nothing.
  • RBAC permissions are a typed union (UserRole and AuthPermission in permissions.ts), so hasPermission('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.json against it, so the config is typed in the editor and checked in CI without shipping the validator to the browser.
  • Framework config lives in a typed next.config.ts, so a misspelled option fails the typecheck instead of being silently ignored.

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, a themed area chart (built on uPlot), 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:

Next Elite 50+ custom and reusable UI components

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, like the square variant the theme toggle uses) 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.

Next Elite role-based admin dashboard layout


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 public page'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: a QueryProvider is pre-configured in the protected layout (src/components/shared/query-provider.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 lives only where client data fetching actually happens: public and auth pages don't load TanStack Query at all, and server-renderable public 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. Every variable is documented in .env.example, with safe defaults (Google sign-in is off until you add its keys). 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."
}
Enter fullscreen mode Exit fullscreen mode

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 in the llmstxt.org format, a structured summary for AI crawlers and assistants with the project's versions, architecture, routes, and links to key files, 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 where it helps, 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.

Two tooling details that catch people out. First, @next/bundle-analyzer only works with webpack, so under Turbopack it silently does nothing; use Next's built-in analyzer instead (next analyze). Second, measure the way your users actually download: CDNs and Vercel serve Brotli, which is about 14% smaller than gzip, but next start on its own only serves gzip, so self-hosted apps need a Brotli-capable proxy in front to get it.

How Next-Elite handles it: I ran that analysis on Next-Elite twice. The first pass 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 chart 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_DSN is 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.

That cut first-load JavaScript by 21–24% per page, and the live demo's total transfer roughly halved, from 906 KB to 462 KB.

The second pass went after shared code that every page paid for:

  • The BetterAuth client now loads only when someone signs in, signs up, or signs out; the current user comes from the server.
  • TanStack Query moved out of the root layout into the protected area, so public pages don't load it.
  • The toast container, the language menu, and demo-only widgets load after the page renders.
  • The chart component moved from ApexCharts to uPlot, with the same API and look: chart pages now load about 21 KB of chart code instead of about 292 KB (gzip).
  • The three.js login animation was removed (the animated globe stays), and Sentry's unused Session Replay code is stripped out.
First-load JS (gzip) Before After
/ (with demo) 340 KB 263 KB
/login 326 KB 263 KB
/dashboard 286 KB 265 KB

Without the demo code, the home page is 212 KB gzip, or 184 KB with Brotli, about 112 KB of which is Next.js and React itself. To keep it there, npm run size measures every page's first-load JavaScript with Brotli on every CI run and fails the build if a page grows past its budget, so a heavy import gets caught in review instead of in production.

The project's Lighthouse report shows 100 across Performance, Accessibility, Best Practices, and SEO, and 3/3 on Agentic Browsing:

Next Elite Lighthouse Report - 100 Performance, Accessibility, Best Practices, and SEO

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 and the size budget belong 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.

The less-discussed half of the practice: tests have to test your code. A test that copies a function's logic into the test file keeps passing even when the real function breaks, and a test that only checks a component's class names breaks on every harmless refactor. Both cost maintenance and protect nothing.

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, and focus on behavior that matters: role and permission logic, form validation rules, locale fallback, translation key parity across all six languages, and the site config schema. I recently cut the suite roughly in half by deleting tests that copied logic or only checked markup, and coverage of real behavior didn't drop. A renderWithProviders helper (@/tests/render) wraps 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 login 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 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. The npm scripts are kept to the ones people actually use; anything that's just another flag on an existing command (UI mode for Playwright, a single browser) is documented as a flag instead of becoming a sixth script to remember.

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 at the root and a project-standards skill in .agents/skills/, read by Claude Code, Cursor, Codex, and others. 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, stay within the bundle budget, 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, and Sentry's bundle-size options strip out debug logging and the unused Session Replay code for projects that do. 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 (the documented docker build command and Docker Compose both read them from .env), while server secrets like BETTER_AUTH_SECRET are read at runtime.

Security headers live in next.config.ts: 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. When self-hosting, put a Brotli-capable proxy in front (Traefik, which Dokploy uses, or Nginx with ngx_brotli), since next start only serves gzip. 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.

Next Elite split-pane auth layout

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, one server-side session check, 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 and a lightweight uPlot chart
  • TanStack Query pre-configured for your REST/GraphQL/BFF API
  • Lean bundles: ~184 KB Brotli first load, with per-page JS budgets enforced in CI
  • Sentry (lazy-loaded) + Vercel Analytics + a /api/health endpoint
  • Security headers (CSP, HSTS, frame and content-type protection) set in a typed next.config.ts
  • Vitest + React Testing Library + Playwright
  • Oxlint + Oxfmt + Knip + Lefthook + Commitlint, with pinned dependencies kept current by Dependabot
  • AGENTS.md and a project-standards skill 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, and llms.txt from 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
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:6767. You'll land on a working app with auth, role-based 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

Top comments (0)