After months of work, I launched Nexus — a Next.js 16 starter kit with a full admin dashboard. Today I'll walk through how the 17-page admin panel works, the architecture decisions, and two sneaky bugs I had to fix.
The goal: edit everything without code
The hardest part of building a landing page template isn't the landing page itself — it's making every word, image, and price editable WITHOUT touching code.
When I started, the question was: "How do I let the buyer change anything, anytime, without opening an IDE or triggering a deploy?"
The answer: a full 17-page admin dashboard with live CRUD.
The 17 pages
- Dashboard — KPIs, charts, reviews feed
- Screenshots — CRUD with image upload
- Features — manage feature cards
- How It Works — 3-step section editor
- Reviews — ratings + platform + replies
- Pricing — 3 tiers (monthly/yearly toggle)
- Press & Awards — media logos
- Comparison — feature comparison table
- FAQ — searchable accordion
- Footer — columns + links + social
- Theme — colors + 5 fonts + radius (LIVE!)
- Blog — markdown editor + draft/publish
- Messages — contact form inbox
- Audit Log — every admin action tracked
- Settings — app identity + store links
- Account — change password
- Login — secure sign-in
Architecture: how edits flow to the live site
Here's the flow when an admin clicks "Save":
- Admin form calls save(nextContent) from useStore()
- useStore does an optimistic update — updates the local cache immediately and publishes a CONTENT_EVENT
- All components using useSyncExternalStore re-render with the new content
- In parallel, a PUT /api/admin/content saves to the database
- If the save fails, the optimistic update is reverted and a toast shows the error
This means the admin sees instant feedback, and the live site updates without a reload.
Bug 1: Tailwind CSS 4 font inlining
The most devious bug I hit: changing the font in the admin didn't reflect on the live site.
The cause: In globals.css, --font-sans was declared inside @theme inline:
@theme inline {
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
}
When Tailwind 4 sees a var() value in @theme, it inlines the value into the generated utility class instead of referencing the variable by name. So the compiled CSS was:
.font-sans { font-family: var(--font-inter), ui-sans-serif, system-ui, sans-serif; }
Notice: .font-sans references var(--font-inter) directly — hardcoded to Inter. Updating --font-sans at runtime had no effect because the utility class didn't look at it.
The fix: Move --font-sans outside @theme inline into a plain :root block, then add the utilities manually so they reference the variable by name (dynamic):
:root {
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
}
@layer base {
.font-sans { font-family: var(--font-sans); }
.font-mono { font-family: var(--font-mono); }
}
Now .font-sans references var(--font-sans) by name — runtime overrides work.
Bug 2: --font-sans scope vs next/font
Even with the fix above, the font still didn't switch. The second cause was a scope mismatch.
In theme.ts, generateThemeCss() was emitting:
:root{--font-sans:var(--font-poppins), ...;}
But next/font/google declares the per-family variables on body:
body. { --font-poppins: '_Poppins', ...; }
So var(--font-poppins) was undefined in :root's scope — the lookup failed silently and the font fell back to the system stack.
The fix: Target :root, body in generateThemeCss:
:root{...colors/radius...;}:root,body{--font-sans:${fs};}.dark{...};
Now the override reaches body, where --font-poppins is defined.
Security: not just a CRUD app
I didn't want a basic admin panel with zero security. Here's what's built in:
- JWT auth with httpOnly cookies (24h expiry)
- bcrypt (cost factor 12) for password hashing
- Session versioning — changing the password bumps a DB counter, invalidating all existing JWTs
- CSRF protection — same-origin check on every state-changing route
- Rate limiting — 5 login attempts / 15 min per IP (sliding window)
- Magic-bytes image upload — validates file signature, not just MIME type
- Audit log — every create/update/delete is recorded with the actor + JSON snapshot
- Honeypot — hidden fields on contact + newsletter forms catch bots silently
Try it yourself
I set up a live demo where every visitor gets their own browser sandbox (per-visitor localStorage). Try the admin panel — no signup required:
Live demo: https://nexus-by-hadi-kits.vercel.app/
Click "Continue as demo admin" and edit anything. Your changes are saved to your browser only.
Full template
The full source code (TypeScript), Prisma schema, Dockerfile, GitHub Actions CI, and 65KB of documentation is available for $89 (launch offer, regularly $149).
Buy: https://www.getly.store/product/nexus-next-js-16-mobile-app-landing-starter-kit-with-admin-panel
I write daily about this build on Indie Hackers. Follow along: https://www.indiehackers.com/post/i-just-launched-my-first-next-js-16-starter-kit-after-months-of-work-nexus-eea2fd8f41
—
Note: All demo data (downloads, active users, ratings, top countries) is AI-generated for demonstration purposes only.
Top comments (0)