Files
welshman/docs/app/app.md
T
hodlbod fe5c11b00f
tests / tests (push) Failing after 5m4s
rename client, update docs/skills
2026-06-18 19:31:14 +00:00

6.1 KiB

The App

An App is an application instance. It owns every piece of per-identity state and is the entry point to all features. You will usually create one with createApp and access everything else through app.use(...).

Creating an app

createApp(options?)

The batteries-included factory. It returns an App wired with the default policies (event ingestion, relay-stats collection, gift-wrap unwrapping, and NIP-42 auth) unless you pass your own policies.

import {createApp} from "@welshman/app"

const app = createApp({
  user,                                  // optional signed-in User
  config: {
    dufflepudUrl: "https://dufflepud.example",
    getDefaultRelays: () => ["wss://relay.example"],
    getIndexerRelays: () => ["wss://purplepag.es"],
    getSearchRelays: () => ["wss://relay.nostr.band"],
  },
})

new App(options?)

Use the constructor directly when you want a bare app with no side effects (for example in tests, or when you install policies yourself).

import {App} from "@welshman/app"

const app = new App()              // no policies installed

AppOptions

type AppOptions = {
  user?: User                          // the signed-in identity (at most one)
  config?: AppConfig
  getAdapter?: AdapterFactory          // net-layer adapter factory
  policies?: AppPolicy[]            // side effects to install at construction
}

AppConfig

App-level configuration. All fields are optional; the three relay getters return string[] and feed the Router.

type AppConfig = {
  dufflepudUrl?: string                // optional dufflepud service (batches NIP-05 / zapper lookups)
  getDefaultRelays?: () => string[]
  getIndexerRelays?: () => string[]    // relays used to discover relay lists / profiles
  getSearchRelays?: () => string[]     // NIP-50 search relays
}

IApp

Plugins and policies never depend on the concrete App class — they take the IApp contract:

interface IApp {
  user?: User
  config: AppConfig
  use: <T>(Ctor: new (app: IApp) => T) => T
  netContext: NetContext               // {pool, repository, getAdapter} for the net layer
  pool: Pool                           // connection pool
  tracker: Tracker                     // tracks which relays have seen each event
  repository: Repository               // the local event store / single source of truth
  wrapManager: WrapManager             // NIP-59 gift-wrap bookkeeping
}

Every primitive (pool, tracker, repository, wrapManager) is constructed fresh per instance, so data never bleeds across identities or sessions.

Resolving features: use

use: <T>(Ctor: new (app: IApp) => T) => T

use is a per-app singleton resolver. The first time you pass a plugin class, the app constructs new Ctor(this) and caches it; subsequent calls return the same instance.

const profiles = app.use(Profiles)
const sameInstance = app.use(Profiles)   // identical reference

This is dependency resolution by demand. Plugins reach their own dependencies the same way (this.app.use(Network), this.app.use(Router)), which means dependency cycles resolve lazily and there is no constructor wiring to maintain.

Teardown: cleanup

app.cleanup()

cleanup() runs every policy's unsubscribe function, then clears the pool, tracker, repository, and wrapManager. Call it when you discard an app (e.g. switching identities) to release connections and free memory.

Policies

A policy is the unit of side effects. It runs once at construction and returns an Unsubscriber that cleanup() will later call. Keeping side effects in policies leaves the data plugins pure and centralizes teardown.

type AppPolicy = (app: IApp) => Unsubscriber

Default policies

createApp installs defaultAppPolicies:

Policy What it does
appPolicyIngest Subscribes to the pool; verifies inbound relay events (skipping DVM/ephemeral kinds) and writes them to the repository and tracker. This is how every repository-backed store gets populated.
appPolicyRelayStats Pipes socket activity into the RelayStats store.
appPolicyWraps Enqueues existing and newly-arriving gift-wrap events for unwrapping.
appPolicyAuthUnlessBlocked Answers NIP-42 AUTH challenges, except for relays in the user's blocked-relay list.

Auth policy builders

makeAppPolicyAuth(shouldAuth: (socket: Socket, app: IApp) => boolean): AppPolicy

appPolicyAuthNever            // never answer AUTH
appPolicyAuthAlways           // always answer AUTH
appPolicyAuthUnlessBlocked    // answer unless the relay is blocked by the user

Auth policies are no-ops when there is no signed-in user.

Customizing policies

Pass your own policies array to opt out of, or extend, the defaults:

import {App, defaultAppPolicies, makeAppPolicyLogger} from "@welshman/app"

const app = new App({
  user,
  policies: [
    ...defaultAppPolicies,
    makeAppPolicyLogger(msg => console.log(msg)),   // see Logging
  ],
})

Logging

@welshman/app can make a user's signer observable. User.fromSigner/User.fromSession wrap the underlying signer in a LoggingSigner, which emits a structured LogMessage for every signer operation (pending → success/failure).

type LogMessage =
  | {type: "signer"; id: string; method: string; status: "pending" | "success" | "failure"; error?: unknown; at: number}
  | {type: string; at: number; [key: string]: unknown}

Forward those messages by installing makeAppPolicyLogger:

import {makeAppPolicyLogger} from "@welshman/app"

const app = new App({
  user,
  policies: [...defaultAppPolicies, makeAppPolicyLogger(msg => {
    if (msg.type === "signer" && msg.status === "failure") {
      console.error("signing failed", msg.method, msg.error)
    }
  })],
})

The logger policy is a no-op unless the user's signer is a LoggingSigner (which it is when the user was created via User.fromSigner/User.fromSession).