Skip to main content

How We Built a PaaS with Go, Kubernetes, and React

· 5 min read

Building a PaaS as a solo founder means making choices. Some deliberate, some accidental, all of them tradeoffs.

Every tool comes with pros and cons, and the deciding factor is usually the most expensive resource of all: time.

If I can get the job done with something I already know, I'll take that path. I'll learn new tools when the project pays for it. Until then, it's all about moving forward with what works.

Here's how Hostim.dev is put together today – the stack that runs every app, database, and service behind the scenes.


🖥 Infra: Ansible + Kubespray

Before Kubernetes even comes into the picture, there's infrastructure to manage.

I've spent six years working with Ansible, so it was my first pick. Hostim.dev runs on bare metal servers – and the Kubernetes clusters on top of them are provisioned with Kubespray, which itself is a set of Ansible playbooks.

That means everything integrates nicely:

  • My own playbooks handle server lifecycle (deploy new users, rotate keys, manage credentials).
  • Kubespray handles cluster lifecycle (deploy, upgrade, or scale clusters).

Could Terraform do this job too? Maybe. But I'd spend more time learning it than deploying clusters. That's the tradeoff.

The upside: flexibility. I can run only the parts I need, when I need them. The downside: it's not centralized – I run playbooks from my own machine. If two people apply different changes at the same time, you could hit conflicts. For now, that's a human problem, and we'll solve it in a human way.


⚙️ The Kubernetes Operator

The heart of the platform is a custom operator written in Go with Kubebuilder.

If you're not familiar: an operator is basically a program that runs in the cluster and ensures the "desired state" matches the "actual state."

Examples:

  • Update an app's environment variables → operator notices, triggers a restart.
  • Create a new database → operator picks a server, provisions it, updates permissions, applies migrations.
  • Scale an app → operator reconciles replicas until the cluster matches your request.

It also emits the events you see in the dashboard. The backend subscribes to them, stores them, and triggers UI updates so what you see is always fresh.

This piece does a lot – from managing app lifecycles to handling Redis and Postgres placements – and is probably the best candidate for open-sourcing later on. No fixed timeline yet, but it's on my mind.


🔗 Backend API: Schema First

All the business logic sits in the backend. It's written in Go, with:

  • Gin for the HTTP server
  • Ent as the ORM
  • oapi-codegen to generate code from an OpenAPI schema

The workflow looks like this:

  1. Write the OpenAPI schema first.
  2. Generate the server interfaces with oapi-codegen.
  3. Implement the interfaces manually.
  4. Wire them up with Ent models and Kubernetes operator objects.

It's not 100% smooth (Ent and oapi-codegen don't integrate perfectly, so there's some type conversion glue). But overall, it means less boilerplate and more consistency.

At the end of the day, three parts come together:

  • K8s objects (via the operator's Go package)
  • Database code (via Ent)
  • HTTP API (via oapi-codegen)

My job: glue them together and add business logic. Which is exactly what Hostim.dev runs on today.


🎨 Frontend: React + Ant Design

This is where I had the least experience. A good friend helped me bootstrap the project, and I leaned on LLMs for some of the early decisions.

Framework of choice: React + TypeScript. UI library: Ant Design (Antd) – suggested by an LLM, picked mostly on a gut call. It does the job.

Could I have picked Svelte or Vue or "framework XYZ" instead? Sure. But I had a friend to guide me through React, not those other frameworks. And that meant I could start shipping right away. That's the tradeoff.

What I'm happy about is how code generation carries through to the frontend. The OpenAPI client is autogenerated, so the frontend just calls strongly typed functions.

If I change an object in the backend and re-generate, any breaking changes are immediately visible in the IDE. That feedback loop saved me a ton of time and bugs.


Wrapping Up

So that's the stack:

  • Ansible + Kubespray for infra
  • Go + Kubebuilder operator for apps, DBs, Redis, volumes
  • Go + Gin + Ent + OpenAPI for backend
  • React + Ant Design for frontend

It's not perfect. Every layer has its tradeoffs. Some tools might be "hotter" or "easier," but experience and context matter more. In the end, the real problem to solve isn't "which framework is coolest" – it's time.

That's true for me building the platform, and it's true for anyone using Hostim.dev instead of wiring up VPS configs or AWS bills.

Think of it like this: you can choose Terraform vs. Ansible, React vs. Vue… and you can also choose "Do I spend Saturday night fixing SSL, or do I just deploy and move on?" 😅

👉 If you want to try it out, join the beta here: hostim.dev