Files
README.md

Let It Disconnect: A Local-First Future #

Closing keynote · CodeBEAM Europe 2026 · Track 1, Kleine Zaal · Thu 16:45–17:30

“Let it crash” was never really about crashing — it was about designing systems that assume failure and keep working anyway. Local-first software takes the same view about the network, service providers, and open source: assume disconnection, variable latency, service EOLs, and forks to design for convergence and user agency rather than coordination and lock-in.

Thirty-nine slides, ~45 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: peer mesh, entrances, logo bounce
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 3 Title, about, Lamport epigraph
I. Let it crash 5 Telecom origins, four ideas, Joe's thesis
II. Lost in the cloud(s) 10 Cloud promise, AWS map, the long way to the next seat, carrier vs bike, eras
III. Let it disconnect 9 The four "assume"s, CRDT switch, sneakernet-complete, kill the server, same loop
IV. Let it delegate 6 Holding a reference is permission, authority you can see, auth in the wrong layer, interop n² vs n, b5
V. Software in 2030 6 Automerge/Keyhive, BEAM → LF map (summary), closing, Lamport callback

Interactive slides #

Three slides have live widgets rather than builds. 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
Never about crashing supervisor Runs itself (data-auto): workers crash at random, supervisors restart them, calls keep climbing
Who's in charge here? topology Cloud vs local-first; kill the server; quilts show who is in sync
Choose which versions…? crdt Flip the network off, edit both replicas, flip it back on

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).
  • .swap stacks children in one cell, so a later fragment cross-fades over an earlier image.
  • The peer mesh runs on the title and section slides, or anywhere with a data-mesh 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 warm paper, with an Elixir-purple → Gleam-pink gradient for headings. Erlang red is kept for one meaning: the central server. Teal means "converges"; amber means "breaks". Fonts are vendored, so nothing loads from a CDN and the deck works with no network (which would be embarrassing to get wrong in this talk).

Gotchas #

  • Keep inline <svg> free of blank lines. Markdown treats a blank line as a block break and strands child nodes outside the SVG.
  • World map: do not edit dot positions or ellipse geometry. They were placed by hand.