Brian Kimurgor
Get in touch
Behind the siteProduction architecture

One VPS, two systems, zero magic

This site isn't just a portfolio — it's a small production platform: a Next.js app with an admin CMS and public API, plus a self-hosted relay that fans one OBS stream out to multiple platforms. Here's how it all fits together.

Topology

InternetFirewall · only HTTPS + restricted RTMPVPS · nginx + pm2 + postgresExternalVisitors · crawlersbrowsers · search · AI modelsAdminCMS + stream dashboardGitHub ActionsCI · deploy on pushOBS · encoderone authenticated RTMP pushnginxTLS · reverse proxythe only public HTTP doorNext.js · portfoliosite · admin CMS · public APIpm2 cluster · loopback-onlystream-relayExpress · session engineloopback-only · secret authMediaMTXRTMP ingest (firewalled)control API · loopback-onlyffmpeg · per session−c copy · tee fan-outspawned by the relayPostgreSQL · loopback-onlyportfoliocontent · users · api keysstream_relayencrypted keys · sessionsSaaS APIsCloudinary · mediaPlunk · emailPostHog · GA · analyticsYouTube Livertmps ingestFacebook Livertmps ingesthttpshttpsci deployrtmpreverse proxyauth proxyingest statusspawnpullrtmpsrtmpsprismaprismauploads · email
request / control live media path deploy / outbounddashed zones = trust boundaries

Five flows that explain everything

F1 A visitor loads a page

browser httpsnginxNext.jsportfolio db

Static pages are pre-rendered and served without touching the database. The public REST API adds API-key auth and per-key rate limiting on top.

F2 I control the stream from the admin dashboard

admin browser httpsnginxNext.js auth proxy secretstream-relayrelay db

The browser never talks to the relay. A session-guarded server-side proxy forwards only allowlisted endpoints and injects a shared secret the client never sees.

F3 Going live — the hot path

OBS rtmpMediaMTX pullffmpeg −c copy teeYouTube + Facebook

Zero transcoding on the server: the stream is encoded once at home and byte-copied to every platform, so VPS CPU stays near idle while live. If one platform drops, the others keep going.

F4 The LIVE badge you might see in the navbar

any visitorNext.jsrelay public-status{ live, watchUrls }

The public endpoint exposes only whether I'm live and where to watch — never keys, stats, or session internals.

F5 Shipping code

git pushGitHub Actions typecheckdeploy over SSHpm2 reload

The portfolio deploys itself on every push to master. The relay is deployed deliberately by hand — streaming infrastructure shouldn't restart just because I fixed a typo on the blog.


What's reachable from the internet

ServiceExposureWhy
nginx (HTTPS)PublicThe site — the only public HTTP door
RTMP ingestFirewalled + authenticatedAccepts exactly one stream: mine
Next.js app serverLoopback onlyReached exclusively through nginx
stream-relay APILoopback onlyReached exclusively through the auth proxy
MediaMTX control APILoopback onlyPolled by the relay for ingest state
PostgreSQL (×2 databases)Loopback onlyOnly the two apps ever connect

Design decisions I'd defend

Two databases, one Postgres

  • Each app owns its schema and migrations — a relay migration can never touch portfolio tables.
  • Either database can be backed up, restored, or dropped without touching the other.
  • Same server, same operational cost — isolation for free.

The relay hides behind the portfolio

  • One public origin: no CORS, no second TLS certificate, no second attack surface.
  • The portfolio already has authentication — the proxy reuses it instead of duplicating it.
  • The relay is built SaaS-ready: everything is keyed by account, so multi-tenancy is additive.

Copy, don't transcode

  • ffmpeg's tee muxer duplicates the encoded stream instead of re-encoding it.
  • Encoding quality is decided once, in OBS, on hardware built for it.
  • Adding a platform is one entry in a map — the engine doesn't change.

Defense in layers

  • Internal services bind to loopback — unreachable from the internet by construction.
  • Every relay request needs a shared secret; stream keys are AES-256-GCM encrypted at rest.
  • APIs only ever return masked credentials, and the firewall drops everything not explicitly opened.

Built and operated by one person. Questions welcome.

See the projects →Try the public API