Building Tirraflow Part 1: A Full-Stack Blog Platform from Scratch
I know what you're thinking — why build a blog platform when WordPress, Ghost, and Medium already exist? Honestly? I wanted to learn. I wanted to make decisions, break things, fix them, and...
I know what you're thinking — why build a blog platform when WordPress, Ghost, and Medium already exist?
Honestly? I wanted to learn. I wanted to make decisions, break things, fix them, and understand every layer of a real production app. Not from a tutorial. From actually shipping something.
So I built Tirraflow — the platform you're reading this on right now. And I'm going to tell you how.
The Stack
Here's what I landed on after some back and forth:
Frontend: React 19 + Vite + Tailwind CSS
Backend: Go with Gin and PostgreSQL
Auth: Keycloak (OAuth2 with PKCE)
Deployment: Docker Compose on a Hetzner VPS, behind Cloudflare
Here's an overview of how it all fits together:

The Frontend
I went with React and Vite because honestly, the developer experience is hard to beat. Hot reload in milliseconds. Production builds in under 3 seconds. And React 19's ecosystem just has everything you need.
The interesting parts:
TanStack Query changed everything. Before I added it, I was manually managing loading states, caching, and refetching everywhere. Now, hovering over a post card silently prefetches the article data. By the time you click, the page loads instantly. Likes and bookmarks update optimistically — you see the result before the server responds.
Dark mode was more work than I expected. Every single Tailwind class needs a dark: counterpart. It sounds simple until you're 40 components deep and realize you missed the border color on a dropdown. But the result is worth it — the theme respects your system preference and remembers your choice.
The editor was the hardest part. I wanted both a rich text editor (for people who prefer visual editing) and a Markdown mode (for developers). TipTap handles the rich text side — tables, code blocks with syntax highlighting, resizable images.
The Backend
I picked Go for the backend, and I'm glad I did. One compiled binary, no runtime dependencies, and it starts in milliseconds. The entire API — posts, comments, auth, file uploads, search, RSS — runs without heavy frameworks or abstractions.
Every handler follows the same boring pattern: validate input, call the database, return a response. No magic. No hidden layers. Just straightforward code that does what it says.
One decision I'm proud of: skipping Elasticsearch (for now). I almost added it for search. Then I tried PostgreSQL's built-in full-text search tsvector and tsquery — and it does everything a blog needs. Prefix matching, result ranking, context snippets. One less service to worry about.
It's simpler than you think: one tsvector column, a GIN index on it, and a trigger that keeps it current whenever a post is saved.
Authentication
I did not want to roll my own auth. That's how you end up on the news !
Keycloak handles everything: login, registration, password reset, OAuth2 tokens, role management. I just had to integrate it. The tricky part was making it not annoying.
Most Keycloak tutorials show you a "Loading authentication..." screen that blocks the entire app on every page load. That's terrible for a blog — most visitors are anonymous readers who don't care about auth. So I made it non-blocking: the app renders immediately, and Keycloak silently checks your session in the background using a hidden iframe. First-time visitors see the blog instantly. Returning users get logged in without noticing.
Getting that right took some trial and error. I wrote a dedicated article about it: Keycloak Silent SSO in React.
Deployment
The whole platform runs on a single Hetzner VPS. Here's what happens when you open an article:

Five containers: Nginx, the frontend, the API, PostgreSQL, and Keycloak (six today: a backup service joined them, see the update below). Two networks — one public-facing, one internal.
Both the frontend and backend use multi-stage Docker builds. The frontend compiles with Node, then gets served by a tiny Nginx image. The Go API compiles to a single binary and runs on a minimal Debian image. Small images, fast deploys.
GitHub Actions runs lint, tests, and builds on every push. The backend pipeline even spins up a real PostgreSQL container for integration tests — no mocks.
What I Learned
A few things that surprised me:
Validation has to live in two places. The frontend validates for a smooth user experience — character counters, max lengths, instant feedback. The backend validates for security — it's the real gatekeeper. Both enforce the same limits. I keep them synced with a shared table in the docs. It's tedious, but necessary.
Small UX details matter more than features. The reading progress bar at the top of articles. Keyboard shortcuts (/ to search). Auto-saving drafts every 30 seconds. Skeleton loading screens instead of spinners. None of these are "features" but they're what make the app feel polished.
Start simple. Seriously. I could've spent months designing a perfect architecture. Instead, I shipped with Docker Compose on a single VPS. It works. When I need to scale, I'll scale. But right now, simplicity wins.
Update: Six Months Later
The shape above still holds; what changed is around the edges. Keycloak moved to the same origin under /auth, because browsers started partitioning cookies inside cross-site iframes (Part 2 has the story). A sixth container dumps both databases and the uploaded images every night. A merge to main deploys itself, waits for health checks and rolls back on failure (Part 4). And every article is also served as plain Markdown, with an llms.txt index, so AI agents read the text instead of a JavaScript shell.
What's Next
Part 2: Keycloak Silent SSO in React, No Loading Screens — non-blocking auth for a SPA, and why Keycloak ended up same-origin.
The Series
Building Tirraflow is a 5-part series on how the platform you are reading was built and run; every part is linked below.
Part | Topic |
|---|---|
Part 1 (this post) | The stack, the architecture, and what I learned shipping it |
Non-blocking auth for a SPA, and why Keycloak ended up same-origin | |
Testing the whole frontend with no backend at all | |
From push to production, and how the pipeline learned to roll back | |
How it got in, and the six layers that keep it out |