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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179 | # home
The home of [ardegazu.ro](https://ardegazu.ro) — a small constellation of
serverless p2p apps. The page is the suite's front door: a hash-routed
multi-view PWA (`#/apps` · `#/about` · `#/you` · `#/boards` · `#/bots`) whose
app catalog renders from `catalog.json` at runtime, plus the **identity card**,
where the one identity every app recognizes is created, named, exported and
imported. It also serves the **identity bridge** at `/id/` — the tiny page
every `*.ardegazu.ro` app embeds to pick that identity up automatically.
Still no origin server: a static ClojureScript build on IPFS.
Public mirror (git dumb-HTTP — no git server):
```sh
git clone https://git.ardegazu.ro/home.git
```
## Layout
```
client/public/index.html the hub shell: nav tabs + the view sections
client/public/id/index.html the identity bridge page (embedded by every app)
client/public/style.css hub-specific styles (shared layer comes from the theme)
client/catalog.json the app catalog (schema v1) — cards + the resident bots
client/version.json the live version number (drives the update banner)
client/deps.edn exact clojurescript + shadow-cljs pins, and the suite kits' src on the classpath
client/shadow-cljs.edn the three builds: :app, :bridge, :testlib
client/src/home/main.cljs boot + identity card logic (reads apex storage directly)
client/src/home/router.cljs hash router — only `#/…` hashes; `#add=` links pass through
client/src/home/catalog.cljs fetch + validate catalog.json, render the cards
client/src/home/glyphs.cljs the hand-drawn isometric SVG glyphs, keyed by `glyph`
client/src/home/bots.cljs the bots view — the resident players, one-tap befriending
client/src/home/social.cljs friends/presence/invites — the only module touching libp2p
client/src/home/lb_ui.cljs the leaderboards (co-signed receipts, verified here)
client/src/home/social_share.cljs hands the social module's friend surface to the bots one
client/src/home/update.cljs version polling + the one-tap update banner
client/src/home/bridge.cljs bridge host boot (its own :bridge build — see below)
client/scripts/build.mjs the release build + every shipping gate
client/scripts/defines.mjs the two closure-defines (app version, catalog snapshot)
client/scripts/bump-version.mjs bumps version.json (`npm run release`)
client/test/vectors/ golden fixtures extracted from the TS while it was canon
client/test/dom-stub.mjs the small DOM the fixtures and the tests share
site/index.html landing page for this repo on git.ardegazu.ro/home/
deploy/publish-repo.sh builds the anonymous dumb-HTTP mirror for git.ardegazu.ro
```
The identity itself (Ed25519 seed, profile, bridge protocol) lives in the
shared [`ardegazu-id-kit`](https://git.ardegazu.ro/id-kit/) package, and the
friends/presence/leaderboard layer in
[`ardegazu-social-kit`](https://git.ardegazu.ro/social-kit/). Both are
commit-pinned npm git dependencies, and both are read as ClojureScript SOURCE
off `client/deps.edn`'s classpath rather than as a compiled dist: one
`cljs.core`, one copy per build, real dead-code elimination. Foreign packages
(`@noble/*`, the libp2p family) stay string requires. The shared look comes from
[`ardegazu-theme`](https://git.ardegazu.ro/theme/). shadow-cljs does not bundle
CSS, so `scripts/build.mjs` copies `theme.css` verbatim out of the pinned dep
into `dist/` and `index.html` links it BEFORE `style.css` — the same cascade
order main.ts's two CSS imports used to give it.
The service worker precaches the shell only — everything under `/id/` is
strictly network-only, excluded three redundant ways in `scripts/build.mjs`
(`globIgnores`, a `NetworkOnly` runtime route that must stay FIRST, and
`navigateFallbackDenylist`); any workbox change must preserve all three, and
the build **gates on all three against the emitted `sw.js`** rather than
trusting a comment. A stale-cached `/id/` would freeze the suite's identity
protocol.
That is also why `/id/` is its own shadow build (`:bridge`, output
`dist/id/assets/`) instead of a module of `:app`: CLJS module filenames are
unhashed, so a base shared with the hub would sit at a *stable* url under
`dist/assets/` — precached, outside all three exclusions, and served stale to
a bridge page whose own code came fresh off the network. Vite only escaped the
same shape because it content-hashes. Nothing `/id/` loads may live outside
`dist/id/`; the build gates on that too.
`version.json` and `catalog.json` are never precached (globPatterns omit
`.json`): the catalog can change without a shell redeploy, and a version bump
surfaces the update banner. Activation is prompt-mode — `skipWaiting: false`
plus `clientsClaim: true`, so the page reloads on your tap, never under you.
## Adding an app to the catalog
One entry in `client/catalog.json`:
```jsonc
{ "id": "game7", "name": "…", "host": "game7.ardegazu.ro", "kind": "game",
"tagline": "…", "description": "…", "tags": ["…"],
"accent": "rose|peach|sage|lilac|sky|gold", "glyph": "game7",
"status": "new|beta|stable",
"lb": { "g": "game7.ardegazu.ro/v2", "maxScore": 10000 } // leaderboard games only
}
```
The hub renders the card, the app/game sections and the boards picker from
it at runtime (a bundled snapshot covers the fetch failing). Bespoke glyph
art goes in `client/src/home/glyphs.cljs` under the `glyph` key — unknown keys fall
back to a plain accent diamond; a `glyphSvg` field can inline one instead.
Then `ardz release home` — the release bumps `client/version.json` (commit
the bump), rebuilds and deploys, and installed hubs offer the update.
## Build & deploy
Needs a JVM (>= 17) and the `clojure` CLI on the dev machine, plus node >= 22.
The VPS never builds.
```sh
cd client && npm install && npm run build # cold shadow-cljs release → client/dist
cd client && npm test # the golden-vector suite + the source lint
cd client && npm run release # bump version.json + build (what ardz release runs)
cd client && npm run dev # shadow watch on :dev-http 5173
```
`npm run build` is cold-cache by design (a warm `.shadow-cljs` assigns Closure
property renames from a different pool) and ends in the shipping gates:
relative asset refs in both pages, no local paths in `dist`, the social-kit
lazy boundary, `/id/` self-containment, the rule-4 `sw.js` assertions, and a
minified `dist/catalog.json`. Two cold builds are byte-identical.
Dev ports are **4173/5173 only** — the managed relay's origin allowlist covers
nothing else. Note the inherited wart: while `npm run dev` runs, 4173 serves
the watcher's *dev* build out of `dist/assets`, not a release.
Deploy `client/dist` anywhere that serves static files (the live site pins it
to IPFS and points the `ardegazu.ro` DNSLink at it). The hub is versioned:
each release bumps `client/version.json` so installed PWAs see the update
banner — commit the bump afterwards.
## Known limits
Honest ones, worth knowing before you rely on them.
**Clearing an app's profile section is not durable on a multi-device identity.**
Each app owns one section of the suite record (`apps.<appKey>`), written through
the `/id/` bridge and carried between your devices by the hub's self-sync. The
merge rule reads a section only one side has as *adoption* — "an app the other
device has never opened is not a deletion" — and the format has no tombstones,
so a section you clear on one device comes straight back on the next sync from
any device that still holds it. Clearing is local and best-effort until the
format grows tombstones (a change to two released packages and every app that
reads the map, so: its own increment, not a patch).
**The sections have a collective budget, and a merge can hit it.** The record
caps at 128 KiB with 32 KiB reserved for the profile and the friends list,
leaving 96 KiB for all app sections together (each capped at 16 KiB). Two
devices can each be legally full and still not fit merged. When that happens the
hub keeps *this* device's sections untouched and refuses the incoming ones that
do not fit, biggest first, with a console warning naming them — it never trades
your own data for a peer's. The same budget is enforced on *every* write the hub
makes, not just on the merge: a friends list that would push the record past the
cap is refused too (with a warning), because storing it would make the bridge
host refuse every app's section write from then on. A device that a pre-fix
build already pushed past the cap repairs itself as folds arrive: a merge that
makes the map *smaller* is stored even when it still does not fit.
**A backwards clock jump can make a device chatty.** Section timestamps are
clamped to "now" on the way out but stored unclamped, so a clock moved
backwards leaves entries stamped in the future; they still win every fold (the
data is correct), but the sync fingerprint can differ each round and cost an
extra mailbox deposit per cycle until the clock catches up.
## The ecosystem
The living list is `client/catalog.json` (it's what the hub itself renders);
this table is a convenience snapshot.
| app | live | source |
|---|---|---|
| sueta — p2p e2e chat + calls | [chat.ardegazu.ro](https://chat.ardegazu.ro) | `git.ardegazu.ro/chat.git` |
| board — p2p e2e infinite whiteboard | [board.ardegazu.ro](https://board.ardegazu.ro) | `git.ardegazu.ro/board.git` |
| neon-grid — light-cycle arena | [game1.ardegazu.ro](https://game1.ardegazu.ro) | `git.ardegazu.ro/game1.git` |
| valley-blocks — monument-valley tetris | [game2.ardegazu.ro](https://game2.ardegazu.ro) | `git.ardegazu.ro/game2.git` |
| tessera — mosaic race | [game3.ardegazu.ro](https://game3.ardegazu.ro) | `git.ardegazu.ro/game3.git` |
| séance — haunted-house party game | [game4.ardegazu.ro](https://game4.ardegazu.ro) | `git.ardegazu.ro/game4.git` |
| lampion — night climb on turning rings | [game5.ardegazu.ro](https://game5.ardegazu.ro) | `git.ardegazu.ro/game5.git` |
| odeon — p2p concert hall band jam | [game6.ardegazu.ro](https://game6.ardegazu.ro) | `git.ardegazu.ro/game6.git` |
| theme — the shared look, one CSS file | [cdn.ardegazu.ro/theme/v1/theme.css](https://cdn.ardegazu.ro/theme/v1/theme.css) | `git.ardegazu.ro/theme.git` |
| id-kit — the shared identity package | — | `git.ardegazu.ro/id-kit.git` |
| social-kit — friends, presence, invites, leaderboards | — | `git.ardegazu.ro/social-kit.git` |
| dev — the dev-management suite (`ardz`) | — | `git.ardegazu.ro/dev.git` |
|