Architecture Decision Record: Relay authentication
Title: Who a query, subscription, broadcast or Blossom operation authenticates as
status
accepted
Updated on 2026-09-25
contributors
-
Main contributor(s): nogringo
-
Reviewer(s): frnandu, 1leo
-
Final decision made by: frnandu, 1leo, nogringo
Context and Problem Statement
A connection carries at most one identity, immutable for its whole lifetime
(RelayConnectionKey). The caller-facing half is missing: authenticateAs: List<Account>?
cannot say "never be attributable for this request", nor "authenticate before asking".
And the absent case is the leaky one. With no authenticateAs, a request that meets
auth-required falls back to the logged account, so the relay decides when an identity is
revealed.
Main Proposal
AuthPolicy
sealed class AuthPolicy {
const factory AuthPolicy.never(); // (url, null), never sends AUTH
const factory AuthPolicy.allow(Account a); // (url, null), moves to (url, a) if refused
const factory AuthPolicy.require(Account a); // (url, a) from the start
}
ndk.broadcast.broadcast(
nostrEvent: report,
auth: const AuthPolicy.never(),
);
AuthHandler
typedef AuthHandler = Future<bool> Function(String url, String pubkey);
Configured once on the NDK config, asked before a pubkey authenticates on a relay or a Blossom server.
The default account is the logged one, or a broadcast's author when it is registered. A Blossom read has none and stays anonymous.
require asks before sending, one URL at a time: a request on three relays accepted on two
goes out on those two only. So does a Blossom write without auth, which authorises by
default. The rest asks after a refusal.
A relay is asked when a connection bound to that pubkey opens, a Blossom server once per operation.