Managing reverse proxy routes by hand is fine when you have five of them. It’s less fine when you have a few hundred and someone on your team fat-fingers a backend URL at 2am. At some point you stop editing config files and start building a panel. This is that panel.
The problem
If you’ve been following along with the CDN work on this blog, you’ll know the infrastructure has grown considerably over the past few years. Wildcard SSL certificates, hundreds of routing zones, multiple users who need varying levels of access… The whole thing was held together by a combination of Redis keys, shell scripts, and good intentions.
What I needed was a single pane of glass: somewhere to manage domains, issue certificates, configure routing rules, and do it all without SSHing into a box. Ideally something that wouldn’t fall over the moment PostgreSQL had a bad day.
The stack
The backend is FastAPI running on Python 3.12, async all the way down. SQLAlchemy handles the ORM layer with asyncpg underneath, and Alembic keeps the migrations honest. Redis sits alongside PostgreSQL- Not as a cache-and-forget layer, but as the actual source of truth for the reverse proxy. Zones get synced from Postgres into Redis on an hourly loop, and the proxy reads from Redis directly. Low latency, no excuses.
The frontend is React 19 with TypeScript, bundled by Vite, styled with Tailwind. Dark mode, because obviously. React Router handles navigation, and a context-based auth layer gates routes for regular users vs. admins.
The whole thing ships as Docker Compose. Multi-stage builds for the frontend (Node builds the assets, Nginx serves them), PostgreSQL and Redis containers with health checks, and the FastAPI backend behind Nginx.
Certificates without the pain
This was the part I was most eager to get right. Domain creation triggers a wildcard certificate issuance via acme.sh using DNS-01 challenges through Cloudflare. One cert covers *.example.com and example.com. No per-subdomain renewal headaches. The backend tracks certificate status through a simple state machine: pending, active, or failed. A background task runs every 24 hours calling acme.sh --cron to handle renewals automatically.
The certificate data lives in the database, synced out to Redis for the proxy to consume. If issuance fails, it’s logged and the domain status reflects it immediately in the UI. No silent failures.
Degraded mode
This is where it starts to get interesting. If PostgreSQL goes down and the databases do go down, the panel doesn’t just throw 500s and call it a day. Zone listings fall back to the Redis cache. User authentication works against cached credentials with a 5-minute TTL. The health endpoint reports per-service status so monitoring can tell the difference between “Postgres is down but we’re serving traffic” and “everything is on fire.”
It’s not a replacement for a healthy database, but it buys you time to fix the problem without taking routing offline.
Multi-tenancy
Not everyone needs access to everything. The access control model maps users to specific domains and backends with granular permissions, read-only or edit. Admins see the full picture. Regular users see their slice. There’s a full audit log tracking who did what to which resource and when, which has already saved me from at least one “I didn’t change that” conversation.
API key authentication sits alongside session cookies. The web UI uses sessions for convenience; scripts and automation use API keys. Both paths go through the same permission checks.
Zones and named backends
A zone is a mapping from a frontend hostname to a backend URL. Simple enough, but when you’re managing hundreds of them, you want bulk operations and you want reusable backend definitions. Named backends solve the second problem, define a backend URL template once, reference it everywhere. Presets for common backends vs. custom ones per user.
Bulk create and update endpoints keep the API call count reasonable when you’re standing up a new batch of routes. Everything syncs to Redis automatically.
What’s next
The panel handles the day-to-day well enough now that I’m not manually poking at Redis keys anymore, which was the whole point. There’s more to do, better certificate error reporting, maybe a zone diff view, and some UI polish that I keep putting off in favour of backend work. The usual.
If you’re running a similar setup and wondering whether it’s worth building your own control plane: it is. The first version took longer than expected. Every version after that has saved me more time than it cost.
