A self-hosted developer portfolio CMS built with Next.js 16, featuring a public-facing site (Home, About, Projects, Blog, Contact) and a password-protected admin dashboard for full content management. The project eliminates reliance on third-party CMS platforms while giving the developer complete ownership of their content, design, and deployment pipeline.

Developer Portfolio CMS is a full-stack web application that serves as both a public-facing portfolio site and a self-hosted content management system. Built on Next.js 16's App Router, it gives developers complete control over their online presence — from showcasing projects and writing blog posts to managing their professional experience and skills — all from a custom admin dashboard.
Most developer portfolios rely on either static site generators with no dynamic content, or third-party CMS platforms that introduce vendor lock-in, recurring costs, and limited customisation. The goal was to build a portfolio that felt truly owned: one where content could be updated without touching code, images had a proper storage pipeline, and the public site remained fast and SEO-friendly.
The application is split into two route groups within Next.js App Router:
app/(public)/) — statically rendered pages for Home, About, Projects, Blog, and Contact, with slug-based routing for individual posts and projects.app/(admin)/admin/) — a fully protected CRUD interface for all content types, accessible only after authentication.Content is stored in PostgreSQL (via Supabase) with five Prisma models: Admin, Project, BlogPost, Skill, and Experience. Server Actions handle all mutations with Zod validation, keeping the API surface minimal and type-safe.
Authentication uses NextAuth 5 beta with a Credentials provider (email/password, bcryptjs hashing) and stateless JWT sessions. The auth config is split into an edge-compatible layer for middleware and a full layer for the Credentials provider, allowing route protection at the edge without hitting the database on every request.
File uploads (images and resumes) go through dedicated API routes that validate MIME type and size before uploading to Supabase Storage with UUID filenames. The Supabase service role key is used exclusively server-side and never exposed to the client.
| Technology | Role |
|---|---|
| Next.js 16 (App Router) | Framework — SSR, static generation, server actions, API routes |
| TypeScript | Type safety across the entire codebase |
| Tailwind CSS v4 | Utility-first styling with dark/light theme support |
| PostgreSQL + Prisma | Data persistence via Supabase with @prisma/adapter-pg for PgBouncer compatibility |
| NextAuth 5 beta | Edge-compatible authentication with JWT sessions |
| Supabase Storage | CDN-backed image and resume storage |
| Tiptap | Rich text editor for blog post and project content |
| Framer Motion | Page transitions and micro-interactions |
| Zod | Runtime validation for all server action inputs |
| Vercel | Deployment (Mumbai region, bom1) |
Edge-compatible auth with Prisma
NextAuth 5's middleware runs on the Edge Runtime, which doesn't support Node.js-only modules. Prisma cannot be imported in the Edge context. This was solved by splitting the auth config into two files: lib/auth.config.ts (edge-safe, no Prisma) used by the middleware, and lib/auth.ts (full config with Credentials + Prisma) used for session handling. Both share the same base config, avoiding duplication.
PgBouncer and Prisma migrations
Supabase uses PgBouncer for connection pooling, which is incompatible with Prisma's migration engine (migrations require a direct connection). The solution was a dual-URL strategy: DATABASE_URL points to the pooled endpoint for runtime queries, while DIRECT_URL bypasses PgBouncer and is used exclusively for migrations via prisma.config.ts.
CI without real secrets
The GitHub Actions workflow needs to run tsc and next build without real database credentials. Dummy fallback values are injected as environment variables in CI, allowing type checking and build validation to pass without a live database connection.
Image optimization with external CDN
Next.js image optimization works well for local assets, but adds a redundant processing layer when images are already served from Supabase's global CDN. Image optimization was disabled (unoptimized: true in next.config.ts) to avoid double-CDN latency and the need to whitelist Supabase domains via remotePatterns.
main is type-checked, linted, and built before deployment.