Files
parrhesia/docs/LOCAL_API.md
Steffen Beyer bbcaa00f0b
Some checks failed
CI / Test (OTP 27.2 / Elixir 1.18.2) (push) Failing after 0s
CI / Test (OTP 28.4 / Elixir 1.19.4 + E2E) (push) Failing after 0s
Release / Release Gate (push) Failing after 0s
Release / Build and publish image (push) Has been skipped
chore: Bump version to 0.7.0, 1st beta
2026-03-20 04:21:51 +01:00

5.1 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 :parrhesia as 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.Config and Parrhesia.Web.Endpoint
  • the config defaults in this repo's config/*.exs are 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.Plug from the host app when you still want Parrhesia ingress behind that same HTTPS edge.
  • Parrhesia.Web.* modules are internal runtime wiring. Treat Parrhesia.Plug as 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.

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.Events for publish, query, and count
  • Parrhesia.API.Stream for REQ-like local subscriptions
  • Parrhesia.API.Auth for event validation and NIP-98 auth parsing
  • Parrhesia.API.Admin for management operations
  • Parrhesia.API.Identity for relay-owned key management
  • Parrhesia.API.ACL for protected sync ACLs
  • Parrhesia.API.Sync for outbound relay sync management

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

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