Why I Chose Go + HTMX + SQLite Instead of React for My Portfolio

When I started rebuilding my portfolio, I didn't want to create another static website with a few project cards and a contact form.

When I started rebuilding my portfolio, I didn't want to create another static website with a few project cards and a contact form.

I wanted the portfolio itself to demonstrate how I approach product development.

That meant building something with a real backend, persistent data, authentication, security controls, automated deployment and an actual purpose beyond displaying screenshots of previous work.

The result became a server-rendered Go application backed by SQLite, with HTMX used where additional interactivity makes sense.

React could absolutely have worked.

I just didn't think I needed it.

1. What I wanted the portfolio to become

The original requirement was simple: build somewhere to show my work.

But fairly quickly I realised there was an opportunity to make the portfolio part of the work itself.

Instead of separating my public portfolio from everything happening behind it, I wanted one application with two sides.

The public side would explain what I design and build.

The private side would become an authenticated workspace for managing the actual client relationships behind that work: clients, projects, contracts and eventually other parts of the workflow.

That changed the technical requirements significantly.

I wasn't really building a portfolio anymore.

I was building a small web application that happened to contain my portfolio.

2. Why a static portfolio wasn't enough

There's nothing inherently wrong with a static portfolio.

For a designer or developer who simply needs somewhere to display projects, it can actually be the better solution.

My problem was that it wouldn't demonstrate much of the engineering work I enjoy doing.

I wanted to work with:

  • authentication and authorisation
  • persistent relational data
  • HTTP middleware
  • sessions and cookies
  • CSRF protection
  • application security
  • CRUD workflows
  • database migrations
  • testing
  • CI/CD
  • production deployment

A collection of static pages couldn't meaningfully exercise most of those areas.

Adding a separate dashboard application would solve that, but it would also leave me maintaining two different systems.

Instead, I decided the portfolio and workspace should be one product.

3. Why Go

Go was an easy decision because I wanted the backend to remain the centre of the application.

HTTP requests come into Go, pass through middleware and handlers, interact with the repository and database when necessary, and ultimately produce an HTML response.

There isn't a separate frontend application that needs to reconstruct the same state from JSON.

That gives the project a relatively straightforward request path:

Request → middleware → handler → repository → SQLite → Go model → template → HTML response

It also encourages me to keep the architecture explicit.

The HTTP layer deals with HTTP concerns. Persistence lives behind repositories. Templates deal with presentation. Middleware handles concerns shared between requests.

For a relatively small application, I find that much easier to reason about than introducing another application boundary simply because the browser is involved.

Go also fits the type of software I like building: small services, clear interfaces, relatively few dependencies and boring infrastructure.

And I mean "boring" as a compliment.

4. Why server-rendered HTML

One of the most important architectural decisions was that HTML would remain a server concern.

When somebody requests a page, the server already knows what that page should contain.

It has the session.

It has access to the database.

It knows whether the user is authenticated.

It knows whether a project exists.

It knows what validation errors occurred.

So rather than converting all of that information into JSON, sending it across another application boundary and asking JavaScript to reconstruct the interface, Go renders the HTML directly.

For this product, that keeps the mental model remarkably simple.

Normal links remain links.

Forms remain forms.

URLs represent actual resources.

Browser navigation continues behaving like browser navigation.

JavaScript becomes something I can introduce when it improves an interaction rather than something required before the interface can exist.

5. Why HTMX instead of a SPA

This is probably the part of the stack that sounds most like an anti-React decision, but it really isn't.

React is extremely useful when an application has substantial client-side state and interaction.

My portfolio doesn't.

Most interactions are fundamentally HTTP operations:

Show me this project.

Update this client.

Archive this record.

Create this contract.

Return the updated interface.

I don't need a complete client-side application architecture to accomplish those things.

HTMX lets me progressively enhance the server-rendered application while keeping Go responsible for application state and HTML generation.

If I want part of a page to update without performing a full navigation, the browser can make a request and the server can return the HTML fragment that should replace it.

That means I don't need to introduce a JSON API, duplicate models in TypeScript, add client-side routing and then decide how I'm going to synchronise server and client state.

More importantly, HTMX isn't responsible for the architecture.

If I removed it, the fundamental application could still be built around ordinary HTTP requests and server-rendered pages.

I like that property.

The enhancement isn't the foundation.

6. Why SQLite

The database decision followed the same philosophy.

I could have used PostgreSQL.

For a large distributed application with significant concurrent writes, multiple application instances and more complicated infrastructure requirements, I probably would.

But that isn't what this application is.

The client workspace has relational data, but its scale is tiny.

A client can have projects.

A client can have contracts.

Contracts can optionally relate to projects.

Those relationships still deserve proper constraints and migrations, but they don't automatically require a separate database server.

SQLite gives me a relational database while keeping the operational footprint extremely small.

It also works particularly well with the repository structure I've used.

Handlers don't need to care whether the data comes from SQLite or something else. They depend on repository behaviour rather than scattering SQL throughout the HTTP layer.

If the application's requirements eventually outgrow SQLite, that's a migration I can justify when the requirement actually exists.

I didn't want to design the infrastructure around imaginary scale.

7. What the stack actually supports now

The interesting part is that this is no longer just an architectural experiment.

The portfolio is a functioning application.

Authentication

The private workspace is protected behind authentication rather than being exposed alongside the public portfolio.

Passwords are hashed rather than stored directly, and authentication is handled by the Go application itself.

CSRF protection

State-changing operations are protected against cross-site request forgery.

That includes authentication-related actions rather than treating the login system as somehow separate from the rest of the application's security model.

Sessions

Authenticated sessions are handled server-side with secure cookie controls.

Private responses can also be treated differently from public portfolio pages, including preventing authenticated content from being cached where it shouldn't be.

Clients, projects and contracts

The workspace has moved beyond a generic dashboard.

Clients are real records.

Projects belong to clients.

Contracts can be associated with the appropriate client and, where relevant, one of their projects.

That relationship is represented in the database rather than being implied by disconnected pieces of UI.

Security middleware

Security isn't something I wanted to bolt on after finishing the interesting parts.

The application includes controls around authentication, request handling, cross-origin behaviour, response headers, session handling and login attempts.

Building those features was also part of the point of the project: the portfolio should demonstrate how I think about production software, not just how I make interfaces look good.

CI/CD

Changes go through an automated development workflow rather than relying entirely on manual production deployment.

Tests and other checks can run before changes reach production, making the repository itself part of the product engineering story.

Fly.io + Cloudflare

The application is deployed on Fly.io with Cloudflare sitting in front of the public site.

Again, this is more infrastructure than a static portfolio technically requires.

But the goal isn't to find the smallest possible way of putting my name on the internet.

The goal is to operate a real application.

8. What went wrong while building it

Quite a lot.

And that's probably been more useful than everything that worked immediately.

One recurring problem was allowing the structure to evolve as features were added.

Something that looks perfectly reasonable with three handlers can become annoying once there are fifteen.

Templates that feel organised initially can become harder to navigate.

Middleware ordering starts to matter.

Authentication introduces edge cases.

Database relationships force you to think about what should happen when records are edited, archived or deleted.

Deployment exposes assumptions that local development happily lets you make.

CSS can work locally and then remind you that production build pipelines exist for a reason.

I've refactored parts of the HTTP structure as the application has grown, changed how features are organised and repeatedly found places where an abstraction was either introduced too early or not introduced soon enough.

There were also plenty of smaller problems: redirects, trailing slashes, template behaviour, CSRF handling, migrations and production configuration.

None of those make particularly impressive screenshots.

They're also exactly the kind of problems you encounter when you build actual software.

That's one reason I'm glad the portfolio became more than a static site.

The mistakes became part of the project.

9. What I'd change if I started again

The main thing I'd change wouldn't actually be the technology.

I'd start with a clearer picture of the application the portfolio was going to become.

The early version was still mentally a "portfolio with some backend features."

I now think about it as a product containing a public experience and a private operational experience.

That distinction would have influenced some of the initial package structure, template organisation and feature boundaries.

I'd also establish conventions earlier.

Where does a page handler live?

Where does a command that mutates a project live?

What belongs in middleware?

What belongs in the repository?

When should something become a reusable component?

These aren't difficult questions individually, but consistency becomes increasingly valuable as the codebase grows.

What I wouldn't change is choosing a deliberately small stack.

If I restarted the project tomorrow, Go, server-rendered HTML, HTMX and SQLite would still make a lot of sense.

Not because they're universally better than React, Next.js, PostgreSQL or any other popular technology.

They're better aligned with this particular product.

And that's a distinction I think gets lost surprisingly often in discussions about web development.

10. The result

The portfolio now does considerably more than display my work.

It is part of my work.

It demonstrates UI/UX decisions on the public side while the application behind it demonstrates backend architecture, relational data modelling, authentication, application security, testing and deployment.

There are certainly applications I would build with React.

There are applications where I'd immediately reach for PostgreSQL.

There are products where a dedicated API and separate frontend are absolutely the correct architecture.

This just isn't one of them.

Choosing a smaller stack meant I could spend more time building the product and less time maintaining boundaries the product didn't require.

And that has probably been the most useful lesson from building it:

Complexity should be something a product earns, not something a project starts with.

You can view the live portfolio or explore the source code on GitHub.