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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310 | # PROMPT: build & publish a serverless p2p app on ardegazu.ro
You are an agentic developer with access to the **ird MCP tools** (IPFS pins,
IPNS names, gateway domains, DNS) and the workspace's `ardz` CLI. Build a
browser app exactly in the house style described below, then deploy it and
publish its repo. Everything you need is public; follow the flow in order.
Verify each stage before moving on.
This is the app-flavored sibling of `game1/docs/NEW-GAME-PROMPT.md` (games)
and `dev/templates/bot/docs/NEW-BOT-PROMPT.md` (bots). Games have their own
prompt; if the thing you're building is a game, use that one.
---
## 1. APP SPEC (filled in by the requester)
```
APP NAME: <e.g. "ledger">
SUBDOMAIN: <name>.ardegazu.ro (app at <name>., source at git.ardegazu.ro/<name>)
VARIANT: live | rooms (see the decision rule below)
WHAT IT DOES: <the app in two sentences>
DATA MODEL: <rooms: the LogOp union — what gets appended, who may append;
live: the wire-frame union — what peers broadcast while present>
UI/AESTHETIC: <palette, mood, layout>
EXTRAS: <anything else: media, exports, spectators, ...>
```
**Variant decision rule.** Ask one question: must state survive both tabs
closing, or reach members who were never online together? If yes → `rooms`
(durable encrypted log, mailbox offline delivery, multi-room lobby, versioned
PWA). If everything can evaporate when the last tab closes → `live` (the
games' stack minus the game: presence + sealed broadcast only, far less
machinery). When in doubt, start `live` — you can rebuild on `rooms` later;
the reverse never happens.
Everything below is fixed house style — do not ask the requester about it.
## 2. House architecture (non-negotiable)
Common to both variants:
- **Static client only. No servers, no accounts, no database.**
**ClojureScript on shadow-cljs** — both variants, no exceptions: the suite
publishes only Clojure/ClojureScript, and no TypeScript canon survives
anywhere to vendor from. `dev/docs/CLJS.md` is the build canon; read it
before touching `deps.edn`, `shadow-cljs.edn` or the vendored
`client/scripts/`. The production build is plain static files, served both
at the apex domain and at `/ipfs/<cid>/` paths — hence
`:asset-path "./assets"` and relative URLs in the hand-written
`public/index.html`, never absolute asset paths. The app name must be
`[a-z0-9]` only: it becomes the CLJS namespace root.
- **Relay & TURN are shared and already running** at `signal.ardegazu.ro`
(descriptor at `/.well-known/ap2p`, TURN creds at `/turn-credentials`,
compiled-in relay fallback in the config). **Do NOT create a new p2p app**
on the relay. The dev server MUST run on port 4173 or 5173 — that is what
`:dev-http` in `shadow-cljs.edn` binds, and the relay's origin allowlist has
no other entries. Browser verification serialises across the whole suite for
the same reason (CLJS.md).
- **Room = URL fragment.** A `#<base64url secret>` that never leaves the
browser; membership is proved cryptographically (sealed hello), not by
discovery. The invite link IS the room key.
- **`APP-SALT` namespaces everything** — salts, storage, protocol ids. It is
`<name>.ardegazu.ro/v1` (rooms) or `/v2` (live, matching the games' current
hello version). Never reuse another app's salt.
- **Suite identity (id-kit)** via the hidden iframe bridge at
`https://ardegazu.ro/id/`; **social layer (social-kit)** riding your
node's pubsub so friends see presence and get invites. Both are sha-pinned
git deps (`git+https://git.ardegazu.ro/<kit>.git#<sha>`) — bump ONLY with
the explicit `npm install <pkg>@git+…#<sha>` form (npm silently reuses
stale lockfile resolutions otherwise).
- **i18n en/ro/hu** — `i18n/runtime.cljs` is byte-identical across the CLJS
apps modulo the namespace rename (game1 canonical; the scaffolder vendors
it). English baked into `public/index.html` behind `data-i18n` where the
markup is static; all code-side text through `t`/`tn` (plurals NEVER via
string concatenation — Intl.PluralRules, so Romanian's few/other split and
Hungarian's uninflected numerals fall out of the catalogs). Canon:
`game1/docs/I18N.md`.
- **PWA + iOS home-screen quality bar**: `viewport-fit=cover` +
`env(safe-area-inset-*)` padding on fixed elements, `black-translucent`
status bar, no pinch/double-tap zoom or rubber-banding.
Variant **live** (game stack, generalized) — canon repo `game1` (neon-grid):
- `client/src/<name>/net/`, `id/boot.cljs` and `i18n/runtime.cljs` are
vendored from game1 at scaffold time and are **byte-identical to the canon
modulo the namespace rename** (`game1.` → `<name>.`, `src/game1/` →
`src/<name>/`) **except one line** — the protocol prefix in `net/peers.cljs`
(the scaffolder already set yours). Never edit anything else in there; fixes
happen in game1 and replicate. `client/scripts/` and the package files are
vendored the same way.
- Every frame E2E-encrypted by noise; define your wire protocol as `mk-*`
builders plus a guard in `src/<name>/game/frames.cljs` (quoted string keys —
never renamed by `:advanced`; build anything wider than eight fields with
sequential `unchecked-set`) and validate on receipt with the JS forms
(`js/Array.isArray`, `=== null`). Keep frames well under 64 KiB; design for a
full mesh (practical ceiling ~12 simultaneous peers).
- If your app needs an authority (one peer refereeing shared state), copy
neon-grid's proven host model (`game1/client/src/game1/game/game.cljs`: host
claim, lowest-id takeover, seat-indexed rounds, snapshots for late joiners)
rather than reinventing.
- Build: `:optimizations :advanced` + `:infer-externs :auto` (the surface is
small and typed enough to audit). PWA: workbox `generateSW` from
`scripts/build.mjs` with `skipWaiting: true` — no version.json, not
`versioned` in apps.tsv.
Variant **rooms** — the shared core is a KIT (`ardegazu-rooms-kit`); `chat`
(sueta) is the canon for the app-local remainder:
- **The durable core is not in your repo, and you do not vendor it.** Net, the
encrypted OrbitDB log, the hardened access controller, the OrbitDB identity
provider, the crypto/encryption layers, TURN, the descriptor fetch and the
`js` helpers (`truthy?`, `nn`, `ordered`) are `ardegazu-rooms-kit`'s own
ClojureScript sources, and your app **compiles them off its classpath**:
`client/package.json` sha-pins the kit, `client/deps.edn` puts
`node_modules/ardegazu-rooms-kit/src` on `:paths`, and you require them under
their real names — `ardegazu.rooms.js`, `ardegazu.rooms.lib.*`. No rename, no
copy, one place to fix. The npm sha pin stays the ONE cross-repo dependency
mechanism (`dev/docs/CLJS.md`); the classpath entry only points at what the
pin already installed. *Never* edit anything under `node_modules/` — fix it
in rooms-kit, `ardz kit-release rooms-kit`, then bump your pin.
- **What IS yours, in `client/src/<name>/`:** `lib/protocol.cljs` (your `LogOp`
union — the scaffold's demo rides chat's `{t:"chat"}` op, and replacing it is
your step 1); `lib/mailbox.cljs` and `lib/selftest.cljs` (scaffold-time copies
from chat, which the kit has no counterpart for — board's mailbox already
diverges, at a `canon.tsv`-tracked delta of 15); and `stores.cljs`, the
browser IndexedDB blockstore/datastore opener that `lib.log`'s `open-helia`
deliberately does not name, so the same kit file serves a tab and a Node bot.
`lib/media.cljs` — chat's call plumbing — is **not** scaffolded; copy it from
chat the day you add calls.
- **No protocol-prefix delta on this line.** The libp2p protocol ids
(`/sueta/2/msg/1.0`), the OrbitDB provider type `"sueta"` and the crypto
domain tags stay as deployed — that is what makes your app wire-compatible
with the rooms core — and **`APP-SALT` is what makes your rooms unreachable
from chat's**.
- **`onEntry` is wired BEFORE replication starts** in `room.cljs` — this
ordering is load-bearing (`rlog/open` starts sync and marks hashes seen, so a
handler attached afterwards loses those entries permanently). Keep the
scaffold's structure, including the `pending` buffer.
- Storage is namespaced through `NS`/`ns-key`/`ns-db` from `config.cljs` —
never raw localStorage keys or db names.
- Identity: rooms apps **never auto-adopt** on identity conflict (logs and
grants are bound to the old key), so `room.cljs` uses `IdBridge` directly and
deliberately does NOT vendor game1's `id/boot.cljs`. Keep that; if you
surface the conflict, make it an explicit two-tap choice (chat's identity
sheet is the worked example).
- Build: the scaffold ships `:optimizations :simple`, the safe default and
where board still is. chat has since moved this same stack to `:advanced`
(main.js 236 KB gz → 91 KB gz) on the observation that Closure never renames a
string-keyed property access, so the whole untyped-ESM surface is
renaming-immune; what breaks is our own prototype installs, and it breaks at
room boot, not at build time. If you follow chat, read its `shadow-cljs.edn`
header and `externs.js` first.
Three shadow modules keep the two dynamic boundaries real
(`:main` lobby / `:room` p2p stack / `:social` full kit) and
`scripts/build.mjs` gates all three, plus the `events` resolution, the
relative-URL rule and the SW semantics.
- **Versioning discipline**: the app is `versioned=y` in apps.tsv.
`ardz release <name>` runs `npm run release`, which bumps
`client/version.json` and builds; **commit the bump after every release**
or installed PWAs never see the update banner. Never flip the worker to
auto-update — the prompt+banner combination is the suite's update contract
(workbox `skipWaiting: false` + `clientsClaim: true`, and `globPatterns`
must omit `.json` so `version.json` is never precached).
## 3. Project layout
`ardz new-app <name> <host> <live|rooms>` scaffolds all of this, appends the
apps.tsv row, and sets the anonymous git identity. Files marked VENDORED are
copied from the canon repos at scaffold time — do not edit them, fix the canon
and replicate. Files marked KIT are not in your repo at all. (`live-cljs` and
`rooms-cljs` are still accepted as aliases; the `-cljs` suffix is redundant now
that both stacks are ClojureScript.)
```
<name>/
README.md LICENSE(MIT) .gitignore # node_modules/ client/dist/ deploy/.site/
# + .shadow-cljs/ .cpcache/ *.js.map (+ assistant dirs)
<agentcfg>/launch.json # dev server config, port 5173 (scaffolded to the live dotfile)
site/index.html # landing page for the published repo (§6)
deploy/publish-repo.sh # anonymity-gated mirror builder (new-style
# gate: reads ~/.config/cod-sursa/idpat)
client/
package.json package-lock.json # VENDORED (canon deps, renamed to yours)
deps.edn shadow-cljs.edn # YOURS (exact-pinned compiler pair; see CLJS.md)
# rooms: deps.edn's :paths also carries
# node_modules/ardegazu-rooms-kit/src
scripts/ # VENDORED (dev.mjs, build.mjs + the build gates)
test/source-hygiene.test.mjs # TEMPLATE'S OWN app-agnostic copy of chat's
# CLJS lint (chat's carries chat-only façade
# checks a fresh app cannot pass)
test/README.md .cljfmt.edn # what `npm test` runs, and what to add
public/index.html public/style.css # YOURS — hand-written shell, RELATIVE urls only
public/manifest.webmanifest
public/icons/ # VENDORED placeholder art — REPLACE IT
src/<name>/i18n/runtime.cljs # VENDORED (game1 canon, byte-identical)
src/<name>/i18n/{en,ro,hu}.cljs src/<name>/i18n.cljs # YOURS
src/<name>/config.cljs src/<name>/main.cljs # YOURS
# live variant:
src/<name>/net/ # VENDORED (game1; peers.cljs prefix pre-set)
src/<name>/id/boot.cljs # VENDORED (game1, verbatim)
src/<name>/social.cljs # YOURS — the lazy social-kit module
src/<name>/game/{app,frames,ui}.cljs # YOURS — the demo compiles; gut it
# rooms variant:
version.json # {"version":1} — the release bump target
ardegazu.rooms.js / .lib.* # KIT — ardegazu-rooms-kit's sources, compiled
# off the deps.edn classpath. Not in this repo.
src/<name>/lib/protocol.cljs # YOURS — the LogOp union, your only wire surface
src/<name>/lib/{mailbox,selftest}.cljs # VENDORED (chat; the kit has no counterpart)
src/<name>/stores.cljs # VENDORED (chat; the browser IDB store opener)
src/js/ # VENDORED (chat; the @libp2p/config shim)
src/<name>/room.cljs src/<name>/social.cljs # YOURS (scaffolded skeleton)
src/<name>/app/{rooms,store,ui}.cljs # YOURS — the demo compiles; gut it
```
## 4. Anonymity — already set up; verify BEFORE the first commit
The scaffolder ran `git init` and set the repo-local identity
`<name> <name@noreply.local>`. Verify it (`git config user.name`), and keep
every commit anonymous: no real names, emails, usernames, machine hostnames
or `$HOME` paths anywhere in tracked files. The publish gate (§6) decompresses
every mirror object and aborts on identity strings — it reads private
patterns from `~/.config/cod-sursa/idpat`. Prefer targeted `git add <files>`;
never `git add -A`.
## 5. Build, test, deploy
1. Develop with two browser tabs on the same `#room` URL: `ardz dev <sub>`
(port 5173; `<sub>` is the first label of your host — the apps.tsv key the
scaffolder printed). Rooms variant: also run `ardz relay` (:9090) for the
local relay + mailbox pair, and test offline delivery by closing one tab,
posting in the other, reopening.
2. `npm test` in `client/` — out of the scaffold this is `cljfmt check src`,
a compile of the `:app` build, plus `test/source-hygiene.test.mjs`, the only
gates a day-zero app can run honestly, and all of them pass on a fresh
scaffold (the demo UI builds DOM nodes via `app/dom.cljs`, so the lint's
raw-HTML allowlist ships empty). It deliberately does NOT run
a `:testlib` build, because you have no black-box tests yet;
`client/test/README.md` says what to add, in what order, and how to put the
`:testlib` link back into the script. Add golden vectors before you have two
appenders, not after.
3. `ardz build <sub>` (shadow-cljs release + workbox → `client/dist/`).
4. Create the site once, when ready: `ipns_create(hostname="<host>",
serve=true)` — IPNS name + gateway domain + DNS in one call.
5. `ardz release <sub>` — builds (rooms: bumps version.json first), adds to
IPFS, re-pins as `site:<host>`, publishes IPNS, unpins the previous site
pin only. **Rooms variant: commit the version.json bump it printed.**
6. Verify live: poll `https://<host>/` until 200, then exercise the app in
two tabs ON THE LIVE URL.
## 6. Publish the repo
Same "this page is also the git repo" pattern as every app:
`https://git.ardegazu.ro/<sub>.git` (clone) and `https://git.ardegazu.ro/<sub>/`
(landing page).
1. Rewrite `site/index.html` in the app's aesthetic: what it is, how it
works, the clone command, a link to the live app, honest guarantees.
2. `deploy/publish-repo.sh` is pre-placed with `APP`/`IDENT` set and the
new-style anonymity gate. It needs `~/.config/cod-sursa/idpat` to exist.
It also generates the in-browser source browser (`gen-browse.py` →
`/browse/`), linked from the landing page.
3. Register in `../git/assemble.sh` (a `repo_for` case + the default list)
and add a card to `../git/index.html`, then `ardz publish-src <sub>`.
4. Verify: `git clone https://git.ardegazu.ro/<sub>.git` into a temp dir;
log 100% anonymous, tree complete.
## 7. Registration follow-ups (manual, all of them)
- `home/client/catalog.json`: append an apps[] entry (schema v1:
id/name/host/kind:"app"/tagline/description/tags/accent/glyph/status,
plus the additive `i18n: {ro:{…}, hu:{…}}` map — never bump `v` for
additive fields). Bespoke glyph art goes in `home/client/src/home/glyphs.cljs`
keyed by the `glyph` field. Then release home (versioned — commit its
version bump).
- `social-kit`'s `SUITE_APPS` list.
- The git-site card (`../git/index.html`) if not done in §6.
## 8. Definition of done
Common:
- [ ] Two-tab session end-to-end on `https://<host>` (not just localhost)
- [ ] All three languages render; Romanian plurals right at n = 1 / 2 / 20
- [ ] `npm run build` twice from a clean clone produces an identical `dist`
- [ ] `npm test` green (and it checks something real: at minimum the source
lint plus your own golden vectors — see `client/test/README.md`)
- [ ] Mobile viewport: safe areas respected, PWA installable
- [ ] `git clone https://git.ardegazu.ro/<sub>.git` succeeds; log is 100% anonymous
- [ ] README documents dev/build/release; no `node_modules`/`dist`/`.site` tracked
- [ ] Hub card live (catalog.json + glyph), SUITE_APPS updated
- [ ] Report the two URLs and total pinned size
Live variant:
- [ ] Late joiner sees the current presence roster immediately
- [ ] If the app has an authority: host tab killed → survivor takes over
Rooms variant:
- [ ] Two devices that were NEVER online together converge via the mailbox
- [ ] Offline reload replays the local log (airplane-mode test)
- [ ] Lobby room list syncs across two same-identity devices; forget/tombstone propagates
- [ ] After a release bump, an installed PWA shows the update banner and
reloads into the new version
- [ ] Nothing under `node_modules/ardegazu-rooms-kit/` was edited; a fix to the
shared core landed in rooms-kit and arrived here as a sha bump
## 9. Cost summary to include in your report
No tunnels. IPFS storage hourly per MB (typically ~1 MB), TURN traffic per MB
only when peers can't connect directly; the mailbox rides the existing managed
relay. The shared relay app (`signal.ardegazu.ro`) already exists — do NOT
create a new p2p app.
|