■ space · field notes · fig 1
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.
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 server | Hosted platform | |
|---|---|---|
| Lines of code I can read | 1,258, all of them | 0 |
| List ownership | one SQLite file | export button |
| Unsubscribe | RFC 8058 header, self-signed | dashboard setting |
| Deliverability ceiling | ~5/10 mail-tester until DKIM aligns | their reputation |
| Time to first send | a weekend | an evening |
rsync from gone02THE SUBSCRIBE PATH
one POST, three guards — the signature is the only authority, so the door opens with the ledger asleep
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
curl -X POST https://mrsaynothing.dev/letters/api/subscribe \
-H 'content-type: application/json' \
-d '{"email":"[email protected]"}'
subscribers, a CHECK pinning status to pending · confirmed · unsubscribed, and sends, the audit trailwebsite humans never see — a filled field buys a fake ok, nothing elseSET status='maybe' is refused03THE TOKEN PRESS
your half computes anywhere — the box's half is walled; that wall is the security
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}`;
}
timingSafeEqual against a recomputed MAC — no token table, no expiry to manageA 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
pnpm 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.# the --dry flag renders the full text and HTML for review before anything moves
cli.mjs send --subject ... --file post.md
end of the one-file newsletter · nothing fetched · everything local