Skip to content

Nostreon/relay-auth

Repository files navigation

relay-auth

Access control service for gated Nostr relays. Verifies whether an authenticated pubkey is allowed to access a relay, based on NIP-63 membership events and/or a database of active subscriptions.

Built as a reference implementation for two in-flight NIPs:

The /check-access HTTP API in this service is an implementation detail, not an interoperability surface. It's what a relay operator calls internally after NIP-42 auth to make a yes/no decision. The client-facing interoperability surface is the NIP-11 access_control field (see below), which this reference relay advertises.

How it works

Client → (NIP-42 AUTH) → Relay → (POST /check-access) → relay-auth ─┬─▶ Database (subscriptions)
                                                                     └─▶ Gated relay (kind 1163 events)
  1. Client connects to a gated relay and completes NIP-42 authentication
  2. Relay extracts the authenticated pubkey and calls this service
  3. This service checks two sources in parallel:
    • NIP-63 path: queries the gated relay for kind 1163 membership events tagged with the pubkey, then validates the NIP-40 expiration tag
    • Database path: looks for an active subscription row in Supabase
  4. Relay grants or denies access based on the response

The two paths are independent. The NIP-63 path is the protocol-native fallback that lets any payment provider grant access by publishing kind 1163 events, with no shared database integration required. The database path is for platforms that already track subscriptions internally and want richer metadata (which tier, which creator, etc.).

Why both paths?

  • NIP-63 alone is portable but lossy. You only know "this pubkey has a valid membership until time T." You don't know what they're subscribed to.
  • Database alone locks the relay to one platform. Other Nostr clients/apps can't grant access.
  • Both together means a platform can run its own DB for richer features while still respecting kind 1163 events from third parties, and other relays can adopt the same service with just the NIP-63 path enabled.

NIP-63 events

A kind 1163 membership event looks like:

{
  "kind": 1163,
  "pubkey": "<payment-provider>",
  "tags": [
    ["p", "<subscriber-pubkey>"],
    ["expiration", "1735689600"]
  ],
  "content": ""
}
  • The p tag identifies the subscriber
  • The expiration tag (NIP-40) carries the unix timestamp when access ends
  • The event is signed by whoever verified the payment (a platform, a Lightning service, etc.)

When this service receives a /check-access request, it queries the gated relay with {"kinds":[1163], "#p":[<subscriber>], "limit":10} and looks for any event whose expiration is in the future.

NIP-11 access_control (client-facing discovery)

The gated relay in front of this service advertises its access requirements via the access_control field proposed in NIP-11 PR #2318. Clients can GET / on the relay with Accept: application/nostr+json before connecting and render an actionable UI based on the response.

{
  "name": "Premium Relay",
  "supported_nips": [1, 11, 42, 63],
  "access_control": {
    "authentication": "required",
    "permissions": [
      { "action": "read",  "kinds": [1163, 30402, 30403] },
      { "action": "write", "kinds": [1, 1163, 30023] }
    ],
    "description": "Active subscription required for access to premium content.",
    "info_url": "https://example.com/subscribe"
  },
  // other fields...
}

A full runnable example of this document is committed at examples/nip11-example.json. When this service is running locally you can also fetch it live:

curl http://localhost:3003/nip11-example
curl -H "Accept: application/nostr+json" http://localhost:3003/

Both endpoints return the same JSON with the Content-Type: application/nostr+json header, matching what a conforming relay would serve.

When the relay denies a gated action, it uses a human-meaningful reason after the restricted: prefix so clients can render it directly:

["CLOSED", "<sub-id>", "restricted: no active subscription"]
["OK", "<event-id>", false, "restricted: write access requires membership"]

The field describes capability advertisement, not runtime policy. It tells a client that read/write is gated for specific kinds and where a non-member can go to obtain access. How the relay actually decides (database lookup, kind 1163 query, token check, etc.) stays an implementation detail — in this reference, that's what the /check-access endpoint below handles.

API

POST /check-access

// request
{ "pubkey": "abc123...", "relay": "wss://premium.example.com" }

// response
{ "allowed": true, "reason": "NIP-63 membership", "expires_at": 1735689600 }

reason is one of:

  • "active subscription" — matched a row in the database
  • "NIP-63 membership" — matched a kind 1163 event
  • "no active subscription" — neither source had a valid record

POST /accessible-creators

// request
{ "pubkey": "abc123..." }

// response
{ "creators": ["def456...", "ghi789..."] }

Returns the list of creator pubkeys this user has paid for. Used by relays that host content from multiple creators and want to filter events per-subscriber. (This endpoint reads from the database only; kind 1163 events don't carry creator identity in the standard tag set.)

GET /health

Health check.

Setup

Requirements

  • Node.js 22+
  • A WebSocket-reachable Nostr relay where kind 1163 events live
  • (Optional) A Postgres/Supabase database with subscriptions and tiers tables for the database path

Database schema (optional)

If you're using the database path, the service expects:

create table tiers (
  id uuid primary key default gen_random_uuid(),
  creator_pubkey text not null,
  name text not null,
  price_cents integer not null,
  cadence text not null
);

create table subscriptions (
  id uuid primary key default gen_random_uuid(),
  subscriber_pubkey text not null,
  tier_id uuid references tiers(id),
  status text not null,
  expires_at timestamptz not null
);

If you only want the NIP-63 path, leave SUPABASE_URL and SUPABASE_SERVICE_KEY unset and the database checks will return "no record." The NIP-63 check still runs.

Run locally

cp .env.example .env
# Edit .env

npm install
npm run dev

Run with Docker

cp .env.example .env
docker compose up

This starts the access control service, a strfry gated relay (which holds the kind 1163 events), and an nginx reverse proxy.

Configuration

Variable Description Default
GATED_RELAY_URL WebSocket URL of the relay where kind 1163 events live ws://strfry-gated:7777
SUPABASE_URL Supabase API URL (optional, for database path)
SUPABASE_SERVICE_KEY Supabase service role key (optional)
PORT Server port 3003
HOST Bind address 0.0.0.0

Adapting to your stack

The two paths are independent, so you can use this service in three ways:

  1. NIP-63 only — point GATED_RELAY_URL at the relay that holds your membership events, leave Supabase unset. Any payment provider that publishes kind 1163 events grants access.
  2. Database only — point SUPABASE_URL at your DB, point GATED_RELAY_URL at an empty relay (or a non-existent one; the WebSocket connection failure is handled gracefully).
  3. Both — the default. Database for your platform's native subscriptions, NIP-63 for everything else.

To swap Supabase for another database, replace the createClient and queries in src/access.ts. The HTTP API stays the same.

Architecture

                    ┌─────────────────┐
                    │   nginx proxy   │
                    └────┬───────┬────┘
                         │       │
              ┌──────────┘       └──────────┐
              │                             │
     ┌────────▼────────┐          ┌─────────▼────────┐
     │  strfry (gated) │◀─────────│   relay-auth      │
     │  NIP-42 auth    │  REQ     │   (this service)  │
     │  kind 1163      │  1163    └────────┬──────────┘
     └────────┬────────┘                   │
              │                            ▼
              │ POST /check-access  ┌──────────────────┐
              └────────────────────▶│   Database        │
                                    │   (subscriptions) │
                                    └───────────────────┘

Related

License

MIT

About

NIP-42 relay access control service — verifies subscription status for gated Nostr relays

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors