@soapbox.pub/nostr-canvas
v0.4.6
Published
Programmable Nostr tiles — runtime, Lua sandbox, and React bindings for the nostr-canvas NIP.
Readme
nostr-canvas
A Nostr client framework where UI is defined by tiles — mini apps written in Lua and published as kind-30207 Nostr events. Any compatible client can fetch and run tiles without modification.
See the demo for a running example.
- Tile scripting API and kind-30207 event format: NIP.md
- Host integration guide and philosophy: PHILOSOPHY.md
- Full example host app:
app/ - Sample tiles:
tiles/
Installation
pnpm add @soapbox.pub/nostr-canvasEntry points
| Import path | Contents |
| ---------------------------------------- | ---------------------------------------------------------- |
| @soapbox.pub/nostr-canvas | Core runtime, types, adapter interface, patch helpers |
| @soapbox.pub/nostr-canvas/react | React provider, hooks, and TileView component |
| @soapbox.pub/nostr-canvas/components | Optional default implementations of Feed, Comments, Markdown |
Quick start (React)
1. Implement the adapter
The adapter bridges the runtime to your app's existing relay pool, signer, and
profile cache. Only subscribe is required; all other methods are optional.
import type { NostrAdapter } from "@soapbox.pub/nostr-canvas";
const adapter: NostrAdapter = {
// Required: wire relay subscriptions
subscribe(filter, onEvent) {
const sub = pool.subscribe(relays, filter, { onevent: onEvent });
return () => sub.close();
},
// Optional: expose capabilities as your app supports them
fetchEvents: (filter) => pool.querySync(relays, filter),
getPublicKey: () => window.nostr.getPublicKey(),
signEvent: (event) => window.nostr.signEvent(event),
publishEvent: async (event) => {
const signed = await window.nostr.signEvent(event);
await pool.publish(relays, signed);
return signed;
},
getProfile: (pubkey, cb) => profileCache.subscribe(pubkey, cb),
navigate: (target) => router.push(target),
};2. Wrap your app in the provider
import { NostrCanvasProvider } from "@soapbox.pub/nostr-canvas/react";
function App() {
return (
<NostrCanvasProvider adapter={adapter} options={{ onPermissionRequest }}>
{/* your app */}
</NostrCanvasProvider>
);
}3. Install a tile and render it
import { useNostrCanvas, TileView } from "@soapbox.pub/nostr-canvas/react";
import { Feed, Comments, Markdown } from "@soapbox.pub/nostr-canvas/components";
import { NEvent } from "./your-nevent-component";
function MyView() {
const { runtime } = useNostrCanvas();
useEffect(() => {
runtime.registerFromEvent(kind30207Event);
}, []);
return (
<TileView
identifier="[email protected]:longform"
placement="main"
components={{ Feed, Comments, Markdown, NEvent }}
/>
);
}TileView mounts the tile, subscribes to its output and patch events, and
renders the resulting TileOutput tree as unstyled semantic HTML with
nc-tile-node-* CSS classes. Apply your own styles on top.
Component slots
Complex node types (feed, comments, markdown, nevent) are delegated to
host-supplied components via the components prop. The /components subpath
ships ready-to-use defaults for the first three:
import { Feed, Comments, Markdown } from "@soapbox.pub/nostr-canvas/components";These are optional and tree-shaken when not imported. NEvent is always
host-supplied — you know best how to render a Nostr note card in your app.
Listening to runtime events
const unsub = runtime.on("toast", ({ message, variant, duration }) => {
showToast(message, variant);
});
// cleanup
unsub();Available event types: tile-output, tile-patch, tile-removed,
nav-changed, registrations-changed, settings-fields-changed, modal,
toast. See RuntimeEventMap for full type signatures.
Core API
const runtime = new TileRuntime(adapter, {
onPermissionRequest: async (identifier, capability) => "granted",
});
// Install a tile from a kind-30207 event
runtime.registerFromEvent(event);
// Create an instance
const tileId = runtime.createTile("[email protected]:weather", {
placement: "widget",
props: { location: "London" },
});
// Subscribe to its output
const unsub = runtime.onTileOutput(tileId, (output) => render(output));
// Deliver a user interaction
runtime.deliverInputEvent(tileId, "button-clicked", { value: 42 });
// Tear down
runtime.removeTile(tileId);
runtime.destroy();Development
pnpm i && pnpm run dev # start the example app dev server
pnpm build # build the library