1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112 | # board
Multi-user **p2p, end-to-end encrypted** infinite whiteboard PWA at
**https://board.ardegazu.ro** — no origin server, no accounts, no server-side
storage the operator can read.
Public mirror (served from IPFS via git's dumb-HTTP protocol — no git server):
```sh
git clone https://git.ardegazu.ro/board.git
```
MIT licensed. Built on the sueta p2p stack
(`git clone https://git.ardegazu.ro/chat.git`) — the stack's durable core is
`ardegazu-rooms-kit`, sha-pinned and compiled off the classpath (`deps.edn`);
`client/src/board/lib/` holds only board's own layer, chiefly the allow-list
access layer (`wrapbox.cljs`, `policy.cljs`, `epoch.cljs`).
```
the board an infinite pan/zoom canvas, white or black; pen (Apple Pencil
pressure), text, sticky notes, rect/ellipse/line/arrow,
connectors that follow the shapes they attach to, images from
the clipboard, an eraser, PNG export
the data every change is one op in an end-to-end-encrypted append-only
log (OrbitDB), replicated member⇄member and folded identically
on every device; live strokes/cursors ride sealed WebRTC
broadcasts and never touch the log
offline changes made alone reach late joiners through an encrypted
store-and-forward mailbox the node cannot read (sueta v16);
a peer who was never online with the author still converges
boards capability links — the secret lives in the URL fragment and
never leaves the browser; your board list lives on your device
and syncs only between devices holding the same identity
private boards the creator can flip a board to invite-only: content re-homes
onto a hidden second log whose keys are granted per-identity
(X25519 sealed boxes); URL holders can only knock — name,
message, key fingerprint — and any approved member can let
them in; removing someone re-keys everything after
identities persistent Ed25519 seeds with emoji fingerprints signing every
op; TOFU with key-change warnings
```
## The access model, honestly
- **Link mode (default):** the link IS the key. Anyone holding it reads and
draws. Sharing the link is sharing the board.
- **Invite-only mode:** the URL still finds the board, but content moves to a
vault log rooted in a secret distributed only inside per-identity sealed
grants. A URL holder cannot compute the vault's address, its mailbox room,
or any content key. What they DO see: history from before the switch (they
could always have read it), and the knocking ritual — who requests and who
approves. **Removal is not retraction**: an ex-member keeps everything they
were ever granted, and can watch (but not read) the vault's traffic shape;
they can never open content sealed after their removal.
- The board link of a strict-capable board is `#<secret>.<creatorPub>` — the
creator's key rides in the link itself, so policy authority never depends on
log ordering. Only the creator flips modes and removes members; approvals
are open to every member (that's the point of a whiteboard).
## Layout
| Path | What |
|---|---|
| `client/` | ClojureScript PWA (shadow-cljs); `src/board/lib/` is board's own layer (the access layer + mailbox/protocol/selftest — the durable core comes from `ardegazu-rooms-kit` on the classpath), `src/board/app/` is the whiteboard |
| `client/test/` | golden vectors extracted from the pre-port TypeScript + the black-box suite that replays them (`npm test`) |
| `client/e2e/` | Playwright suites: mesh convergence, offline mailbox, access layer — each spawns its own relay |
| `deploy/relay/` | ~130-line libp2p dev relay (websocket + circuit-relay-v2 + gossipsub discovery) |
| `deploy/publish-repo.sh` | anonymous source-mirror builder (publish.git toolkit) |
| `site/` | landing page for the git mirror |
## Dev
Needs node >= 22, a JVM >= 17 and the `clojure` CLI (the VPS never builds).
```sh
# terminal 1 — dev relay on :9090 (deterministic PeerId, baked into the client's dev default)
cd deploy/relay && npm install && npm run dev
# terminal 2 — app on :5173 (shadow-cljs watch + :dev-http; the relay's
# origin allowlist covers 4173/5173 ONLY)
cd client && npm install && npm run dev
# unit + golden-vector suite (no ports, no browser)
cd client && npm test
# browser suites — each spawns its own relay and a static server over dist,
# so BUILD FIRST; stop the terminal-1 relay before running them
cd client && npm run build
cd client && node e2e/board.e2e.mjs # mesh convergence, LWW edits, connectors, tombstones, replay
cd client && node e2e/access.e2e.mjs # strict boards: stranger-blindness, knock/approve, removal re-key
# the mailbox suite needs the stub endpoints baked into the build — see its header
cd client && node e2e/mailbox.e2e.mjs
```
## Deploy
```sh
cd client && npm run release # bumps version.json + builds dist/
# then: ird ipfs add client/dist → ird ipfs ipns publish board.ardegazu.ro <cid>
```
The relay + TURN + mailbox are ird's managed p2p service (shared with the
sueta app; the distinct app salt keeps every derivation, room and mailbox id
disjoint). The client re-reads the host's `/.well-known/ap2p` descriptor at
boot, so infrastructure keys can rotate without a client release.
The source mirror republishes via `deploy/publish-repo.sh` (anonymity gate:
single anonymous author, zero identity bytes in any git object) and the
ecosystem's assembler, which re-points `git.ardegazu.ro`.
Boards are a full mesh, comfortable up to ~12 peers. Bigger would need
different architecture, which would no longer be this project.
|