This repository has no description

README.md

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 their transform attribute).
  • class="draw" on an SVG stroke draws it in when its fragment appears (--len sets 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-hive attribute.
  • <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.