Skip to content

Release 0.202

v0.202.1 — 2026-07-25

Release-engineering fix — use this tag, not v0.202.0. The split’s release matrix (its first real run) collapsed to arm64-only: platform lived only in the matrix include, whose entries merge into existing combinations with later includes overwriting values earlier ones added — so both targets built arm64 twice, and mantle-{server,client}:v0.202.0 + :latest published as arm64-only manifests unusable on amd64 boxes. (The server image’s node:sqlite probe caught it; the client had no such gate and published.) No code changes vs v0.202.0.

  • platform is now an original matrix dimension (a true 4-job cross product); the include entries only attach the runner.
  • Two new tripwires in the merge job: exactly one digest per architecture BEFORE anything is tagged, and the pushed manifest must list both platforms — for both targets, closing the client’s gateless publish.

v0.202.0 — 2026-07-25

Do not deploy this tag — its published images are arm64-only (release matrix bug, fixed in v0.202.1). Everything below shipped correctly in v0.202.1.

The server tier runs Hono now — Next.js is removed from server/web. After the member carve (v0.201.0), server/web was an API-first tier: the whole /api/** plane plus a handful of render surfaces, with almost no React left. Carrying the full Next.js runtime — App Router, RSC, the Edge middleware sandbox, next build — to serve JSON and two static pages was pure weight. So server/web now runs a Hono app under @hono/node-server, executed by tsx — the same runtime server/api and the workers have always used. Boot is a sub-second tsx server/main.ts; there is no compile step. client/web stays a Next.js app, untouched.

  • The gate is a faithful port of the Edge middleware. Session-HMAC verify, the k:'m' mobile bearer, ?at= asset tokens, PUBLIC_PATHS, and CORS (including the wildcard refusal on the credential-minting /api/auth/** paths, preflight-before-auth) all moved to server/middleware/gate.ts. Request path/method now travel via AsyncLocalStorage instead of injected x-mantle-* headers.
  • Route files kept their shape. A local NextResponse/cookies()/ headers() compat shim (server/http-compat/) and a generated, precedence-sorted route manifest (288 app/**/route.ts handlers, lazily imported and adapted onto Hono) mean the ~280 route files carry the same handler convention behind the seam — a mechanical, reviewable diff, not a rewrite. Migrating individual routes to native Hono idioms is optional future cleanup.
  • Render surfaces are hand-rolled, no Next renderer. /s/<token> server-renders via react-dom/server with three client islands (app/table/token-prompt) bundled into public/share-runtime/ (Tailwind v4 CLI compile + esbuild + KaTeX); /print/pages/<id> is a plain HTML template around renderPageDoc; /login, /hub, /team/* are redirect stubs.
  • The HTTP contract did not change. Same routes and shapes, same port (3000), same /api/health, and no new env vars. e2e is green in both topologies (29 passed / 0 failed), with SSE client-abort, 8 MB multipart upload, and share-asset Range verified live under the node server.
  • The Docker server image drops the compile step: build is asset generation only (app-runtime, route manifest, share-runtime), and CMD is the exec form pnpm -C server/web exec tsx server/main.ts — exec, not the run-script form, so SIGTERM reaches the server instead of dying in the package-manager wrapper (docker stop settles in ~0.2 s rather than burning the full 10 s grace and taking a SIGKILL). The client target is unchanged.
  • pnpm dev:fe now runs client/web. The client app is zero-secret and natively detached, so the old bearer-minting machinery is gone — you sign in on the login page. Config moved to client/web/.env.detached.local (MANTLE_REMOTE=… only; the legacy server/web file auto-migrates). The remote box must allowlist your dev origin (http://localhost:3000) in MANTLE_API_CORS_ORIGINS — the wildcard never covers /api/auth. See docs/db-less-dev.md.
  • Scheduled backups work again on PostgreSQL 18 boxes. The image shipped the PostgreSQL 17 client, and pg_dump refuses to dump a server newer than itself — so from the moment a box moved to pg18, every scheduled backup failed silently. The image now ships the 18 client (a newer pg_dump handles older servers, so pg17 boxes are unaffected). After upgrading, run one manual backup (Settings → Backups) to confirm the pipeline is alive.
  • Migration guide: docs/upgrading-to-v0.202.md — the full path from the single-image era to the split (DNS, env additions, compose adoption, per-box smoke checklist, the pg17-era notes, rollback).
  • The runtime moves to Node 26 (26.5.0, V8 14.6) — base image node:26-slim, engines: node >=26, .nvmrc and CI matched. Node 26 is the current line, not yet LTS; it promotes around Oct 2026, so until then this pin rides ahead of LTS deliberately, for the V8 and stream performance work. Nothing in the application tree needed changing: the only native/wasm dependencies (@napi-rs/canvas, libsodium-wrappers) are N-API/wasm and survive the ABI 147 bump untouched, and the node:sqlite engine probes that back Tables v2 and the per-app broker — the exact things a runtime bump would break — pass unchanged.
  • The image’s base OS moves with it: Debian 12 (bookworm) → 13 (trixie), since that is what node:26-slim is built on. This broke the image build until fixed: the PostgreSQL apt repo line hardcoded bookworm-pgdg, which does not resolve on trixie, and apt-get install postgresql-client failed outright. The codename is now derived from the base image (. /etc/os-release → ${VERSION_CODENAME}-pgdg) so the next base bump can’t reintroduce it. Anything else that assumes bookworm package names in an image layer is worth a second look.
  • The brain’s appearance is server-rendered — one delivery path. The colour theme + the two display fonts (system-wide: they live on the anchor owner’s profile row, so one admin choice brands every surface and every browser) now render straight into the <html> tag as attributes + inline font vars, everywhere: the client app’s root layout fetches the new public GET /api/appearance server-to-server (30s cache, 2s timeout, fail-soft — a page never fails over branding) and the share/print surfaces read the DB directly. The old localStorage before-paint scripts are DELETED, not coordinated with: the document arrives correct, the client providers read the attributes back as initial state, and the theme-flash on a never-visited browser (which the client-origin split would have made universal) is gone. Semantics: share/print surfaces are the brain’s brand — the owner’s appearance is the only appearance, including branded PDF exports (still forced-white paper); a default choice is the absence of the attribute. The font picker also gains a home it never had: Settings → Appearance → Typography (it was previously mounted only on an unrouted demo page, so display fonts could not be set from the UI at all).
  • Footprint (measured back-to-back on one host, idle boot): server image 1.81 GB, down from 2.01 GB (the .next output is gone); settled RSS ~643 MB vs ~683 MB under next start; boot-to-ready ~3.2 s vs ~2.4 s — the +0.8 s is tsx transpiling TypeScript at startup (the same trade server/api and every worker already ship with), not request-path cost. The multi-minute next build disappears from the image build entirely.