Prism

The zero-dependency backend

A complete blog server — routing, SQLite, sessions, full-text search, RSS — with an empty package.json.

nodebackendsqlite

npm ls on this server prints nothing. Not "a few carefully chosen packages" — nothing. The whole backend is the Node.js standard library, and the surprise of 2026 is how little that costs you now.

The standard library grew up

The classic argument for Express was never routing — it was everything else you'd have to hand-roll. That list has quietly evaporated:

You used to install…Node now ships
express routinghttp + the WHATWG URL parser
better-sqlite3node:sqlite with FTS5 compiled in
bcryptcrypto.scryptSync
compressionzlib brotli/gzip
nodemonnode --watch
jestnode --test

The router that dispatches every request on this site is genuinely this:

js
const url = new URL(req.url, origin(req));
if (await api.handle(req, res, url)) return;       // /api/*
if (serveStatic(req, res, url.pathname)) return;   // public/*
const page = ssr.renderPage(url, nonce);           // /, /posts, /post/:slug…
sendHtml(req, res, page ? 200 : 404, page ?? render404(url, nonce));

Four lines of policy. When something misbehaves at 1 a.m., the entire call stack is code with my name on it.

SQLite is the perfect blog database

A personal blog is thousands of reads, a handful of writes, one writer. That workload is SQLite's home turf, and node:sqlite means no native compilation, no version drift:

sql
CREATE VIRTUAL TABLE posts_fts USING fts5(
  title, tags, body, content='', tokenize='porter unicode61'
);

That one statement is the entire search backend. The ⌘K palette on this site queries it with a prefix MATCH and gets ranked, snippeted results in under a millisecond — no Elasticsearch, no embeddings, no queue. Porter stemming even forgives my typos-by-inflection: searching shaders finds shader.

Triggers keep the index in sync with writes, so the application code cannot forget:

sql
CREATE TRIGGER posts_ai AFTER INSERT ON posts BEGIN
  INSERT INTO posts_fts (rowid, title, tags, body)
  VALUES (new.id, new.title, new.tags_json, new.content_md);
END;

Sessions without a session library

Admin auth is one password, scrypt-hashed, compared with crypto.timingSafeEqual. A successful login inserts a random 256-bit token into a sessions table and sets it as an HttpOnly; SameSite=Strict cookie. CSRF is handled by requiring a custom X-Prism header on every mutation — cross-origin code can't attach one without a CORS preflight this server never grants.

What it buys you

  • Cold start in the low tens of milliseconds — the deploy story is git pull && systemctl restart blog.
  • No supply chain. Every npm audit horror story is somebody else's changelog now.
  • Archaeology-proof. No lockfile drift, no peer-dependency puzzles in 2031. Node 22 LTS is the only contract.

The honest cost: I wrote a static-file server with ETags and pre-compressed responses (an afternoon), body parsing with size limits (an hour), and a login rate-limiter (thirty minutes). Call it two days of work, once, for a backend I will never again have to update — only change. On a personal site, that trade isn't close.