Architecture Decision Record: Relay authentication
Title: Relay authentication - who a query, subscription or broadcast authenticates as
status
accepted
Updated on 2026-09-02
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
RelayAuth
sealed class RelayAuth {
const factory RelayAuth.never(); // (url, null), never sends AUTH
const factory RelayAuth.allow(Account a); // (url, null), moves to (url, a) if refused
const factory RelayAuth.require(Account a); // (url, a) from the start
}
ndk.broadcast.broadcast(
nostrEvent: report,
auth: const RelayAuth.never(),
);
RelayAuthHandler
typedef RelayAuthHandler = Future<Account?> Function(RelayAuthRequest);
sealed class RelayAuthRequest {
final String relayUrl;
final Account? account;
final AuthRefusal? refusal; // null when asked before any refusal
}
class ReadAuthRequest extends RelayAuthRequest {
final Filter? filter;
final String? requestName;
}
class WriteAuthRequest extends RelayAuthRequest {
final Nip01Event event;
}
class AuthRefusal {
final AuthReason reason; // authRequired, restricted, blocked, rateLimited
final String message; // raw relay message
}
Configured once on the NDK config, consulted only for allow and require. With no handler,
both authenticate automatically.
Returning null means do not authenticate.