Portfolio API
This site's public api: a Go binary with SQLite on a free VPS, behind Caddy with automatic TLS. Content lives in git and is re-imported on every start; a release tests the bundle on a scratch database and rolls back on its own if the new version does not come up. It replaced a Next.js api on Postgres without changing any of the 51 compared responses, and used 14 MB of memory on its first release.
- category
- personal
- stack
- Go
- SQLite
- Caddy
- systemd
- GitHub Actions

Context
This site's api started as Next.js on Postgres, Prisma, and paid queue and e-mail services, designed for an admin that did not exist yet. I'm the only one who writes the content, and the only reader is the site itself.
Pressure and constraint
No new paid services. The api had to fit on a free VPS that already runs another service of mine, and a bad release could not take the site down.
Decision
A single Go binary with SQLite behind Caddy. Content lives in git and the database is derived from it: every start re-imports the bundle, so the content backup is the repository itself. The contract between the site's TypeScript and the api's Go is checked at three points: Zod at export, strict import in Go that rejects unknown fields and the wrong bundle version, and response validation in the client.
Execution
A release tests the bundle on a scratch database before switching versions, swaps the link atomically, waits for readiness and rolls back on its own if the new version doesn't come up. The server keeps three versions, which is why migrations only add: the previous binary must start on the already-migrated database. The database is copied every day and before every migration, and a copy only counts after it passes an integrity check. The service runs sandboxed by systemd, with no shell, no writes outside its own folder and a memory cap. The health check reads the database and answers 503 when it fails, separate from a liveness probe that touches nothing, and an external schedule checks readiness every hour.
What was measured and cut
- Keeping the Next.js api on Postgres. I estimated 150 to 250 MB of memory and a managed database to serve content that only changes when I commit.
- Rust. The memory gain doesn't matter at this load, and slow compiles would weigh on every CI run.
How I know it worked
Before the switch, both apis answered the same 51 requests over the same content, and the responses came out identical apart from ids and creation dates. On every pull request, CI exports the real content and runs the Go tests with the race detector against it.
- api memory
- 14 MBRSS on the first production release, against 150 to 250 MB estimated for the previous api
- responses compared
- 51identical between the old and new api before the switch, apart from ids and dates
- versions on the server
- 3automatic rollback to the previous one if the new one isn't ready
impact
The site has its own backend at zero infrastructure cost, and publishing is one command that never leaves a broken version serving the site.
what transfers
If one person writes and content changes by commit, the database can be derived from the repository. Backup and audit come for free, and what's left is the deploy.