I recently built and launched a premium website template using Next.js 16 App Router. In this post, I'll walk through the architecture, technical decisions, and challenges I faced.
What I Built
A full-stack luxury business website template with:
- Complete admin panel (CMS for all content)
- 3 languages (English, French, Arabic with automatic RTL)
- Two-factor authentication (TOTP)
- 5 switchable color schemes
- Docker one-command deployment
Live demo: luxbrand-neon.vercel.app/en
Admin demo (read-only): luxbrand-neon.vercel.app/admin
Tech Stack
| Technology | Purpose |
|---|---|
| Next.js 16 | Framework (App Router) |
| PostgreSQL | Database |
| Prisma | ORM |
| NextAuth v5 | Authentication |
| Tailwind CSS 4 | Styling |
| next-intl | Internationalization |
| TipTap | Rich text editor |
| Framer Motion | Animations |
| Docker | Deployment |
Architecture Decisions
1. Environment-Driven Demo Mode
One of the most useful patterns I implemented was a single NEXT_PUBLIC_DEMO_MODE environment variable that switches the entire app between demo and production behavior.
typescript
// middleware.ts
const isDemo = process.env.NEXT_PUBLIC_DEMO_MODE === "true";
if (isDemo) {
// Block all write operations, open admin without login
} else {
// Require JWT authentication, allow all operations
}
This means the same codebase serves both:
Vercel showcase (demo mode) — open admin, read-only
Buyer's deployment (production) — real auth, full editing
No separate branches, no code duplication.
2. Custom TOTP Implementation
I initially tried otplib for 2FA, but it had ESM/CJS compatibility issues with Next.js 16's Turbopack bundler. Instead, I built a lightweight TOTP implementation using @noble/hashes:
import { hmac } from "@noble/hashes/hmac";
import { sha1 } from "@noble/hashes/sha1";
function generateCode(secret: string, timeStep: number): string {
const key = base32Decode(secret);
const time = Buffer.alloc(8);
time.writeBigUInt64BE(BigInt(timeStep));
const hash = hmac(sha1, key, new Uint8Array(time));
const offset = hash[hash.length - 1] & 0x0f;
const code =
((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
return (code % 10 ** 6).toString().padStart(6, "0");
}
Works perfectly with Google Authenticator and Authy. No dependency issues.
3. CSS Variable Color Schemes
Instead of hardcoding colors, the entire site's accent color is driven by three CSS custom properties:
:root {
--accent-color: #789904; /* Green (default) */
--accent-light: #8fb012;
--accent-dark: #5d7603;
}
Change these three lines and every button, icon, badge, and link across the site updates. I ship 5 presets (Green, Gold, Navy, Burgundy, Emerald).
The Tailwind theme maps to these variables:
@theme {
--color-brand-green: var(--accent-color);
--color-brand-green-light: var(--accent-light);
--color-brand-green-dark: var(--accent-dark);
}
4. CMS with Translation Fallbacks
All page content is stored in the database and editable from the admin panel. But translations in JSON files serve as fallbacks:
const { content } = await getPageContent("services", locale);
const cv = (section, key, fallback) => content[section]?.[key] || fallback;
// Database content takes priority, translation is the fallback
<h1>{cv("hero", "title", t("hero.title"))}</h1>
This means the site works out of the box with seed data, but every text is customizable from the admin without touching code.
5. Docker Auto-Setup
The Docker entrypoint automatically:
Runs Prisma migrations
Checks if any admin user exists
Seeds the database if it's empty
Starts the server
#!/bin/sh
npx prisma db push --skip-generate
node -e "/* check if users exist */"
if [ $? -ne 0 ]; then
node prisma/seed.js
fi
exec su-exec nextjs node server.js
The buyer literally does: docker compose up -d and everything works.
Challenges
NextAuth v5 in Edge Middleware
This was the biggest pain point. getToken() doesn't auto-detect AUTH_SECRET when running in Edge runtime (middleware). You have to pass it explicitly:
// This fails in production Edge runtime:
const token = await getToken({ req: request });
// This works:
const token = await getToken({ req: request, secret: process.env.AUTH_SECRET });
Took a while to debug because it works fine in dev mode.
Turbopack JSON Parse Errors
Next.js 16 forces Turbopack for dev. I kept hitting SyntaxError: Unexpected non-whitespace character after JSON at position 521 — a corrupted Turbopack cache issue. Fix: delete .next and restart. The production build (npm run build) always worked fine.
RTL Layout
Arabic RTL support was surprisingly smooth with next-intl. The key is setting dir="rtl" on the <html> tag based on locale. Tailwind's rtl: variants handle the rest. The only tricky part was ensuring CSS grid and flexbox layouts didn't break.
What I'd Do Differently
Use Auth.js stable instead of NextAuth v5 beta — the beta has too many undocumented Edge runtime quirks
Build the TOTP utility from day one — instead of trying otplib first and wasting time on compatibility issues
Add dark mode early — it's harder to retrofit than to build in from the start
Links
Live demo: luxbrand-neon.vercel.app/en
Admin demo: luxbrand-neon.vercel.app/admin
Available on Gumroad: 286899761190.gumroad.com/l/luxbrand-template
Thanks for reading! Happy to answer any questions about the implementation.
Top comments (0)