All projects

Portfolio CMS

Personal project

The site you are reading, plus the admin system behind it. Next.js and Supabase, feature-based architecture, every input validated against a Zod schema on the server, and Postgres row-level security so the public site can read without the admin surface being exposed.

Role
Sole Developer
Timeline
Jan 2026 — Feb 2026
Status
Completed
Portfolio CMS cover

Overview

I built this site as a content management system rather than a set of hard-coded pages, because a portfolio that requires a code change and a deploy to fix a typo is a portfolio that stays out of date.

The structure is feature-based. Every domain, projects, experiences, skills, services, and the rest, owns its own schemas, server actions, queries, types and components. The reason is boring and practical: when I come back to change how certifications work, everything about certifications is in one directory, and I am not tracing a change through layers organised by technical role instead of by subject.

Validation happens in one place per feature and runs on the server. Each feature has a Zod schema that defines its real constraints, field lengths, allowed enum values, URL and date formats, cross-field rules like an end date not preceding a start date, and the server action validates against it before anything reaches the database. The client form and the server share that schema, so the rules cannot drift apart into a UI that permits what the server will reject.

Authorisation is enforced in Postgres through row-level security rather than in application code. The public site reads published content through policies that allow exactly that and nothing more, while write access requires an authenticated admin. This means the security boundary does not depend on my remembering to check a condition in every query, which is the kind of thing that holds for the first twenty queries and fails on the twenty-first.

File handling goes through Supabase Storage with a separate bucket per content type and policies scoped to each, so an upload path for certificates cannot be used to write into resumes. Schema changes are versioned as migrations rather than applied by hand, so the database has a history I can read and reproduce.

It is a small system, but it is built the way I would build a system that had to last, because that is the point of it.

Problem

  • A hard-coded portfolio needs a code change and a deploy to correct a typo, so it drifts out of date and stops being maintained.
  • Validation duplicated between client and server drifts apart, producing forms that accept input the server rejects.
  • Authorisation enforced in application code depends on remembering to check a condition in every query, which fails on the query you forget.
  • A single storage bucket for all uploads means any upload path can write to any content type.

Approach

  • Built the content as a CMS backed by Supabase, so every section is editable through an admin interface without a deploy.
  • Organised the codebase by feature, with each domain owning its schemas, server actions, queries, types and components in one directory.
  • Defined one Zod schema per feature, shared between the client form and the server action, so validation rules cannot diverge.
  • Enforced access in Postgres with row-level security: public read for published content, authenticated write for admin.
  • Separated Supabase Storage into a bucket per content type with policies scoped to each.
  • Versioned all schema changes as migrations so the database history is readable and reproducible.

Challenges

  • Deciding where validation belongs. Sharing a single Zod schema between the form and the server action is what stops the client permitting input the server will reject.
  • Row-level security is harder to write than an application-level check and is the only version that cannot be forgotten. Moving the boundary into the database was a deliberate trade of effort for reliability.
  • Feature-based structure requires discipline when a concern spans domains, since the easy move is a shared utility directory that slowly becomes where everything lives.
  • Modelling flexible content, case study sections, deliverable groups, architecture breakdowns, as JSONB while keeping it validated rather than free-form.

Results

  • Every section of the site is editable through an authenticated admin interface with no code change or deploy.
  • Validation rules exist once per feature and are enforced server-side, so the client form and the database cannot disagree.
  • Access control lives in Postgres row-level security rather than in application code, and storage is scoped per content type.
  • The full source is available for review, which is the point: it is the one project here whose engineering a client can inspect directly.

Architecture

Frontend

  • Next.js App Router with server actions
  • React components per feature
  • Tailwind CSS
  • Framer Motion for interaction

Code Organisation

  • Feature-based directories
  • Schemas, actions, queries, types and components co-located
  • Shared Zod schema between client and server

Data

  • Supabase Postgres
  • Row-level security for public read and admin write
  • JSONB for flexible case study content
  • Versioned SQL migrations

Storage

  • Supabase Storage
  • Separate bucket per content type
  • Per-bucket access policies

Stack

  • Next.js
  • React
  • TypeScript
  • Tailwind CSS
  • Zustand
  • Supabase
  • PostgreSQL
  • Database Schema Design
  • Clean Architecture

Have something similar to build?