# unibus playground An all-in-one, web-based sandbox for the **unibus** message bus. One command brings up the entire stack embedded — no NATS to install, no services to wire — and a browser UI lets you exercise the bus visually: create peers, create and join rooms (cleartext or end-to-end encrypted), invite, publish, watch messages arrive live, and kick members (forward secrecy). This is a **playground** (see `.claude/rules/playgrounds.md`): it lives inside the `unibus` app, reuses the parent Go module (no separate `go.mod`), is not indexed, and keeps all runtime state under `playground/local_files/` (ephemeral, safe to delete). ## Run From the `unibus` app directory: ```bash cd /home/enmanuel/fn_registry/projects/message_bus/apps/unibus go run ./playground ``` Then open **http://localhost:7700** in your browser. Stop with `Ctrl-C` — the server tears down the web UI, every bus client, the control plane, and the embedded NATS cleanly (no orphaned processes). ## Architecture The browser never speaks NATS. The Go server is the actual bus peer: ``` browser ──fetch/SSE──▶ playground server (:7700) │ holds one unibus client per named peer ├──HTTP──▶ membership control plane (127.0.0.1:8480) └──NATS──▶ embedded NATS + JetStream (:4260) ``` - **:7700** — web UI (the only browser-facing port). - **127.0.0.1:8480** — membership control plane (rooms, members, sealed keys, rekey, blobs). Internal only. - **:4260** — embedded NATS + JetStream (the data plane). Internal only. Each named peer gets its own long-term identity, persisted to `playground/local_files/.id`, so a peer keeps the same endpoint across restarts. When a peer creates or joins a room, the server subscribes on its behalf and streams every received frame to that peer's open browser tabs over Server-Sent Events. The playground only orchestrates the public unibus client API (`CreateRoom`, `Join`, `Subscribe`, `Publish`, `Invite`, `Kick`); it never reimplements bus or crypto logic. ## Try it: 2 peers + encryption + kick 1. Open **two browser tabs** on http://localhost:7700. 2. Tab A: type `alice`, click **Connect**. 3. Tab B: type `bob`, click **Connect**. 4. Tab A (alice): type a subject like `room.general`, tick **🔒 encrypted (E2E)**, click **Create room**. Copy the resulting `room_id`. 5. Tab A (alice): in the Action panel, pick `bob` as the target peer (use the ↻ button to refresh the peer list if needed) and click **Invite to this room**. 6. Tab B (bob): paste the `room_id` into the join field and click **Join**. 7. Type messages in **both** tabs and hit Send — each message appears live in both tabs, tagged with subject, sender, time, and 🔒 (encrypted) or `clear`. 8. Tab A (alice): click **Kick from this room** with `bob` selected. The room key rotates to a new epoch. New messages alice sends are no longer visible to bob — **forward secrecy**: bob no longer holds the current key. Cleartext rooms (leave the checkbox unticked) behave like plain NATS fan-out: fast, ephemeral, unsigned. Encrypted rooms are the Matrix-like mode: E2E encrypted, persisted, and per-message signed. ## State / cleanup All writable state lives under `playground/local_files/`: - `.id` — per-peer identity (private keys; treat like an SSH key). - `play.db` — membership store (rooms, members, sealed keys). - `blobs/` — media blob store. - `js/` — embedded JetStream store. Delete the whole `playground/local_files/` directory to reset to a clean slate. It is gitignored and never distributed.