Skip to content

Commit 32f85ac

Browse files
committed
docs: fork-with-fallback ships — record mode, the new civic kinds, and the keyless smoke test
1 parent db2d24d commit 32f85ac

4 files changed

Lines changed: 143 additions & 13 deletions

File tree

docs/FORKING_GUIDE.md

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@ towns who want to **fork the platform** for themselves. If you just asked *"is t
55
a staging environment / how do I test this easily?"* — start with
66
[Testing against staging](#testing-against-staging).
77

8+
- **Just want to see it run, zero setup?**[Ohne Supabase starten (record mode)](#ohne-supabase-starten-record-mode)
89
- **Contributor?**[Prerequisites](#prerequisites)[Testing against staging](#testing-against-staging)
910
- **Maintainer provisioning staging?**[docs/STAGING_ENVIRONMENT.md](STAGING_ENVIRONMENT.md)
1011
- **Forking for your own town?**[Fork for your own town](#fork-for-your-own-town)
1112

1213
---
1314

14-
## TL;DR — the three ways to test
15+
## TL;DR — the four ways to test
1516

1617
| Goal | What to do | Setup effort |
1718
|---|---|---|
19+
| Clone and run the **web** app with zero backend | `pnpm install`, run `apps/web` with no env | none |
1820
| Just look at / click through the app | Open **https://stage.roebel.app** | none |
1921
| Develop the **web** app | Run `apps/web` locally, point at staging Supabase | ~2 min |
2022
| Develop the **mobile** app | Run `apps/expo` in Expo Go, point at staging Supabase | ~5 min |
@@ -24,6 +26,64 @@ You only fork if you want your *own town's* independent instance.
2426

2527
---
2628

29+
## Ohne Supabase starten (record mode)
30+
31+
The fastest way to see the app run at all: **no Supabase project, no env file, no
32+
credentials.** `apps/web` reads the town's public record straight off the node's
33+
HTTP index instead of PostgREST, so every public page renders with real data even
34+
when `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` are absent.
35+
36+
```bash
37+
git clone https://github.com/Roebel-Labs/Roebel-App.git
38+
cd Roebel-App
39+
pnpm install
40+
cd apps/web
41+
pnpm dev # → http://localhost:3000, no .env.local needed
42+
```
43+
44+
Open it and you'll see **Röbel's actual public record** — the default index is
45+
`https://index.roebel.app`, the same node the production app talks to, just
46+
through its read-only public interface instead of the database.
47+
48+
To point the app at a **different community's node** instead, set one variable:
49+
50+
```bash
51+
NEXT_PUBLIC_NODE_INDEX_URL=https://index.example-town.app pnpm dev
52+
```
53+
54+
That's the whole seam — [`apps/web/src/lib/record.ts`](../apps/web/src/lib/record.ts)
55+
constructs the index client from that single env var, defaulting to Röbel's own
56+
node when it's unset.
57+
58+
**What works in record mode** — all public reading: the feed, events, news,
59+
cinema programme, organisations, marketplace, business deals, restaurant menus,
60+
governance proposals, civic notices, and the map.
61+
62+
**What does not** — anything that needs a backend to write or to know who you
63+
are: login, posting, likes/comments, any form, DMs, notifications, the points
64+
card, the mini-app runtime, QR ordering. A navy banner across the top says so
65+
plainly: *"Öffentlicher Datensatz – nur Lesen. Diese Instanz läuft ohne Backend
66+
und zeigt das öffentliche Register der Stadt."* — and every write affordance is
67+
hidden rather than shown-then-failing.
68+
69+
This is genuinely read-only, not a demo mode with fake data — it's Röbel's live
70+
public record, sourced the same way an outside client would. See
71+
[Public data on Nostr](PUBLIC_DATA_ON_NOSTR.md) for what's published and why, and
72+
[Roadmap and deferred work §13a](ROADMAP_AND_DEFERRED.md) for how this shipped.
73+
74+
**The acceptance test is [`apps/web/scripts/keyless-smoke.sh`](../apps/web/scripts/keyless-smoke.sh)**
75+
it builds and boots the app with the Supabase env genuinely absent (not just
76+
unread) and asserts all eight public routes (`/`, `/news`, `/app`,
77+
`/app/marktplatz`, `/proposals`, `/karte`, `/app/events`, `/unternehmen`) return
78+
HTTP 200 with the record-mode banner present. Run it yourself with:
79+
80+
```bash
81+
cd apps/web
82+
./scripts/keyless-smoke.sh
83+
```
84+
85+
---
86+
2787
## Prerequisites
2888

2989
- [Node.js](https://nodejs.org/) v20+

docs/PUBLIC_DATA_ON_NOSTR.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ public datasets onto the relay every 5 minutes — first live pass: 41 events ac
3535
| **images** — mirrored content-addressed at `/media/<sha256>` (Blossom-shaped reads); the hash in the signed event is the integrity check | |
3636
| **the manifest**`/manifest` on the index: chain id + every contract address, so governance and Münzen are discoverable on-chain without asking anyone | |
3737

38+
As of 2026-08-02, the **web app itself** reads this table's left column back out, not just the
39+
publisher writing it: any public route renders from the node's index when Supabase is absent,
40+
which is the other end of this migration — [Forking Guide → Ohne Supabase
41+
starten](FORKING_GUIDE.md#ohne-supabase-starten-record-mode) and
42+
[Roadmap §13a](ROADMAP_AND_DEFERRED.md) cover the read side. The remaining "not yet" row —
43+
per-person opt-in content — is the only genuinely missing public dataset; everything else a
44+
keyless fork needs to render is already in the left column above.
45+
3846
**The withdrawal path shipped before the publish path**, exactly as §3.4 demands — and the
3947
first kind-30018 events on the record were four withdrawal tombstones, published before any
4048
active listing (their sellers had not opted in, so the gate held them back). The index honours

docs/ROADMAP_AND_DEFERRED.md

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,35 @@ media: calendar data should use NIP-52 kinds rather than invented ones, and medi
185185
served from a Blossom server next to strfry — NIP-96 is officially deprecated in its
186186
favour. **Trigger:** publishing the events/cinema/org datasets, or any media, to the relay.
187187

188-
### 13a. Fork-with-fallback: the app reads Nostr when Supabase is absent
189-
190-
The end state of the dual system: someone forks the Röbel web or expo repo, configures no
191-
Supabase, and the app still shows the whole public record by reading the node's index/relay —
192-
same data, slightly slower. The pieces exist (every public dataset is on the record, the
193-
consumer contract is documented in the Netizen repo's CONSUMING_THE_RECORD.md); what is
194-
missing is a data-layer seam in the apps that falls back from PostgREST to `/events` queries
195-
per dataset. **Trigger:** the first real fork, or the Atlas proving a full read-only client —
196-
whichever comes first.
188+
### 13a. ~~Fork-with-fallback: the app reads Nostr when Supabase is absent~~ — DONE 2026-08-02
189+
190+
The end state of the dual system: someone forks the Röbel web repo, configures no Supabase,
191+
and the app still shows the whole public record by reading the node's index — same data,
192+
read-only. **Shipped for web**: all three Supabase client factories return a throw-on-access
193+
Proxy when keyless (construction never crashes SSR), every public read path branches on
194+
`hasSupabase` and falls back to `@netizen-labs/record-client` against the node's
195+
`/events` index, and a sweep removed render-time Supabase access from every public route. A
196+
navy banner marks the instance as read-only and hides write affordances rather than letting
197+
them fail. `NEXT_PUBLIC_NODE_INDEX_URL` repoints a fork at a different community's node;
198+
default is Röbel's own (`https://index.roebel.app`).
199+
200+
**Proof:** [`apps/web/scripts/keyless-smoke.sh`](../apps/web/scripts/keyless-smoke.sh) builds
201+
and boots the app with the Supabase env genuinely absent and asserts all 8 public routes (`/`,
202+
`/news`, `/app`, `/app/marktplatz`, `/proposals`, `/karte`, `/app/events`, `/unternehmen`)
203+
return HTTP 200 with the record-mode notice present — not just a 200, the actual record-mode
204+
banner, so the test fails if a route silently falls back to empty data instead of reading the
205+
index. See [Forking Guide → Ohne Supabase starten](FORKING_GUIDE.md#ohne-supabase-starten-record-mode).
206+
207+
**Honest gaps, not swept under this:**
208+
- **~60 of the ~127 handlers under `apps/web/src/app/api/**` are unaudited** for keyless
209+
behaviour. A page rendering does not imply every API route it might call degrades
210+
gracefully — an unaudited handler that assumes Supabase will 500, which is a broken
211+
endpoint, not a broken page. The smoke test only exercises page routes.
212+
- **Interaction counts (likes, comments, reposts) are advisory in record mode.** The index
213+
reflects what the relay has mirrored, not a live tally, so a count shown to a keyless reader
214+
can lag the authoritative Supabase-backed number by up to one publisher cycle.
215+
- **Expo is out of scope.** This shipped for `apps/web` only; the mobile app still requires
216+
Supabase env to run at all.
197217

198218
### 13. ~~The index does not honour replaceable events~~ — DONE 2026-07-30
199219

docs/STATE_OF_NOSTR.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,14 @@ Two rails, distinguished by **who holds the key**:
107107
- **Node-signed** (node-held per-organisation keys, since 2026-07-30): the public CMS
108108
datasets, mirrored by `@netizen-labs/publisher` (`services.publisher` in the manifest).
109109
Events and cinema as NIP-52 `31923`, organisation profiles as kind 0 — the cinema's
110-
screenings signed by the cinema's derived key, an org's events by that org's key. relay-sync
111-
merges the publisher's pubkeys into the allow-list each pass (`EXTRA_KEYS_FILE`). Details
112-
and the privacy boundary: [Public data on Nostr](PUBLIC_DATA_ON_NOSTR.md) §1.
110+
screenings signed by the cinema's derived key, an org's events by that org's key. Three
111+
civic kinds round out the CMS datasets: town news as NIP-23 `30023` (`d=news:<uuid>`),
112+
restaurant menus as **kind `32101`** (one replaceable event per restaurant, `d=restaurant:<id>`),
113+
civic notices as **kind `32102`** (town-signed, a resolved alert is an edit, never a
114+
deletion), and governance proposals as **kind `32100`** — a discoverable pointer only, body
115+
stays on Irys and tallies stay on-chain. relay-sync merges the publisher's pubkeys into the
116+
allow-list each pass (`EXTRA_KEYS_FILE`). Details and the privacy boundary: [Public data on
117+
Nostr](PUBLIC_DATA_ON_NOSTR.md) §1.
113118

114119
On delete the app publishes a NIP-09 kind 5 request and says plainly in the UI that erasure
115120
on Nostr is **advisory** — relays may ignore it, and clients that already fetched an event
@@ -201,6 +206,16 @@ history, and **provenance** — which node an event came from.
201206

202207
`GET /events?q=&kinds=&authors=&since=&until=&node=&limit=` · `GET /stats` · `GET /health`
203208

209+
**`/events` also takes `e`, `p` and `d` tag filters**, added 2026-08-02 for the fork-with-fallback
210+
read path (§13a of the roadmap): `e`/`p` match via a GIN-indexed `tags @> …` JSONB containment
211+
query (`idx_nostr_events_tags`), so replies and reactions to a given event or author are
212+
queryable without a full scan; `d` filters on the `d_tag` column directly — the same column the
213+
replaceable-event collapse ([Roadmap §13](ROADMAP_AND_DEFERRED.md)) already maintains — so a
214+
client can ask for one stable record (a restaurant's menu, a proposal pointer) by its `d` tag
215+
alone. This is what lets
216+
`@netizen-labs/record-client` (and so a keyless `apps/web`) fetch a specific parameterised
217+
replaceable event instead of paging through everything of a kind.
218+
204219
Public read by design. Everything in it came off world-readable relays, so publishing leaks
205220
nothing new, and it is what lets a **peer's** agent query this node.
206221

@@ -209,6 +224,33 @@ event whose signature is re-verified on ingest rather than trusted from a peer,
209224
store is rebuildable by re-reading the relays. Drop the database and nothing is lost — that is
210225
what keeps query efficiency from turning into lock-in.
211226

227+
## 6a. Fork-with-fallback: the web app reads the index — live 2026-08-02
228+
229+
The other end of publishing is consuming. **`apps/web` now has a record-mode read path**: every
230+
public route reads through `@netizen-labs/record-client` against a node's `/events` index
231+
instead of PostgREST whenever Supabase credentials are absent, so a fork with no backend at all
232+
still renders the town's real public record — read-only, slightly slower, same data. This closes
233+
[Roadmap §13a](ROADMAP_AND_DEFERRED.md), previously the largest gap in §8/§9 below: until now,
234+
publishing to the relay proved the record was *public*, not that an outside app could actually
235+
*consume* it end to end.
236+
237+
The mechanism: the three Supabase client factories construct a throw-on-access `Proxy` when
238+
keyless rather than crash on import, every public data-fetching function branches on
239+
`hasSupabase` and falls back to the record-client, and `NEXT_PUBLIC_NODE_INDEX_URL` (default
240+
Röbel's own `https://index.roebel.app`) is the one variable a fork sets to point at a different
241+
node. A navy banner marks the instance read-only and every write affordance is hidden rather
242+
than left to fail. [`apps/web/scripts/keyless-smoke.sh`](../apps/web/scripts/keyless-smoke.sh)
243+
is the acceptance test — it builds and boots the app with the Supabase env genuinely absent and
244+
asserts all 8 public routes return HTTP 200 with the record-mode notice present. See [Forking
245+
Guide → Ohne Supabase starten](FORKING_GUIDE.md#ohne-supabase-starten-record-mode) for how to
246+
run it yourself.
247+
248+
Scoped to `apps/web` only — Expo still requires Supabase env to run — and honest gaps remain:
249+
roughly 60 of the ~127 `apps/web/src/app/api/**` handlers are unaudited for keyless behaviour
250+
(a page rendering does not prove every API route it might call degrades gracefully), and
251+
interaction counts (likes, comments, reposts) shown in record mode are advisory, reflecting
252+
whatever the index last mirrored rather than a live tally.
253+
212254
## 7. Agents on the record (slice 4) — live 2026-07-28
213255

214256
An agent has no wallet, so it cannot derive a key from a wallet signature the way a Citizen

0 commit comments

Comments
 (0)