Most portfolio sites are a stack of static pages. This one looks like a stack of static pages - that's deliberate, because static is fast and search engines love it - but the project cards, the contact form, the visitor analytics, and the password-protected admin panel are all wired to a live Postgres database. There is no API server in the middle that I wrote and maintain. The browser talks to the database directly.
That sounds reckless until you see how it's locked down. The rest of this post is the guided tour: skim the diagrams, poke the interactive bits, and open the "for the curious" drawers if you want the gritty detail.
01 The one idea that explains everything
There is no custom backend server. My JavaScript calls a database API directly, and a security layer inside the database decides who can read or write.
Deep dive: The whole site in one sentence
There is no custom backend server. My JavaScript, running in your browser, calls a database's auto-generated API directly - and a security layer inside the database decides, row by row, what each visitor is allowed to read or write.
Everything else is a detail of that idea. The "API key" shipped in the page is public on purpose; it only says "this request is an anonymous visitor." It grants no powers by itself. The actual gatekeeper is Row Level Security (RLS) in Postgres, and we'll play with it in section 07.
02 The stack, in plain terms
No framework, no build step, no bundler. It's hand-written HTML, CSS, and JavaScript - the "vanilla" stack - with Supabase (managed Postgres) doing the heavy lifting.
Deep dive: The stack breakdown
| Layer | What it is | Its job here |
|---|---|---|
| Pages | Hand-written HTML | One file per page. Server-rendered-by-default = great SEO, with JSON-LD, a sitemap, and an llms.txt. |
| Styling | Plain CSS | A small design system built on CSS variables (colour, spacing). No Tailwind, no Sass. |
| Behaviour | Vanilla JavaScript | Custom cursor, carousels, overlays, the project reveal, two little games. No React or Vue. |
| Backend | Supabase (managed Postgres 17) | Database + login + an auto-generated REST API, all in one. This is the only moving part. |
| Hosting | Vercel | Serves the static files from a global CDN. A git push deploys. |
| Analytics | Home-grown | Cookieless, no personal data, honours Do-Not-Track. The data is mine, not a third party's. |
03 The architecture map
Three zones: Your browser runs the app, Vercel hosts files, and Supabase handles all data (projects, messages, analytics, skills, and certs).
Deep dive: Architecture diagram
Your browser runs the whole app. Vercel only hands over files. Supabase is the entire backend. The teal lines are the live data calls that travel straight from the browser to the database.
04 What happens on one page load
The browser fetches static files, scripts load in order, analytics logs silently, data is fetched, and the UI renders - with offline fallbacks if the database is down.
Deep dive: The load sequence
Follow a single visitor from typing the URL to seeing project cards. This is the order things actually run in:
- The browser asks Vercel for the page.
It gets back the HTML, CSS, and images straight from the nearest CDN edge - no server thinking required.
- The scripts load, in a fixed order.
The database client first, then the config, then analytics, then the UI, then the data layer. Each one needs the one before it.
- Analytics fires a quiet "pageview."
One write to the database. If it fails, nothing breaks - analytics is designed to fail silently.
- The data layer asks for the projects.
It requests the published project rows. The database returns only those - drafts never leave the building.
- The cards render, and the page is interactive.
If the database was unreachable, it quietly falls back to a bundled copy of the list so you still see cards.
05 The three data flows
Everything dynamic is a direct round-trip from the browser to the database, governed by strict read/write rules.
Deep dive: The data flows
Everything dynamic on the site is one of three round-trips to the database. Tap each one:
Reading the projects
The data layer asks for projects where published = true.
The database applies the read rule and returns only those rows.
Drafts stay invisible to the public - not hidden by CSS, actually filtered out before they leave the database.
Logging an analytics event
A pageview, a project click, time-on-page - each becomes one insert into the events log. The public can write events but can never read them back.
No read-back, and errors are swallowed - analytics can never slow down or break the page.
Sending a contact message
The form runs client-side checks first (a honeypot for bots, a disposable-email blocklist, a typo-fixer), then inserts one message. Only the logged-in admin can ever read the inbox.
If the insert ever fails, the form builds a pre-filled email instead - so it's never a dead end.
06 The database
Six tables. Three are simple ledgers (projects, messages, events), while the rest power the new dynamic Skills & Certifications modules.
Deep dive: The schema
projects 10 rows
- code · MSN-01
- title · summary
- role · method · outcome
- chips · metric pills
- github_url · optional
- published · the gate
messages 5 rows
- name · email · phone
- body
- status · received…
- flagged · priority star
- created_at
events ~2,000 rows
- type · pageview…
- path · source
- session_id · anon id
- meta · flexible JSON
- created_at
skills & certs
- skill_categories
- skill_nodes
- certifications
For the curious: the one clever bit
The admin dashboard could run a dozen separate queries against the raw events. Instead, a single database function does all the counting and grouping server-side and hands back one tidy JSON blob - KPIs, top pages, a daily series, the game leaderboard.
It's set up so the public can write events but can never read the analytics back out: permission to run that function is granted to logged-in users only.
-- the whole dashboard, one call, admin-only select intel_dashboard(); -- → { kpis, traffic, topPages, daily, leaderboard }
07 Who can do what - try it
A live simulator showing how Row Level Security blocks unauthorized reads and writes.
Deep dive: Interactive RLS simulator
This is the part that makes "no backend server" safe. Pick who you are, pick what you want to do, and see whether the database lets you - and why. This mirrors the actual rules.
08 The signature 3-tier reveal
Projects unfold in three stages (card → hover → modal) without extra network calls.
Deep dive: The reveal pattern
The nicest interaction on the site is how a project unfolds in three stages. Each tier shows more of the same record that was already loaded - so there are zero extra network calls. Try it for real on the homepage project grid; here's the anatomy:
It's a classic UX pattern - progressive disclosure - done without a single extra request, because the full record is already in memory from the first load.
09 The admin console
The protected CMS where I edit projects, manage skills & certs, read messages, and view analytics.
Deep dive: The Ops Console
Behind a login at /admin is the one page only I can use. To the public it's just a
login screen; once authenticated, the database's rules flip open and the page becomes a small
content-management system for the whole site.
From the console I can:
- Edit the live site's content. Add, edit, reorder, publish, or unpublish a project - and the homepage updates instantly. Same goes for the new inline Skills and Certifications editor canvases!
- Read and triage the inbox. See contact messages, flag the important ones, mark them handled, delete spam. The public can send mail but can never see this.
- Watch the visitors. A dashboard of KPIs, top pages, traffic sources, a daily trend, and even a leaderboard for the hidden games - all from that one server-side function.
10 Shipping & little tricks
A few details I'm fond of, for anyone building something similar.
Deep dive: The little details
Two separate update paths. Design and code changes ship through git push →
Vercel. Content changes (project copy, reading messages) happen live in the database via the
admin console. That separation means a recruiter never catches the site mid-redeploy.
Analytics you actually own. No Google Analytics, no third-party script watching visitors. A tiny home-grown tracker writes cookieless events - no IP, no name, just a random id in your browser's storage - and it honours Do-Not-Track. The data lives in my database, and I read it through the admin dashboard.
Hidden games. There's a little driving game and a tic-tac-toe tucked into the site as easter eggs. Beat them and your score quietly joins the leaderboard in the admin dashboard - the same analytics pipe, just having fun with it.
Accessibility and SEO weren't afterthoughts. Skip link, visible focus rings, a
reduced-motion mode, and colour contrast that clears the standard. Because the pages are real
HTML (not rendered by JavaScript), search engines and even language models can read them
cleanly - there's a hand-written llms.txt for exactly that.
The whole point: a portfolio that's fast and simple on the surface, but quietly runs on a real database - and stays safe doing it because the rules live where the data lives.