NostrClay demo B · approach B, the event-composed page

Relay Day

A session board with three people writing to it and no server anywhere. The file you just downloaded contains none of the sessions. They arrive a moment later as signed, addressable kind 35129 events, are verified in your browser, sanitised, and applied into the marked regions below.

01 The board

One region, one row per contributor. A row is an addressable event: the relay files it under (kind, pubkey, d-tag), so republishing with the same d replaces it in place, forever, and nobody else's key can touch it.

SlotSessionSpeakerSigned by
The programme is not in this file Every session below is one signed event, fetched from relays at view time. nobody yet
If you are reading this, the relays stayed quiet Either JavaScript is off, or the relays in nc:relays are unreachable from here.

Your row ·

Three <td> cells: slot, session, speaker. Publish signs one event and pushes it to the relays; every browser on this page re-renders. Retract files a kind 5 deletion for the same address.

Signed in, not on the list

Your key is not in nc:writers, so nc.publishRegion() refuses to sign. Nothing stops you publishing a 35129 event addressed at this page by hand, though — relays take anything. Try it, and watch every reader's copy of this page drop it on the floor.

02 Who is allowed to write

Access control is not a permission bit on a server. It is the subscription filter plus a signature check: an event only becomes DOM if its signature verifies, its a tag names this document, and its author is on the list below.

Writer list (from nc:writers)

    The filter this page actually subscribes with

    Add nc:open to <html> and the authors line disappears: anyone's signature is accepted and the page becomes a wiki. The markup is still sanitised — nc:open relaxes who, never what. This demo ships with the whitelist, so the events on the right are refused.

    Events addressed here from keys not on the list

    • Listening…

    These are real events, sitting on the same relays, naming this exact document. They are on your machine right now — this panel had to ask for them with a second, deliberately unfiltered subscription. The page's own subscription never requests them, and accept() would refuse them anyway. That is the entire access-control model.

    03 What a crawler sees

    This is approach B's bill, and it is due immediately. The bytes the gateway serves are the shell; the board is assembled afterwards, in a browser, over websockets. Below: the served file, fetched back from location.href and measured against the DOM you are looking at.

    Served bytes vs composed DOM

    measuring…

    The rows region, as served

    The fix, when you want it

    nc.compact() (owner only) folds every applied event into the document, stamps nc:since as a watermark, and rewrites the file through approach A. After that the static bytes are the board, the layer starts empty, and new events pile up on top again. Demo B deliberately never compacts, so you can see the gap it closes.

    04 Contributed markup is hostile markup

    A writer is trusted to own a row, not to be nice. One of the whitelisted keys has published a region event whose content is a small pile of XSS. It is on the relays verbatim; it is inert here.

    As signed and stored on the relay

    After nc.sanitize(), as inserted into the DOM

    Rendered, live, in this page

    No hazard event has arrived yet.

    The allow-list is tags and attributes, not a blocklist: script, style, iframe, form and friends are dropped, every on* handler goes, and a URL survives only if it starts https:, http:, mailto:, # or /. publishRegion() sanitises on the way out too, so a compliant client never even signs the dirty version — but that is a courtesy, not a defence. The defence is that your browser sanitises on the way in.

    05 The mechanism, in one paragraph

    This document is addressed as . Every region event carries that address in an a tag and its own d tag of the form <address>#<region>. The runtime subscribes once, verifies each signature with verifyEvent, keeps the newest event per (author, d), sorts by created_at, and writes them into [nc:region] hosts — stamping nc:by, nc:d and nc:at on each item, which is where the provenance column above comes from. No coordinator, so ordering is by the clock in each signer's pocket: edit your row and it moves to the bottom of the board.

    This page's configuration

    booting…

    Sign in

    Signing in only decides which row this browser lets you edit. What decides whether an edit sticks is whose key signed the event, checked again in every reader's browser.

    The key stays in this tab’s memory and is written nowhere — but you are typing it into a page served by a gateway you don’t control. Amber signs without ever handing the key over; prefer it for anything you care about.