Portfolio
A Next.js portfolio with a shared PostgreSQL CMS, Drizzle content queries, technical case studies, and public aggregate analytics.
- Next.js Server Components
- Supabase and PostgreSQL CMS
- Google Drive PDF export
- Resend contact workflow

The product tension
I needed to update experience, project evidence, and articles without maintaining the same facts in both CMS records and source files. The public site also needed to distinguish published work from drafts without depending on a visitor being signed in.
What I built
Portfolio reads published CMS content through typed Server Components and a one-minute cache. Work visibility and case-study publication are separate controls; hidden or unpublished studies do not become public detail pages.
The name comes from a shared identity registry, while positioning, biography, and project narratives stay editable in the CMS. Résumé delivery, contact email, GitHub activity, and aggregate Cloudflare analytics have their own server-side provider boundaries. See the CMS walkthrough and the Drizzle migration.
How the system fits together
A product-owned loader composes public content reads into one SQL statement through Drizzle. It selects explicit columns from the portfolio schema under anonymous RLS, then maps the results into editorial view models.
Admin writes the same content contract through authorized operations. Portfolio owns its editorial components; identity, URL, SEO, and content contracts are shared packages. This keeps the public reading experience separate from the authenticated application shell.
The calls that shaped the build
- 01
Keep one source for editable content
Missing required CMS records surface as errors instead of silently falling back to a second static biography or project catalog.
- 02
Use a restricted public reader
Portfolio uses its own database login and anonymous RLS for public content, including requests from signed-in administrators. It has no service-role client.
- 03
Make discovery part of publishing
Published pages supply canonical URLs, sitemap entries, and person or article structured data. Blog and case-study bylines link back to About.
Trust boundaries
Public selects are column-limited. Administrative writes never run in this app. Contact submissions are validated and throttled; I do not persist raw network identifiers as a lead database. Analytics on the public site are aggregate Cloudflare signals, not visitor dossiers.
What I chose—and accepted
CMS reads introduce database and cache dependencies. Incremental regeneration can briefly serve older content after a save; I accept that freshness window to avoid fetching every editorial record on every visit. Provider failures have their own fallback or unavailable states rather than inventing replacement content.
Why the work matters
Visitors can move from a project summary to its implementation, tradeoffs, and related writing. Admin and Portfolio use the same records, and public content queries stay constrained by RLS. The deployed pages can be checked through the live site, with implementation details in the linked articles.
What I would improve next
Add a preview and field-level comparison before publication, then measure the reading experience across devices using the existing aggregate performance signals.