- CSS 53%
- JavaScript 40%
- HTML 5%
- Nix 2%
Keyhive: End-to-end Encrypted Applications #
J On The Beach 2026 · Málaga · 11:00–11:40 · intermediate
Every app that stores user data is a breach waiting to happen — and a subpoena, an outage, and a migration headache rolled in. […] Keyhive puts users back in charge and takes you out of the line of fire. It uses local-first auth and end-to-end encryption on top of the well known Automerge CRDT — you can think of it like "Signal for documents." […] Working code in Rust and TypeScript.
Forty-one slides, ~38 minutes. Every slide has speaker notes (press S).
Run it #
npm install # reveal.js + anime.js
npm run vendor # copy them into vendor/
npm start # http://localhost:8000
Or with Nix:
nix develop # node, wrangler, language servers
nix run # build the deck in the store and serve it
nix build # just the static site, in ./result
Note
Slides are fetched over HTTP, so opening index.html from the filesystem shows a blank deck. Use the server.
Note
Nix only sees git-tracked files. Until new files are committed, use nix build path:. / nix run path:.
Deploy #
npm run deploy # vendor + assemble _site/, then wrangler deploy
Cloudflare Pages via GitHub: set the build command to npm run build and the output directory to _site.
Tangled sites #
Tangled serves a branch as-is, with no build step, so the built deck lives on its own pages branch:
npm ci # once
npm run publish:pages # build, commit _site/ to `pages` (one signed commit)
git push tangled pages # or: npm run publish:pages -- --push
npm run publish:pages -- --dry-run # build and stage only, no commit
The script uses a throwaway index, so your working tree and staging area are untouched, and it skips the commit if nothing changed. In the repo's Settings → Sites, deploy from branch pages with directory /. All paths are relative, so the deck works as a sub-path site (<you>.tngl.sh/<repo>).
Keys #
| Key | Does |
|---|---|
→ / space |
Next build step / slide |
s |
Speaker view |
f |
Fullscreen |
o |
Overview |
ctrl/cmd + shift + f |
Search |
Structure #
index.html reveal config + slide manifest (order lives here)
css/deck.css hand-written theme, no SCSS build
css/widgets.css interactive widget styles
js/deck.js ambient motion: the hive, entrances, bouncing bees
js/widgets.js interactive slides (data-widget="…")
slides/NN-part/*.md one file per slide
build.mjs assemble _site/
serve.mjs zero-dependency static server (with range requests)
vendor.mjs copy reveal/anime out of node_modules into vendor/
publish-pages.mjs commit the built _site/ to the `pages` branch (Tangled)
| Part | Slides | Beat |
|---|---|---|
| Open | 7 | Title, about, breach, the custodian problem, since 1961, Signal for documents, route |
| I. No Guards? | 6 | CRDT live, the server was the bouncer, auth moves down the stack, b5 on recomposition, requirements |
| II. Direct Access | 7 | Delegation tree, groups, access levels, TS code, concurrent revocation, whiteout |
| III. Sealed Cells | 5 | Docs ≠ chat, causal encryption, FS vs PCS, TS code |
| IV. The Swarm | 6 | One-key rotation, key trees, MLS needs a referee, BeeKEM live, the paper |
| V. Dumb Pipes | 4 | What a relay sees, Sedimentree, the custodian cards flipped (recomposition payoff) |
| VI. Ship It | 6 | Rust code, costs and limits, status, closing line, thanks |
Interactive slides #
Two slides have live widgets. Each mounts when its slide appears and resets when you leave, so every run-through starts clean. Buttons drop focus after a click, so the clicker still advances the deck.
| Slide | Widget | On stage |
|---|---|---|
| No server, same answer | crdt |
Flip the server off, edit both replicas, then 👟 sneakernet the changes across (or flip it back on) |
| BeeKEM: keep both | beekem |
Click a member to rotate their key; go offline and rotate two to make a conflict cell; remove Dan to blank his path |
Adding one: Widgets.register("name", (root, scope) => { … }) in js/widgets.js, then <div class="widget" data-widget="name"></div> in a slide. Use scope.timeout / scope.interval / scope.loop so timers die with the slide.
Builds and animation #
- Presenter-paced steps are reveal
fragments. HTML fragments rise; SVG fragments only fade (a CSS transform would clobber theirtransformattribute). class="draw"on an SVG stroke draws it in when its fragment appears (--lensets the dash length).- The hive (bees wandering a honeycomb, cells filling and capping) runs on the title and section slides, or anywhere with a
data-hiveattribute. <span class="fragment zoom" data-zoom="selector">grows that element to fill the slide below the heading on its step, and shrinks it back on the way out.
Theme #
Ink on wax paper over a faint honeycomb, with a propolis → honey gradient for headings. Honey is for fills and accents, never body text (gold on cream dies on a projector). Teal means "verified / converges"; sting red means "revoked / breached"; slate means "the server". Hex cells are the visual unit: honey-filled = written, wax-capped = sealed. Fonts are vendored, so the deck works with no network.
The .bg-grid honeycomb (CSS) and the hive canvas (js/deck.js) share geometry: pointy-top hexes, circumradius 40px, same origin. Change one, change both.
Gotchas #
- Keep inline
<svg>free of blank lines. Markdown treats a blank line as a block break and strands child nodes outside the SVG. - All slides share one DOM, so SVG
ids (markers,<defs>hexes) must be unique across the deck. Prefix them with the slide name.