Case study
Portfolio Platform
A self-hosted portfolio platform with a custom SQLite CMS, structured case-study editing, persistent media, revision history, and a shared data layer used by both the Next.js website and Go SSH interface.

Technology
- TypeScript
- React
- Next.js
- Tailwind CSS
- SQLite
- Framer Motion
- Role
- Sole developer
- Year
- 2026
- Status
- In active use
Context
When content updates became code changes
My portfolio originally stored every project and case study in a single TypeScript file. This worked while the content was relatively static, but longer case studies gradually turned routine writing changes into application changes.
Updating a paragraph, changing an image, or documenting a new feature meant editing source code, creating a commit, and deploying the entire site. A growing portion of the repository history consisted of content-only commits, and the same project information was also hardcoded separately in my Go SSH portfolio.
I built a private content system so projects could be edited independently from the application while still supporting the custom layouts used throughout the site.
The main design problem
Rich enough for case studies, constrained enough to edit
The difficult part was not storing project metadata in SQLite. Case studies contain paragraphs, highlighted observations, ordered flows, structured comparisons, image groups, captions, and layout-specific presentation choices. A basic collection of text fields would not be expressive enough, while unrestricted HTML would make the editor fragile and tightly couple stored content to the current frontend.
I designed the case-study format as a small collection of structured content blocks. Each block describes the meaning of the content rather than its final HTML, allowing the public site to remain responsible for presentation.
- Paragraphs
- Long-form narrative content separated into readable passages.
- Highlights
- Short observations or constraints that need stronger visual emphasis.
- Steps
- Ordered processes such as search, validation, publishing, or request lifecycles.
- Details
- Repeated title-and-description pairs for comparisons and technical breakdowns.
- Images
- Media references with alternative text, captions, dimensions, layout, and lightbox preferences.
The system
One source of truth for two portfolio interfaces
The Next.js application now stores projects in a SQLite database outside the deployed repository. Published metadata remains available through ordinary columns, while each case study is stored as validated structured JSON. Drafts and revision snapshots are kept separately from the currently published version.
The public website reads published projects through a repository layer. A dedicated published_projects database view exposes the same content contract to my Go SSH portfolio, which opens the database read-only and adapts the rows into its terminal UI model.
Publishing once therefore updates both interfaces. The Next.js site renders the full visual case study, while the SSH portfolio uses the fields relevant to its keyboard-driven terminal experience.
Content workflow
Separating editing from publishing
Editing and publishing are separate operations. A draft can be changed privately without affecting either public portfolio. Publishing validates the complete document, updates the public project record, and creates an immutable revision snapshot before the new content becomes visible.
- 01
Edit
Build project metadata and case-study sections through the private editor.
- 02
Validate
Show readable field-level errors for missing images, alternative text, headings, malformed slugs, and invalid nested content.
- 03
Save
Store unfinished changes as a private draft without changing the public website.
- 04
Publish
Update the published record and preserve the document as a revision.
- 05
Consume
Let the Next.js and Go applications read the same newly published project.
Persistent media
Keeping uploaded content outside deployments
Images were originally stored in the repository’s public directory. That was appropriate for versioned assets, but not for files uploaded through an editor: an automated deployment could replace the application directory and remove anything created at runtime.
Uploaded images now live under persistent application data on the Raspberry Pi. The CMS validates each upload, processes it into WebP through Sharp, records its metadata in SQLite, and exposes it through the /media public path.
Project deletion also collects every media reference from the published document, current draft, and revision history. Because media is owned by one project, the corresponding database records and physical files can be removed together without leaving unused uploads behind.
Administration
A private editor without a public login system
The editor is intended for one administrator on a self-hosted server, so I did not build a multi-user authentication system. Nginx restricts /admin and its descendants to my public IP before the request reaches Next.js.
After the IP check, Nginx replaces a trusted internal header that the application verifies on every admin page, server action, and media endpoint. Client-supplied versions of that header are stripped from ordinary requests, and the application port is blocked from direct public access by the host firewall.
This keeps the authorization boundary at the reverse proxy while still requiring the application to verify that each administrative operation passed through it.
Operations
Developing against real content without committing it
Moving content out of Git created a new development problem: a fresh local database no longer contained the projects visible in production. I added a one-way synchronization command that connects to the Raspberry Pi over SSH, creates a consistent SQLite snapshot, and downloads it together with the persistent media directory.
Both local applications can read that ignored snapshot. The Next.js editor uses it directly, while the Go portfolio points its database environment variable at the same file. Production remains the source of truth, and local development never writes back to the live database.
Remaining limitations
Purpose-built rather than general-purpose
- Single administrator
- IP-based access fits my current deployment but would not be appropriate for multiple editors or frequently changing networks.
- Structured layouts
- The editor supports the layouts used by my portfolio, but adding a fundamentally new presentation still requires a new block type and renderer.
- Local storage
- SQLite and filesystem media keep deployment simple, but both must be backed up and remain available to every consuming application.
- Revision model
- Revisions preserve complete published documents rather than presenting field-level diffs or collaborative editing.
What I learned
Content becomes infrastructure once more than one interface depends on it
This project began as an attempt to avoid editing a TypeScript file and grew into a lesson about content modeling, persistence, trust boundaries, and shared data contracts. The most important decision was not choosing SQLite; it was deciding which parts of a case study were content and which parts should remain presentation.
Separating those responsibilities made it possible for two very different interfaces to share the same project data without forcing either one to understand the other’s rendering system.
It also reinforced that small workflow problems can justify dedicated tooling once they occur frequently enough and begin affecting multiple systems. The result is intentionally specific to my portfolio, but it gives me a publishing workflow I can continue using as the projects and case studies grow.