Skip to content
alexbeh.me
Log in
3 min read

How this site is built

Astro SSR on Cloudflare Workers, with a private area gated in server middleware rather than in the browser. Notes on why it is arranged that way.

  • web
  • astro
  • cloudflare
  • security

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.

ApproachPrivate data in response?Survives devtools?
Client-side display: noneYesNo
Client-side fetch after promptOn second requestNo
SSR gate in middlewareNoYes

The stack

LayerChoiceWhy
FrameworkAstro (SSR)Zero client JS by default; the private/public split is a server concern
RuntimeCloudflare WorkersThe middleware runs at the edge, before any HTML exists
AuthPBKDF2 + HMAC cookieNo database; secrets live in wrangler secret
ContentMarkdown + content collectionsNotes are files in the repo, versioned with the code
StylesTailwindHand-written prose styles on top, for the tables

Auth, in four steps

  1. /login posts a password to a server endpoint.
  2. 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.
  3. On success it sets an HttpOnly; Secure; SameSite=Strict cookie containing a payload signed with SESSION_SECRET. The expiry is inside the signature, so it cannot be extended by the holder.
  4. 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.