Architecture
What it is made of
Five small C++ servers, two React front ends and one CLI, serving nine public surfaces. Small enough to read end to end, which is the only security review a one-person project ever really gets. No Docker and no Kubernetes: a personal site should not need an orchestrator to stay up.
5 servers · 2 front ends · 1 CLI
The components
Each server owns one part of the platform and its own schema, so they start, fail and get audited separately. The ports are defaults on a machine you control.
- lnx-url-server
- C++ · :10030
Short links, link trees, banner sets, link previews - lnx-webring-server
- C++ · :10031
Ring directory, membership, next/prev/random navigation - lnx-guestbook-server
- C++ · :10037
Guestbook embeds, public submit, moderation queue - lnx-widgets-server
- C++ · :10039
Hit counters, polls, status pills - lnx-pages-server
- C++ · :10038
Hosted homepages, block editor storage, HTML sanitizer - lnx-admin-react
- React + Vite · :10032
Admin panel — everything you create, you create here - lnx-web-react
- React + Vite · :10033
Public site — webring browser and hosted pages - lnx-client
- C++
Terminal client for mappings — create, list, export, import
The C++ servers bind 127.0.0.1 and sit behind nginx on any deploy.
- Host
- One Debian or Ubuntu box
- Storage
- Postgres 14+
- Deploy
- Ansible + systemd
- Container
- None — host-native
- Runtime
- No Node in production
- TLS
- nginx in front, your certs
One command per environment
Each server applies its own embedded migrations on startup, so a deploy is the role and nothing else. There is no separate migration step to forget.
Why it is split this way
One process per kind of risk. Hosted pages are the only surface that renders markup somebody else authored; everything else emits markup the server wrote. So pages run on their own, and the sanitizer, the stored content and the render path restart and fail without the shortener’s redirect path in the same address space.
Separate schemas, one database. Each server carries its own migrations and its own version lineage over a disjoint set of tables. Adding a widget does not mean a migration that could take the short links down with it.
Nothing is reachable that doesn’t need to be. The servers listen on loopback only. One nginx vhost fronts every public surface, which is why an embed on a hosted page resolves same-origin with a relative path and no hostname is ever baked into stored content.
Host-native, on purpose. One Ansible role and systemd units onto a Debian or Ubuntu box. No containers and no orchestrator: a personal site that needs a control plane to stay up has a second thing that can be down.