engineering
How this site is built, served, and kept running when someone tries to abuse it — security at the core.
/home/blackxploit/engineering
How I built this site — and how I scale it, break it, and fix it
Most personal sites I come across are just a downloaded theme. This one isn't. Here's what a hand-built, actually-engineered site looks like.
This started as a plain blog. I used Jekyll at first — it's genuinely great for blogging: fast, simple, easy to maintain. But I wanted full control, so I needed my own server and my own code. The moment that became obvious was when I tried to password-protect some writeups on Jekyll. It just checked a hash client-side, so anyone with curl could still read the "protected" content. Building real protection into a static site generator was a headache, so I stopped fighting it and built my own backend instead.
The idea is simple: a real server can do things a folder of files can't — decide whether you're allowed to read a writeup, count who's on the site right now, or slow down someone hammering it with requests.
So I rebuilt it as one small server that I run myself. It reads my writeups, turns them into pages, and makes a few decisions before sending anything back to you. This page is about those decisions.
No database, no admin panel. Publishing a writeup means adding one file.
What it's made of
Node.js + TypeScript
Node runs the server. TypeScript adds types on top of JavaScript, which catches a lot of mistakes before the site ever loads.
Express
Decides what happens to each request — think of it as the reception desk every visitor walks past.
Server-side templates
Pages are built on the server and arrive already finished. That's why the site is fast on a slow phone, and why search engines can read all of it.
Markdown for content
Every writeup is one text file with a short header on top. Writing a post means writing — not clicking around a dashboard.
No frontend framework
The interactive bits are a few dozen lines of plain JavaScript. Nothing to download, nothing to keep updating.
Render
One always-on server, not something rebuilt for every visitor. That's the whole reason the live counter and rate limiting can exist at all.
What happens when you click a link
Six steps, same order, every time. The order isn't random.
The two checks sit in the middle on purpose — refusing a bad request should cost almost nothing.
Staying up when someone misbehaves
This is the part I'm happiest with — and also the part I got wrong the first time.
My first attempt was the obvious one: count requests per visitor per minute, and once they cross the line, block them for the rest of that minute. It passed testing. Then I load-tested it and saw what I'd actually built. Lots of people share one internet address at a time — a cafe, an office, a phone network — so one noisy visitor locked out everybody on that address. I'd built a way for one person to take the site down for a hundred others.
So I threw it out and used a token bucket instead. Every visitor gets a bucket of tokens. Each request costs one. Tokens refill slowly on their own. Empty bucket, the next request waits.
Shown small here. A real bucket holds 60 tokens and refills 2 per second, so bursts are fine and floods aren't.
Every part gets its own bucket
Different parts of the site get used differently, so they don't share a budget. One page view pulls in about six files, which is why pages get a generous bucket. Logins get a tiny one — nobody types their password thirty times a minute except someone guessing it. First number below is the burst size, second is how fast tokens come back.
Speed isn't the whole story
Rate limiting only sees speed. Someone quietly trying one web address after another, a few seconds apart, never trips it — which is exactly what a scanner does. So there's a second layer that watches behavior instead of pace.
It keeps a short memory of each visitor: how many pages they asked for, how many didn't exist, how many were different made-up addresses, how many logins failed. Real readers hit a dead link now and then. Scanners walk through hundreds of addresses that were never real. When it looks like that, the visitor gets blocked — a minute, then five, then thirty, then six hours. A quiet day wipes the slate clean, so nobody is punished forever for one bad afternoon.
One detail I care about: I'm not exempt from any of this. My own logged-in session gets a bigger bucket, but it still has a ceiling. Exempting myself would mean that if my login were ever stolen, nothing would stop it.
The little "online now" badge
The number in the header is real. It counts how many people have the site open right now, and updates without you refreshing anything. My first instinct was to reach for the same tech chat apps use for live messages — but that's built for conversations going both ways, and this only ever goes one way. The browser already knows how to hold a one-way connection open and reopen it after a hiccup, so I let it do that instead.
The connection stays open and only ever travels one way. If it drops, the browser reconnects on its own.
Two small touches you'd probably never notice
Three tabs open still counts you once. And I'm never counted at all — otherwise the badge would say "1 online" every time I checked my own site.
Being able to see what's going on
A diary
Every request gets logged: which page, how long it took, whether it worked. Reading it back is how I find out what broke, and when.
A stopwatch per request
Slow requests get timed step by step, so instead of "the site feels slow" I can point at the exact step that ate the time.
A dashboard
A private page shows the totals: pages served, how many were refused, how many visitors are blocked right now.
All three are linked. Each diary entry for a slow request carries a reference number, and that number leads straight to the stopwatch breakdown for the same request. It turns "something is slow" into "this exact step is slow" — the difference between guessing and knowing. Passwords and session cookies are stripped out of the diary automatically, so a login can never end up written in it.
What I actually measured
I didn't want to guess, so I threw traffic at the site until it complained. The results were useful mostly because they were embarrassing. A simple error page handled around 2,800 requests a second. My homepage managed about 85 — same server, same framework. So the slow part was never the templates: the server was re-reading and re-parsing all 36 of my writeups on every single request, just to build the list of recent posts.
I also pushed the live counter to 500 people connected at once, and confirmed the limit turns extra connections away cleanly instead of falling over.
What I fixed after measuring: that repeated reading. Parsed writeups are now cached in memory and only re-read when the file actually changes, finished pages carry a weak ETag so repeat visits get a cheap 304, and stylesheets ship with a content hash so browsers cache them for an hour without ever going stale.
The boring parts that keep it running
Safety headers
Every response carries a strict content policy with a per-request nonce, HSTS in production, and no permission for camera, microphone, or location. Login and state-changing requests also require a same-origin check.
Search-engine manners
Sitemap, Atom feed, canonical tags, and structured data are generated from the same content — locked writeups excluded. Bad crawlers are told to leave; good ones are told where to go and how fast.
Notices + redirects
Site banners update live over the same one-way connection as the online counter. Old Hack The Box URLs redirect permanently to their new homes, so no bookmark rots.
A private /metrics endpoint exposes request counts, timings, and blocked addresses for monitoring, /health answers load-balancer pings, and the server drains live connections gracefully on shutdown instead of dropping them.
What's next
Serving images and stylesheets from a network built for exactly that, and giving the traffic rules shared memory so they keep working if this ever needs more than one server.
This page describes the site's own code. When the code changes, this page changes with it.