Platform Overview · How it works

5 min read
Foundational8 min read
Rapid overview

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:

ProductWhat it is
ErevnaSurveys / forms
KatalogosOnline menus (for restaurants and cafés)
KefiEvent pages with custom domains
Browser gamese.g. Aurora (a puzzle game), Morphe, KeyboardPiano
Small toolse.g. Quotes
Static pagesLanding / 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:

ProductKeycloak realm
Erevnaquestioner
Katalogosonlinemenu

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:

  1. TLS termination — it serves HTTPS and obtains/renews certificates automatically via Let's Encrypt.
  2. 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).
  3. 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 librariesNuGet 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.)

See also