Navigate
Start a conversation

Nairobi, Kenya · Available for thoughtful work.

The browser should not hold the keys

How this site works: one Rust binary serving the pages, argon2 sessions in httpOnly cookies, double-submit CSRF, and markdown sanitised before it is stored.

Every form on a public site is an invitation. This one accepts messages, so I built it the way I would build a client project: everything that matters happens on the server.

One process, no public API

The site, the admin dashboard, and the data all live in a single Rust binary. Pages are rendered as HTML on the server, so there is no client-side data fetching to secure and no API surface exposed to the internet.

browser ──(same-origin, httpOnly cookie)──► Rust server ──► SQLite

There is no CORS configuration to get wrong because there is nothing cross-origin to talk to.

Sessions JavaScript cannot read

Sign-in produces a random token stored in an httpOnly, SameSite=Strict cookie. The client bundle never sees it. Mutations also need a CSRF value echoed from a second cookie, checked in constant time against the session on the server.

// Double submit, compared without an early exit.
let ok = bool::from(session.csrf.as_bytes().ct_eq(supplied.as_bytes()));

Passwords are hashed with argon2 on first boot and the plaintext is never stored. Login attempts are rate limited more tightly than the contact form, because guessing a password is a more expensive failure than one extra message.

Rendering untrusted input

Post bodies are markdown written by an authenticated owner, but they still go through pulldown-cmark and then ammonia. Nothing to gain from trusting your own database, everything to lose if an import path ever changes.

  • Raw HTML in markdown is stripped.
  • Script URLs never survive sanitisation.
  • Every rendered page carries a restrictive content security policy.

What this buys

  • No keys in the browser.
  • No public API to probe.
  • Injected content cannot execute scripts.
  • One deployable file: copy it next to a SQLite database and run it.

The pattern is not novel; it is just rarely finished. Most tutorials stop at “and then we fetch the data”. The work is in the parts that fail quietly — constant-time compares, sanitised output, and cookies the client cannot touch.

← All writing Start a conversation