Platform Overview · How it works
5 min read- How it works
- The big picture
- The products it hosts
- The backend: .NET microservices
- Messaging: how services talk
- Identity and SSO: Keycloak with a realm per product
- Multi-tenancy: shared infrastructure, isolated data
- Ingress and routing: Traefik
- The frontends
- Where it runs: K3s on Hetzner
- Built for reuse (brief — has its own module)
- How it was built (brief — has its own module)
How it works
The core idea is shared infrastructure, isolated data. One running set of services and one database serve many customers (tenants) and several distinct products, while keeping each tenant's data strictly walled off from every other tenant's. The sections below walk through the layers from the outside in.
The big picture
Internet
│
┌─────────▼──────────┐
│ Traefik │ ingress / reverse proxy
│ • TLS termination │ (Let's Encrypt certs)
│ • host routing │
│ • middleware │ (security headers,
└─────────┬──────────┘ custom-domain rewrites)
│
┌────────────────────┼─────────────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌───────▼───────┐
│ Keycloak │ │ Frontends │ │ Static pages │
│ (SSO/IdP) │◄────►│ RN-Web / │ │ landings, │
│ realm/prod │ │ PWA apps │ │ marketing │
└─────────────┘ └──────┬──────┘ └───────────────┘
│ (calls APIs)
┌────────────────────┼───────────────────────────────────┐
│ │ │
┌──────▼──────┐ ┌──────────▼─────────┐ ┌──────────────┐ ┌──────▼──────┐
│ Tenants / │ │ Surveys (Erevna) │ │ Menus │ │ Content / │
│ Identity │ │ │ │ (Katalogos) │ │ Storage │
└──────┬──────┘ └─────────┬──────────┘ └──────┬───────┘ └──────┬──────┘
│ │ │ │
┌──────▼──────┐ ┌─────────▼──────┐ ┌──────────▼───┐ ┌───────▼──────┐
│Notifications│ │ Payments / │ │ Events │ │ S3-compatible│
│ │ │ Billing │ │ (Kefi) │ │ object store │
└──────┬──────┘ └─────────┬──────┘ └──────┬───────┘ └──────────────┘
│ │ │
└───────────┬───────┴────────────────┘
│ events (publish / subscribe)
┌───────▼────────┐ ┌──────────────┐
│ RabbitMQ │ │ PostgreSQL │ per-tenant
│ (messaging) │ │ (shared DB) │ row isolation
└────────────────┘ └──────────────┘
Everything above runs as pods in the `dloizides` namespace on a
self-managed K3s cluster (Hetzner). Images are digest-pinned and
pulled from a private registry over a WireGuard VPN.
The products it hosts
Rather than one app, the platform runs a catalogue of independent, live products that all lean on the same backbone:
| Product | What it is |
|---|---|
| Erevna | Surveys / forms |
| Katalogos | Online menus (for restaurants and cafés) |
| Kefi | Event pages with custom domains |
| Browser games | e.g. Aurora (a puzzle game), Morphe, KeyboardPiano |
| Small tools | e.g. Quotes |
| Static pages | Landing / marketing pages |
Each product is a real, separately usable application. They share runtime services (identity, notifications, storage, billing) but present as distinct brands and experiences.
The backend: .NET microservices
The backend is a set of .NET microservices, organised so that each service owns exactly one bounded concern:
- Tenants / identity — who the customers are and which tenant owns what.
- Surveys — the engine behind Erevna.
- Menus — the engine behind Katalogos.
- Content / storage — files and images.
- Notifications — emails and other messages out.
- Payments / billing — subscriptions and money.
- Events — the engine behind Kefi.
Splitting by concern means each service can be deployed, scaled, and reasoned about on its own, and a problem in one product is far less likely to take down another. Services keep their data in PostgreSQL and push files/images to S3-compatible object storage rather than holding binaries in the database or on a pod's local disk.
Messaging: how services talk
Services avoid tangling themselves into a web of direct, synchronous calls. Instead they communicate over RabbitMQ, a message broker, using a publish/subscribe style:
Survey response submitted
│
┌────────▼─────────┐
│ Surveys service │ publishes an event ──► RabbitMQ
└──────────────────┘ │
┌───────────────────┼───────────────────┐
▼ ▼ ▼
Notifications Billing/usage (other subscribers)
(send an email) (count toward plan)
The producing service announces "this happened" and doesn't need to know who cares. Interested services subscribe and react. This decoupling is what lets the suite grow: adding a new consumer of an event doesn't require touching the service that emits it.
Identity and SSO: Keycloak with a realm per product
Single sign-on and user management are handled by Keycloak, an identity provider (IdP). The key design choice here is a separate realm per product:
| Product | Keycloak realm |
|---|---|
| Erevna | questioner |
| Katalogos | onlinemenu |
A realm in Keycloak is an isolated space of users, credentials, and clients. Giving each product its own realm means an Erevna account and a Katalogos account are genuinely separate identities — one product's users, logins, and password policies never bleed into another's. SSO still works within a product (one login flows across that product's apps), but products don't share a user directory.
Multi-tenancy: shared infrastructure, isolated data
A tenant is a customer organisation. The platform is multi-tenant: many tenants share the same running services and the same database. Isolation is enforced at the data layer:
- Every tenant-owned row carries a tenant id.
- Queries are automatically filtered by the current tenant, so application code can't accidentally read across tenant boundaries.
PostgreSQL (shared)
┌─────────────────────────────────────────┐
│ menus │
│ ┌────────┬──────────┬────────────────┐ │
│ │ row id │ tenant_id│ ... │ │
│ ├────────┼──────────┼────────────────┤ │
│ │ 1 │ A │ "Café Alpha" │ │ ◄─ tenant A only sees these
│ │ 2 │ A │ "Lunch menu" │ │
│ │ 3 │ B │ "Bistro Beta" │ │ ◄─ tenant B only sees these
│ └────────┴──────────┴────────────────┘ │
└─────────────────────────────────────────┘
every query is auto-scoped to one tenant_id
This is the classic trade-off of pooled multi-tenancy: you get excellent resource efficiency (one set of services to run and patch) at the cost of having to be rigorous about that filtering — which is why it's automatic rather than left to each query.
Ingress and routing: Traefik
All inbound traffic enters through Traefik, the ingress controller / reverse proxy. It does three jobs:
- TLS termination — it serves HTTPS and obtains/renews certificates automatically via Let's Encrypt.
- Host-based routing — it reads the hostname of each request and forwards it to the correct service (this is how many products and many custom domains share one cluster).
- Middleware — small, reusable request transforms applied per route, such as adding security headers and path rewrites for custom domains (e.g. mapping an event organiser's own domain onto Kefi's internal route).
GET some-restaurant.example ──► Traefik ──► (rewrite) ──► Katalogos menu
GET myevent.organiser.com ──► Traefik ──► (rewrite) ──► Kefi event page
GET erevna.dloizides.com ──► Traefik ──────────────► Erevna frontend
The frontends
The user-facing apps are React Native Web / PWA — web-first applications written with React Native Web and delivered as Progressive Web Apps. A shared UI kit gives every product a consistent set of components and behaviours, so a new product doesn't start its interface from scratch. The static landing/marketing pages are plain served pages rather than full apps.
Where it runs: K3s on Hetzner
The entire platform runs on a self-managed Kubernetes cluster using K3s (a lightweight Kubernetes distribution) on Hetzner hardware. A few operational facts define how it behaves:
- Everything lives in the Kubernetes namespace
dloizides. - Container images are digest-pinned — deployments reference an image by its exact content digest, not a moving tag like
latest, so what's running is reproducible and can't silently change. - Images are pulled from a private container registry that is only reachable over a WireGuard VPN, keeping the build artifacts off the public internet.
Built for reuse (brief — has its own module)
The platform deliberately minimises copy-paste. Cross-cutting logic is extracted into shared libraries — NuGet packages for the .NET services and *@dloizides/ NPM packages for the frontends — and there's an end-to-end scaffolder, create-dloizides-app**, that generates a brand-new product from backend to frontend. This is what makes "one engineer, many products" feasible. (Reuse and scaffolding have their own dedicated module; this is just the headline.)
How it was built (brief — has its own module)
The platform was designed, built, and is maintained largely with Claude Code — agentic, AI-assisted development using multi-agent workflows. (This too has its own module; mentioned here only so the architecture makes sense in context.)