Document embedded API surface
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 + Marmot E2E) (push) Failing after 0s

This commit is contained in:
2026-03-18 20:01:12 +01:00
parent 7a43ebd395
commit 970cee2c0e
14 changed files with 311 additions and 165 deletions

View File

@@ -82,16 +82,20 @@ Configured WS/HTTP Listeners (Bandit/Plug)
## 4) OTP supervision design
`Parrhesia.Application` children (top-level):
`Parrhesia.Runtime` children (top-level):
1. `Parrhesia.Telemetry` metric definitions/reporters
2. `Parrhesia.Config` runtime config cache (ETS-backed)
3. `Parrhesia.Storage.Supervisor` adapter processes (`Repo`, pools)
4. `Parrhesia.Subscriptions.Supervisor` subscription index + fanout workers
5. `Parrhesia.Auth.Supervisor` AUTH challenge/session tracking
6. `Parrhesia.Policy.Supervisor` rate limiters / ACL caches
7. `Parrhesia.Web.Endpoint` supervises configured WS + HTTP listeners
8. `Parrhesia.Tasks.Supervisor` background jobs (expiry purge, maintenance)
2. `Parrhesia.ConnectionStats` per-listener connection/subscription counters
3. `Parrhesia.Config` runtime config cache (ETS-backed)
4. `Parrhesia.Web.EventIngestLimiter` relay-wide event ingest rate limiter
5. `Parrhesia.Web.IPEventIngestLimiter` per-IP event ingest rate limiter
6. `Parrhesia.Storage.Supervisor` adapter processes (`Repo`, pools)
7. `Parrhesia.Subscriptions.Supervisor` subscription index + fanout workers
8. `Parrhesia.Auth.Supervisor` AUTH challenge/session tracking
9. `Parrhesia.Sync.Supervisor` outbound relay sync workers
10. `Parrhesia.Policy.Supervisor` rate limiters / ACL caches
11. `Parrhesia.Web.Endpoint` supervises configured WS + HTTP listeners
12. `Parrhesia.Tasks.Supervisor` background jobs (expiry purge, maintenance)
Failure model:

View File

@@ -1,140 +0,0 @@
# Khatru-Inspired Runtime Improvements
This document collects refactoring and extension ideas learned from studying Khatru-style relay design.
It is intentionally **not** about the new public API surface or the sync ACL model. Those live in `docs/slop/LOCAL_API.md` and `docs/SYNC.md`.
The focus here is runtime shape, protocol behavior, and operator-visible relay features.
---
## 1. Why This Matters
Khatru appears mature mainly because it exposes clearer relay pipeline stages.
That gives three practical benefits:
- less policy drift between storage, websocket, and management code,
- easier feature addition without hard-coding more branches into one connection module,
- better composability for relay profiles with different trust and traffic models.
Parrhesia should borrow that clarity without copying Khatru's code-first hook model wholesale.
---
## 2. Proposed Runtime Refactors
### 2.1 Staged policy pipeline
Parrhesia should stop treating policy as one coarse `EventPolicy` module plus scattered special cases.
Recommended internal stages:
1. connection admission
2. authentication challenge and validation
3. publish/write authorization
4. query/count authorization
5. stream subscription authorization
6. negentropy authorization
7. response shaping
8. broadcast/fanout suppression
This is an internal runtime refactor. It does not imply a new public API.
### 2.2 Richer internal request context
The runtime should carry a structured request context through all stages.
Useful fields:
- authenticated pubkeys
- caller kind
- remote IP
- subscription id
- peer id
- negentropy session flag
- internal-call flag
This reduces ad-hoc branching and makes audit/telemetry more coherent.
### 2.3 Separate policy from storage presence tables
Moderation state should remain data.
Runtime enforcement should be a first-class layer that consumes that data, not a side effect of whether a table exists.
This is especially important for:
- blocked IP enforcement,
- pubkey allowlists,
- future kind- or tag-scoped restrictions.
---
## 3. Protocol and Relay Features
### 3.1 Real COUNT sketches
Parrhesia currently returns a synthetic `hll` payload for NIP-45-style count responses.
If approximate count exchange matters, implement a real reusable HLL sketch path instead of hashing `filters + count`.
### 3.2 Relay identity in NIP-11
Once Parrhesia owns a stable server identity, NIP-11 should expose the relay pubkey instead of returning `nil`.
This is useful beyond sync:
- operator visibility,
- relay fingerprinting,
- future trust tooling.
### 3.3 Connection-level IP enforcement
Blocked IP support should be enforced on actual connection admission, not only stored in management tables.
This should happen early, before expensive protocol handling.
### 3.4 Better response shaping
Introduce a narrow internal response shaping layer for cases where returned events or counts need controlled rewriting or suppression.
Examples:
- hide fields for specific relay profiles,
- suppress rebroadcast of locally-ingested remote sync traffic,
- shape relay notices consistently.
This should stay narrow and deterministic. It should not become arbitrary app semantics.
---
## 4. Suggested Extension Points
These should be internal runtime seams, not necessarily public interfaces:
- `ConnectionPolicy`
- `AuthPolicy`
- `ReadPolicy`
- `WritePolicy`
- `NegentropyPolicy`
- `ResponsePolicy`
- `BroadcastPolicy`
They may initially be plain modules with well-defined callbacks or functions.
The point is not pluggability for its own sake. The point is to make policy stages explicit and testable.
---
## 5. Near-Term Priority
Recommended order:
1. enforce blocked IPs and any future connection-gating on the real connection path
2. split the current websocket flow into explicit read/write/negentropy policy stages
3. enrich runtime request context and telemetry metadata
4. expose relay pubkey in NIP-11 once identity lands
5. replace fake HLL payloads with a real approximate-count implementation if NIP-45 support matters operationally
This keeps the runtime improvements incremental and independent from the ongoing API and ACL implementation.

147
docs/LOCAL_API.md Normal file
View File

@@ -0,0 +1,147 @@
# 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 still alpha, so treat the API
as usable but not yet frozen.
## 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.*`
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:
```elixir
defp deps do
[
{:parrhesia, path: "../parrhesia"}
]
end
```
Configure the runtime in your host app. At minimum you should carry over:
```elixir
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:
- Set `listeners: %{}` if you only want the in-process API and no HTTP/WebSocket ingress.
- If you do want ingress, copy the listener shape from the config reference in
[README.md](../README.md).
- Production runtime overrides still use the `PARRHESIA_*` environment variables described in
[README.md](../README.md).
Migrate before serving traffic:
```elixir
Parrhesia.Release.migrate()
```
In development, `mix ecto.migrate -r Parrhesia.Repo` works too.
## 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`:
```elixir
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.
```elixir
%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
```elixir
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](../README.md) for setup and the full config reference
- [docs/SYNC.md](./SYNC.md) for relay-to-relay sync semantics
- module docs under `Parrhesia.API.*` for per-function behavior