8.3 KiB
| name | description |
|---|---|
| welshman | Use this skill for general welshman questions: architecture overview, which package to use, getting started, nostr concepts, or when you're unsure which sub-skill applies. Welshman is a modular TypeScript nostr toolkit for building client applications. |
What is welshman
Welshman is a modular TypeScript nostr toolkit extracted from the Coracle nostr client, designed for building highly configurable nostr client applications. It is production-tested, powering both Coracle and Flotilla. Packages are independent and opt-in — you can grab a single utility or use the full batteries-included framework.
Package map
| Package | Description |
|---|---|
@welshman/util |
Core nostr types, event helpers, filters, NIP implementations, and the relay-selection routing DSL |
@welshman/lib |
General-purpose utilities: LRU cache, event emitter, deferred promises, task queue |
@welshman/net |
Relay connections, request/publish lifecycle, auth, and the Repository/Tracker/WrapManager stores |
@welshman/store |
Svelte store primitives over a Repository — live event and domain-object collections, cached loaders, persistence |
@welshman/signer |
Signing and login methods: NIP-01 (privkey), NIP-07 (extension), NIP-46 (bunker), NIP-55 (app), NIP-59 (gift wrap) |
@welshman/domain |
Typed Reader/Writer classes per event kind (profiles, notes, lists, rooms, relay management) that parse events, build templates, and emit relay routing |
@welshman/feeds |
Dynamic feed construction, filtering, and composition |
@welshman/app |
Instance-based application framework: an App composes net, store, signer, feeds, and domain, exposing data modules via app.use(...) |
@welshman/content |
Parser and renderer for nostr note content (links, mentions, media, custom formatting) |
@welshman/editor |
Batteries-included Svelte rich-text editor component with mention and embed support |
Dependency layering
Packages are layered so lower-level ones have no welshman dependencies:
- Foundational (no welshman deps):
@welshman/lib,@welshman/util - Mid-level (depend only on foundational):
@welshman/net,@welshman/store,@welshman/signer - Composing (depend on mid-level + foundational):
@welshman/feeds,@welshman/domain - Application (composes everything above):
@welshman/app - UI-focused (largely independent, UI rendering concerns):
@welshman/content,@welshman/editor
For deep-dives on any package, load the welshman-<name> skill (e.g. welshman-net, welshman-app, welshman-domain, welshman-signer).
Relay selection spans two packages. The RelaySelection DSL, Resolver and RelayScenario are in @welshman/util (welshman-util skill); the Router plugin that dereferences them is in @welshman/app (welshman-app skill). There is no @welshman/router package.
Getting started
Install only what you need:
# Full application framework (includes app, net, store, signer, feeds, domain)
npm i @welshman/app
# Or assemble manually for more control
npm i @welshman/util @welshman/net @welshman/signer
If you're building a conventional nostr web client, use @welshman/app for batteries-included functionality. For more advanced usage, use the lower-level modules without app for more control.
Key nostr concepts
- event — the fundamental data unit in nostr; a JSON object signed by a keypair
- kind — integer field on an event that determines its type (e.g. kind 1 = short text note, kind 0 = profile metadata)
- filter — a query object (
{kinds, authors, since, until, limit, ...}) sent to relays to request matching events - relay — a WebSocket server that stores and forwards nostr events; clients connect to multiple relays
- NIP — "Nostr Implementation Possibility"; numbered specifications defining protocol behavior and event kinds
- pubkey — 32-byte hex public key that identifies a nostr user
- signer — abstraction over key management; handles signing events and optionally encryption, regardless of where the private key lives (in-memory, browser extension, remote bunker, mobile app)
Common use-case routing
| Goal | Package(s) to use |
|---|---|
| Fetch notes from relays | @welshman/net (low-level) or @welshman/app (high-level) |
| Compose typed events (notes, profiles, lists) | @welshman/domain |
| Select which relays to read from / publish to | @welshman/util (routing DSL) + @welshman/app (Router plugin) |
| Sign and publish events | @welshman/domain + @welshman/app, or @welshman/signer + @welshman/net |
| Build a feed UI | @welshman/feeds + @welshman/app |
| Parse note text and media | @welshman/content |
| Embed a composer / editor | @welshman/editor |
| Cache nostr events client-side | @welshman/net (Repository) + @welshman/store (reactive views over it) |
| Core event/filter utilities | @welshman/util |
| Low-level helpers (LRU, emitter, utility functions) | @welshman/lib |
App Example
import { Nip07Signer } from "@welshman/signer"
import { Note } from "@welshman/domain"
import { createApp, User, Domain, Profiles } from "@welshman/app"
// 1. Create an app instance. Each App owns its own repository, socket pool,
// tracker, and (optional) signing user, so data never leaks across identities.
// Pass the user at construction rather than assigning app.user afterwards.
const user = await User.fromSigner(new Nip07Signer())
const app = createApp({
user,
config: {
getDefaultRelays: () => ["wss://relay.example.com", "wss://relay2.example.com"],
getIndexerRelays: () => ["wss://indexer.example.com"],
},
})
// 2. Hydrate the repository from storage and flush changes back to it.
// See the welshman-net skill for repository.load() and the "update" listener.
// 3. Load the user's profile through the Profiles data module
// (triggers a network fetch via the outbox model if not cached)
const profile = await app.use(Profiles).forceLoad(user.pubkey)
if (profile) console.log("Hello,", profile.display())
// ...or subscribe reactively:
app.use(Profiles).one(user.pubkey).subscribe($profile => {
if ($profile) console.log("Profile:", $profile.display())
})
// 4. Compose and publish a note. Domain builds the event and resolves relays;
// the returned Command sends it through the publish pipeline.
const writer = app.use(Domain).writer(Note).setContent("Hello, Nostr!")
const command = await app.use(Domain).command(writer)
await command.publish()
Lower-level Example
import { AbstractAdapter, ClientMessage, isClientEvent, publish, request } from '@welshman/net'
import type { NetContext } from '@welshman/net'
import { call, sleep } from '@welshman/lib'
import { Nip01Signer } from '@welshman/signer'
import { makeEvent, NOTE } from '@welshman/util'
const pingSigner = Nip01Signer.fromSecret(/* nostr hex secret key */)
const pongSigner = Nip01Signer.fromSecret(/* nostr hex secret key */)
const RELAY_URL = "bogus.relay"
// Create an adapter for our relay url which just prints the content
export class PrintAdapter extends AbstractAdapter {
get sockets() { return [] }
get urls() { return [] }
send = (message: ClientMessage) => {
if (isClientEvent(message)) {
const [_, event] = message
console.log(event.content)
}
}
}
// A net context that routes our relay url to the custom adapter. Context is
// passed per call now — there is no module-level singleton.
const context: NetContext = {
getAdapter: (url: string) => {
if (url === RELAY_URL) {
return new PrintAdapter()
}
},
}
// Loop, sending off pings every so often
call(async () => {
while (true) {
await sleep(1000)
const ping = await pingSigner.sign(
makeEvent(NOTE, {content: 'ping'})
)
await publish({event: ping, relays: [RELAY_URL], context})
}
})
// Meanwhile, listen for pings and quote-note with a pong
call(async () => {
request({
relays: [RELAY_URL],
context,
filters: [{kinds: [NOTE], authors: [await pingSigner.getPubkey()]}],
onEvent: async (ping, url) => {
const pong = await pongSigner.sign(
makeEvent(NOTE, {content: 'pong', tags: [["q", ping.id, RELAY_URL, ping.pubkey]]})
)
await publish({event: pong, relays: [RELAY_URL], context})
},
})
})