Projects
-
# Home Server
Everything on this site runs from a single machine sitting in my office — a Beelink SER5 Max mini PC powered by an AMD Ryzen 7 5800H (8 cores, 16 threads), 32 GB of DDR4 RAM, and 1TB NVMe + 1TB SATA SSD. Small enough to hold in one hand. More than enough to host everything I care about.
It started about four years ago with a 2010 Mac Mini pulled out of my dad's closet — barely functional, but enough to get curious. That machine introduced me to the world of self-hosting: wrestling with Apache configs at midnight, learning what a reverse proxy actually does, figuring out why DNS changes take forever to propagate, and slowly building a mental model of how the internet fits together. Every broken config and every "why isn't this working" moment taught me something that no tutorial could have.
The Beelink is what that curiosity eventually earned. It runs Ubuntu Server, manages multiple virtual hosts through Nginx, handles TLS termination, orchestrates containerized workloads with Docker Compose, and serves everything under a single public IP with a domain I actually own. Apache, Nginx, DNS, networking, firewalls, systemd — I've had to learn all of it, piece by piece, because something downstream always depended on it.
This server is the reason the rest of this portfolio exists. It's what lets me take an idea — a game server, a monitoring stack, a whitelist system — and turn it into something real and running, hosted straight from my office.
-
# WireGuard VPN
WireGuard runs natively on the home server, acting as a private gateway back to my own infrastructure from anywhere in the world. Both my phone and my MacBook are registered as peers — one tap in the WireGuard app and either device tunnels directly into my home network, no third-party service involved.
The more interesting benefit is what the VPN makes unnecessary. With secure remote access handled entirely through the tunnel, there's no reason to expose port 22 to the public internet at all — so I don't. SSH is firewalled off at the WAN edge, which means the server is invisible to port scanners looking for an SSH target. Anyone probing from outside sees nothing; anyone who actually needs access connects through WireGuard first, then SSH is right there on the private interface as usual.
Setting this up meant learning how WireGuard's cryptokey routing model works — each peer has a public/private keypair, a pre-shared key for an extra layer of post-quantum resistance, and an allowed IPs range that determines what traffic routes through the tunnel. Getting the server-side
wg0.confright, configuring the firewall to accept UDP on the WireGuard port while keeping everything else locked down, and generating and distributing the peer configs was a satisfying exercise in understanding exactly what's happening at the packet level. -
# Self-Hosted Media Server
A fully self-hosted media streaming setup running on the home server, built around both Plex and Jellyfin. The library is stored locally and streamed on-demand to any device on the network — or remotely, from anywhere in the world.
Plex was the starting point. It's polished, has excellent client support, and was easy enough to get running. The friction came later: Plex's free tier restricts remote streaming behind a paywall, meaning accessing my own self-hosted media from outside my home network required a paid subscription. That felt like the wrong trade-off — paying a recurring fee for permission to reach a server I already own and operate.
The solution was to dual-host Jellyfin alongside Plex. Jellyfin is fully open-source with no paywalls, no account requirements, and no artificial limits on remote access. Both services share the same underlying media library, so there's no duplication — Plex remains available for its client ecosystem, while Jellyfin handles remote streaming without restrictions. Each is proxied through Nginx and served from its own subdomain with TLS.
On top of streaming, a download client runs on the server as well, making it easy to add new content to the library remotely — from any device, without needing to be physically present at the machine. New additions appear in both media servers automatically once complete.
-
# Grafana Monitoring Stack
A self-hosted observability stack running on Docker Compose. Collects host and container metrics via Prometheus, Node Exporter, and cAdvisor, and ships container logs to Loki through Grafana Alloy — all visualized in Grafana and served from a subdomain.
-
# Minecraft Servers
Two separate Minecraft server instances running concurrently on a single physical machine, differentiated by port and managed independently through systemd. Rather than exposing raw server ports to the internet, each server is fronted by an Nginx reverse proxy that serves a whitelist webpage on the corresponding port. When a connection arrives, the page requires the player to submit their username for approval before the backend server port is made accessible to them — preventing opportunistic griefing through port scanning by ensuring anonymous connections never reach a live game socket.
The whitelist pages themselves are lightweight vanilla JavaScript applications that hit a small Node.js API to read and write each server's
whitelist.json. Nginx handles TLS termination, routes traffic between the two virtual hosts, and applies rate-limiting rules to the API endpoints to deter brute-force or scripted submission attempts. The result is two fully isolated game environments sharing one machine and one public IP, each protected by its own access gate. -
# Kurb Alert
Kurb Alert was a location-based social media app I designed and built for iOS using Swift, backed by Google Firebase. The concept centered on letting users discover and share activity happening around them in real time — a genuinely fun idea to build toward.
The project stalled on authentication. I had implemented phone-number sign-in early on, and as problems mounted it became clear the right move was to switch to a different auth method — but I was too afraid to touch a codebase I had already invested so much time in. Rather than making the call to refactor, I held on, and the project never crossed the finish line.
It was an unfinished app, but a formative experience. Working across the full stack — from Swift UI down to Firebase rules and cloud functions — gave me a real appreciation for how all the layers fit together. And the lesson I walked away with has stuck: productivity shouldn't be measured in lines of code, but in the user story. If a change serves the user, it's worth making — even if it means revisiting work you're proud of.
-
# Resume Tracker
A self-hosted, per-recipient resume open-tracking system. The idea is simple: instead of sending your resume as a PDF attachment and wondering whether anyone ever opened it, you send a unique tracking URL —
seniho.com/r/abc123— that silently logs the visit server-side and instantly redirects the recipient to the actual resume. They see nothing out of the ordinary. You see exactly when they opened it, how many times, from what device, which browser, and on which OS.The technical decision that matters here is why a link and not a tracking pixel in the PDF. Most PDF readers block outbound network requests, and email clients often strip remote resources during rendering — making pixel tracking fundamentally unreliable. A redirect-based approach is 100% reliable: it works in every browser and every email client without exception, because the tracking happens at the server level before the redirect is issued.
The whole system runs in a single Docker container behind Nginx: an Express API handles tracking and auth, SQLite stores recipients and view history in WAL mode for safe concurrent writes, and a vanilla JS dashboard (no framework, no build step) lives at
seniho.com/trackerbehind a password-protected login. Sessions use HMAC-signed cookies backed by a scrypt-hashed password stored in the database. Tracking tokens are 8 bytes of cryptographically random data, making enumeration infeasible.Building this with AI — what actually helped. This project was built collaboratively with AI, and the single most useful habit I developed was asking not just what to do, but why. "Why WAL mode for SQLite?" — because WAL allows concurrent reads during a write, which matters when multiple tracking hits land simultaneously. "Why scrypt over bcrypt for the password hash?" — because scrypt is memory-hard, making GPU-based cracking significantly more expensive. Every time I asked the AI to explain its reasoning, the answer either confirmed the decision made sense or surfaced a trade-off I hadn't considered.
That habit became the real sanity check. AI is fast and confident — which is exactly when you need to slow it down and interrogate it. Asking "why would you do it that way?" forced the reasoning into the open where I could actually evaluate it, rather than just accepting an output that looked plausible. More than once, the explanation revealed a subtlety that changed the direction: why
trust proxy = 1is needed in Express when Nginx sits in front, why the session cookie needsSameSite=Laxand notStrict, why changing the password should rotate the signing secret and not just update the hash. The code didn't just get written — it got understood. -
# Agentic Web Developer
This portfolio was built — and is actively maintained — by an AI agent with real web development capabilities. Rather than just generating code snippets to copy and paste, it reads live project files, writes and updates HTML & CSS directly, and ships changes end-to-end in a single conversation.
Need a new project entry? A style refresh? A layout change? Just ask. The agent inspects what already exists, reasons about the best approach, and applies changes immediately — no copy-pasting, no context-switching, no ticket queue.
To make working with the agent even more seamless, a Developer Dashboard was built — itself an AI-generated web app — that serves as a purpose-built interface to the portfolio agent. Rather than interacting through a generic chat UI, the dashboard provides a clean prompt panel for sending instructions directly to the agent, alongside a log viewer that parses and presents the agent's raw activity logs in a readable, structured format. It makes the feedback loop tighter: prompt, observe, iterate — all in one place.