Back to projects
SOLO FULLSTACK

Portfolio Site

This site itself: a Next.js portfolio with a Supabase-backed admin CMS, bilingual content, and a static seed fallback so it still renders with no database at all.

  • Next.js
  • React
  • TypeScript
  • Tailwind CSS
  • Supabase
  • Framer Motion
Portfolio Site — screenshot

OVERVIEW

Every section — profile, projects, experience, research, stats, skills — is a Supabase row edited from /admin, with 60-second ISR so an edit goes live within a minute. Supabase is never a single point of failure: getContent() tries the database first and falls back whole to a static seed file if the env vars are missing or the query fails, so the repo runs with npm run dev and no .env.local at all. One schema declaration drives the admin dashboard — the same field list renders the form, whitelists which columns a Server Action may write, and coerces types, so adding a field is one line rather than a form plus a validator plus an action. Auth is enforced in three layers: middleware refreshes the Supabase session, the admin layout re-checks table membership, and Postgres RLS is the actual source of truth — only the anon key is used, no service-role key anywhere in the app. EN/ID needs no duplicated routes: every text field is stored as an _en/_id pair and swapped client-side, and a small head script sets the theme before hydration so no page ever paints in the wrong one.

WHAT I DID

  • Content-as-data: every section is a Supabase row edited from /admin with 60-second ISR, and the whole site still renders from a static seed file if Supabase is unreachable or unconfigured.
  • One field schema drives the admin form renderer, the column whitelist and the type coercion for every table — a new field is one line, not a form plus a validator plus an action.
  • Auth enforced in three layers: middleware refreshes the session, the admin layout re-checks membership, and Postgres RLS is the actual source of truth — only the anon key is used, no service-role key in the app.
  • EN/ID with no duplicated routes: every text field is stored as an _en/_id pair and swapped client-side; a small head script sets the theme before hydration so there is no light/dark flash.