flotilla/.agents/skills/welshman/SKILL.md
2026-09-14 14:24:36 -07:00

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})
    },
  })
})