Add in-process Parrhesia clone/fetch helpers for paging, streaming, importing, and copying stored events under existing request-context policy checks. Extend tag filters to support app-scoped multi-character tags such as dataset/ref and document the 0.15.0 release.
6.6 KiB
Parrhesia Local API
Parrhesia can run as a normal standalone relay application, but it also exposes a stable in-process API for Elixir callers that want to embed the relay inside a larger OTP system.
This document describes that embedding surface. The runtime is now beta, so treat the API as usable with minor churn possible while it stabilizes.
What embedding means today
Embedding currently means:
- the host app adds
:parrhesiaas a dependency and OTP application - the host app provides
config :parrhesia, ...explicitly - the host app migrates the Parrhesia database schema
- callers interact with the relay through
Parrhesia.API.* - host-managed HTTP/WebSocket ingress is mounted through
Parrhesia.Plug
Current operational assumptions:
- Parrhesia runs one runtime per BEAM node
- core processes use global module names such as
Parrhesia.ConfigandParrhesia.Web.Endpoint - the config defaults in this repo's
config/*.exsare not imported automatically by a host app
If you want multiple isolated relay instances inside one VM, Parrhesia does not support that cleanly yet.
Minimal host setup
Add the dependency in your host app:
defp deps do
[
{:parrhesia, path: "../parrhesia"}
]
end
Configure the runtime in your host app. At minimum you should carry over:
import Config
config :postgrex, :json_library, JSON
config :parrhesia,
relay_url: "wss://relay.example.com/relay",
listeners: %{},
storage: [backend: :postgres]
config :parrhesia, Parrhesia.Repo,
url: System.fetch_env!("DATABASE_URL"),
pool_size: 10,
types: Parrhesia.PostgresTypes
config :parrhesia, Parrhesia.ReadRepo,
url: System.fetch_env!("DATABASE_URL"),
pool_size: 10,
types: Parrhesia.PostgresTypes
config :parrhesia, ecto_repos: [Parrhesia.Repo]
Notes:
listeners: %{}is the official embedding pattern when your host app owns the HTTPS edge.listeners: %{}disables Parrhesia-managed ingress (/relay,/management,/metrics, etc.).- Mount
Parrhesia.Plugfrom the host app when you still want Parrhesia ingress behind that same HTTPS edge. Parrhesia.Web.*modules are internal runtime wiring. TreatParrhesia.Plugas the stable mount API.- If you prefer Parrhesia-managed ingress instead, copy the listener shape from the config reference in README.md.
- Production runtime overrides still use the
PARRHESIA_*environment variables described in README.md.
Migrate before serving traffic:
Parrhesia.Release.migrate()
In development, mix ecto.migrate -r Parrhesia.Repo works too.
Mounting Parrhesia.Plug from a host app
When listeners: %{} is set, you can still expose Parrhesia ingress by mounting Parrhesia.Plug
in your host endpoint/router and passing an explicit listener config:
forward "/nostr", Parrhesia.Plug,
listener: %{
id: :public,
transport: %{scheme: :https, tls: %{mode: :proxy_terminated}},
proxy: %{trusted_cidrs: ["10.0.0.0/8"], honor_x_forwarded_for: true},
features: %{
nostr: %{enabled: true},
admin: %{enabled: true},
metrics: %{enabled: true, access: %{private_networks_only: true}}
}
}
Use the same listener schema documented in README.md.
Mounted listeners can override any Parrhesia route relative to the mount point:
forward "/nostr", Parrhesia.Plug,
listener: %{
paths: %{
health: "/status/health",
ready: "/status/ready",
relay: "/api/relay",
management: "/api/management",
metrics: "/ops/metrics"
}
}
With that config, the host app exposes /nostr/status/health, /nostr/api/relay,
/nostr/api/management, and /nostr/ops/metrics.
Starting the runtime
In the common case, letting OTP start the :parrhesia application is enough.
If you need to start the runtime explicitly under your own supervision tree, use
Parrhesia.Runtime:
children = [
{Parrhesia.Runtime, name: Parrhesia.Supervisor}
]
Primary modules
The in-process surface is centered on these modules:
Parrhesia.API.Eventsfor publish, query, and countParrhesia.API.Streamfor REQ-like local subscriptionsParrhesia.API.Authfor event validation and NIP-98 auth parsingParrhesia.API.Adminfor management operationsParrhesia.API.Identityfor relay-owned key managementParrhesia.API.ACLfor protected sync ACLsParrhesia.API.Syncfor outbound relay sync management and local bulk clone/fetch helpers
Generated ExDoc groups these modules under Embedded API.
Request context
Most calls take a Parrhesia.API.RequestContext. This carries authenticated pubkeys and
caller metadata through policy checks.
%Parrhesia.API.RequestContext{
caller: :local,
authenticated_pubkeys: MapSet.new()
}
If your host app has already authenticated a user or peer, put that pubkey into
authenticated_pubkeys before calling the API.
Example
alias Parrhesia.API.Events
alias Parrhesia.API.RequestContext
alias Parrhesia.API.Stream
context = %RequestContext{caller: :local}
{:ok, publish_result} = Events.publish(event, context: context)
{:ok, events} = Events.query([%{"kinds" => [1]}], context: context)
{:ok, page} = Parrhesia.API.Sync.page_events(%{"kinds" => [1]}, context: context, limit: 500)
{:ok, ref} = Stream.subscribe(self(), "local-sub", [%{"kinds" => [1]}], context: context)
receive do
{:parrhesia, :event, ^ref, "local-sub", event} -> event
{:parrhesia, :eose, ^ref, "local-sub"} -> :ok
end
:ok = Stream.unsubscribe(ref)
Bulk clone/fetch helpers
Application layers that need policy-filtered clone or fetch flows can use
Parrhesia.API.Sync without exposing unrestricted relay backfill:
page_events/2returns a keyset page over local stored events, equivalent to the in-process form ofSYNC-PAGE.stream_events/2lazily walks ascending pages.import_events/2imports a batch through the normal publish path, including validation, deduplication, and policy checks from the provided request context.copy_events/3composes paging and delivery to a local import target or caller-provided page function, with progress callbacks.
These helpers accept normal Nostr filters. Parrhesia also permits filter keys such as
"#dataset" and "#ref" for multi-character event tag names, which lets embedding apps
page dataset/ref-scoped event histories without scanning unrelated events.
Where to look next
- README.md for setup and the full config reference
- docs/sync.md for relay-to-relay sync semantics
- module docs under
Parrhesia.API.*for per-function behavior