This site has a public half and a private half. The private half holds real personal and financial information, which makes exactly one design decision important and everything else a matter of taste.
The one decision that matters
The tempting way to build a “private area” is to render everything, then hide the private parts behind a JavaScript check:
// Do not do this.
if (prompt('Password?') === 'hunter2') {
document.getElementById('private').style.display = 'block';
}
That ships every secret to every visitor and politely asks them not to look.
Disabling JavaScript defeats it. So does opening devtools. So does curl.
Any check that runs in the browser is a suggestion, not a boundary. If the private bytes are in the response, they are public — regardless of what the page chooses to display.
The alternative is to decide before rendering. Every request hits server middleware running in the Cloudflare Worker, which verifies a signed session cookie and, for a private route without one, returns a redirect. The private page component never executes. Nothing private is read, rendered, or serialised.
| Approach | Private data in response? | Survives devtools? |
|---|---|---|
Client-side display: none | Yes | No |
| Client-side fetch after prompt | On second request | No |
| SSR gate in middleware | No | Yes |
The stack
| Layer | Choice | Why |
|---|---|---|
| Framework | Astro (SSR) | Zero client JS by default; the private/public split is a server concern |
| Runtime | Cloudflare Workers | The middleware runs at the edge, before any HTML exists |
| Auth | PBKDF2 + HMAC cookie | No database; secrets live in wrangler secret |
| Content | Markdown + content collections | Notes are files in the repo, versioned with the code |
| Styles | Tailwind | Hand-written prose styles on top, for the tables |
Auth, in four steps
/loginposts a password to a server endpoint.- The endpoint derives a PBKDF2 hash and compares it — in constant time —
against
PASSWORD_HASH, stored as a Cloudflare secret.- The plaintext is never stored anywhere, including in the env var.
- A wrong password and a missing configuration take the same amount of time to answer.
- On success it sets an
HttpOnly; Secure; SameSite=Strictcookie containing a payload signed withSESSION_SECRET. The expiry is inside the signature, so it cannot be extended by the holder. - Middleware verifies that signature on every subsequent request.
Rotating SESSION_SECRET invalidates every outstanding session at once, which is
the log-out-everywhere button.
What I gave up
Nothing is cached at the edge, because every route is server-rendered. For a site of this size that costs a few milliseconds and buys the guarantee above, which seemed like the right trade.
The source is on GitHub.