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.