■ space · field notes · fig 1

■ the answer first

The whole newsletter is 1,258 lines: double opt-in, one-click unsubscribe, a send CLI — no platform account.

One-click unsubscribe is one header — RFC 8058, required by Google and Yahoo since February 2024.

Honest ceiling: without aligned DKIM, mail-tester ~5/10 and strict receivers may junk — fine for a small list; a relay fixes it.

1,258lines — all of them readable
2tables — subscribers + sends
1header does the unsubscribing

A newsletter is three endpoints, two tables, and one email header most platforms hide from you.

01WHY SELF-HOST AT ALL?

the honest inventory — the trade in one screen; the file wins ownership, the platform wins not-having-to-think

This serverHosted platform
Lines of code I can read1,258, all of them0
List ownershipone SQLite fileexport button
UnsubscribeRFC 8058 header, self-signeddashboard setting
Deliverability ceiling~5/10 mail-tester until DKIM alignstheir reputation
Time to first senda weekendan evening
the platform wins—deliverability reputation, and not-having-to-think
the file wins—a list auditable in an editor, one rsync from gone
the requirement—in February 2024, Google and Yahoo made one-click unsubscribe a bulk-sender rule — RFC 8058

02THE SUBSCRIBE PATH

one POST, three guards — the signature is the only authority, so the door opens with the ledger asleep

FIG 1 · THE SLEEPER DOORkeys 1 · 2 · 3 — or the buttons
THE SLEEPER DOORHMAC-SHA256 · RFC 8058 · NO SESSIONLEDGER · ASLEEPSUBSCRIBERS · SENDSOFFNO WIREMINTCONFIRMSEAL∞ NO-EXP

ROUTE 2 · CONFIRM — THE GAUGE READS THE SEAL · LEDGER ASLEEP · VERDICT STILL CONFIRMS

the tag carries base64url(email|action) plus an HMAC-SHA256 seal — no session, no token table, nothing to look up

the endpoint — one POST
curl -X POST https://mrsaynothing.dev/letters/api/subscribe \
  -H 'content-type: application/json' \
  -d '{"email":"[email protected]"}'
db.ts—two tables: subscribers, a CHECK pinning status to pending · confirmed · unsubscribed, and sends, the audit trail
the honeypot—a form field named website humans never see — a filled field buys a fake ok, nothing else
the bucket—10 requests per hour per IP · one resend per address every 10 minutes
enumeration—responses deliberately identical whether or not the address is already subscribed
the status lock—a CHECK constraint does one job — SET status='maybe' is refused

03THE TOKEN PRESS

your half computes anywhere — the box's half is walled; that wall is the security

the signed token — no session
import { createHmac, timingSafeEqual } from 'node:crypto';

// payload = base64url(email|action), MAC = HMAC-SHA256(payload)
export function sign(email: string, action: 'confirm' | 'unsub'): string {
  const payload = Buffer.from(`${email}|${action}`).toString('base64url');
  const mac = createHmac('sha256', config.hmacSecret).update(payload).digest('base64url');
  return `${payload}.${mac}`;
}
your half — computable herepayload = base64url(email|action) → cmVhZGVyQGV4YW1wbGUuY29tfGNvbmZpcm0
the box's half — walledMAC = HMAC-SHA256(payload, secret) · ×43 chars — computed on the serving box
the token on the linktoken = cmVhZGVyQGV4YW1wbGUuY29tfGNvbmZpcm0.mac
verification—timingSafeEqual against a recomputed MAC — no token table, no expiry to manage
the key—lives outside the repo, mounted read-only at runtime — the press computes your half and refuses to fake the other
unsub tokens—never expire, on purpose — consent mechanics must keep working forever; revoking means rotating the key
the gmail quirk—one-click unsub fires a cross-origin POST — that route trusts the MAC; the origin check stands down

A confirm link that still works when the database is asleep is one less thing that can rot.

04WHAT BROKE, WHAT IT WON'T DO

three faults, fixed — and the ceiling, printed by the setup itself

ERR_PNPM_IGNORED_BUILDSpnpm install --frozen-lockfile ran clean locally and died in the container — esbuild's postinstall script was never approved. The fix: ship the small pnpm-workspace.yaml declaring allowed build scripts into the image. Half a day to find, one file to fix.
CSRF blocked GmailThe one-click POST from Gmail's servers has no same-origin credentials, obviously. The route now trusts the token — and the token is unforgeable.
the deliverability ceilingWithout aligned DKIM, mail-tester scores the setup around 5/10, and strict receivers may junk the mail. That is the honest cost of skipping a paid relay — the fix is known and waiting if the list ever justifies it.
tracking—no open tracking — a principle: tracking pixels are the reason people distrust newsletters
scope—one list, no segments, no scheduling beyond a CLI invoked by hand or timer
backups—copy the file, restore the file — WAL mode keeps the copy consistent while the server runs
the rest—watching it start under systemd-class supervision is the remaining ops burden
sending — one command
# the --dry flag renders the full text and HTML for review before anything moves
cli.mjs send --subject ... --file post.md
■ honest stamp Tested on: node:sqlite (WAL) · HMAC-SHA256 tokens · RFC 8058 headers · plain SMTP. First published 2026-09-26. The line count, the rate limits and the mail-tester score are the server's own numbers; the RFC is quoted from the IETF record and the Gmail/Yahoo requirement from their sender guidelines. Note from the migration: the platform this piece ran on has since been retired — the piece stands as the record of what was built and what it taught.

end of the one-file newsletter · nothing fetched · everything local