NIP-46 — Remote Signing
Handshake and proxy signing flow between relay-connect-web, relay-api, and the Signer Device.
NIP-46 — Remote Signing
For browser-extension signing on the same machine, see NIP-07.
NIP-46 allows a Signer Device (Amber, or any compatible app) to sign Nostr events on behalf of a client, without the client ever holding the private key. The bridge relay acts as the transport layer.
Actors
| Actor | Role |
|---|---|
| Browser | Initiates the flow; renders QR; polls for pairing |
| relay-connect-web | Next.js server — proxies all /signer/* calls to the Web Server |
| relay-api (Web Server) | Orchestrates sessions in Supabase; returns nostrconnect_uri |
| Supabase | Persists nip46_sessions: pending → active → revoked |
| Signer Device | Amber or NIP-46 app; scans QR; connects to bridge |
| Bridge relay | wss:// relay used as NIP-46 transport (not a Nostr content relay) |
Handshake Flow
Session Lifecycle
INSERT (pending)
│
▼
Signer Device scans URI + connects to bridge
│
▼
Browser calls POST /signer/session/:id/complete with pairing_secret
│
▼
UPDATE status = active
│
├── DELETE /signer/session/:id → status = revoked
└── New connect flow → new session (fresh pairing_secret)Reusing an active session without re-scanning is a product choice. relay-connect-web does not implement this by default — every Connect flow starts a new session with a new pairing_secret.
nostrconnect:// URI Structure
nostrconnect://<app_pubkey>?relay=<bridge_wss>&secret=<pairing_secret>&...app_pubkey— the client's ephemeral public keyrelay— the bridgewss://where NIP-46 events will be exchangedsecret— thepairing_secretstored innip46_sessions; verified onPOST /complete
Supabase Table: relay.nip46_sessions
| Column | Type | Description |
|---|---|---|
id | uuid | PK |
provider_user_id | text | Operator's GitHub ID |
relay_config_id | uuid | FK → relay_configs.id |
app_pubkey | text | Client ephemeral pubkey |
bridge_wss | text | Transport relay URL |
pairing_secret | text | Verified on /complete |
status | text | pending | active | revoked |
Required migrations (in order):
20260324140000_relay_nip46_sessions.sql— DDL20260325130000_relay_nip46_sessions_grants.sql— PostgREST role grants
Without the grants migration, INSERT from relay-api returns 42501 (permission denied for PostgREST anon / authenticated roles).
Web Server Endpoints
| Method | Route | Description |
|---|---|---|
| GET | /signer/config | List operator relays for provider_user_id |
| POST | /signer/connect | Create session; returns nostrconnect_uri |
| GET | /signer/sessions | List sessions (polling endpoint) |
| POST | /signer/session/:id/complete | Verify secret → active |
| DELETE | /signer/session/:id | Revoke session |
Also mounted at /api/signer/* (same handlers, same auth).
Security Notes
pairing_secretis single-use per session. A new Connect flow always generates a fresh secret.- The browser never holds
RELAY_API_KEY— all/signer/*calls go through the Next.js server proxy. bridge_wssis operator-controlled. BitMacro default uses its own relay as bridge.- The Signer Device (Amber) holds the user's private key and never transmits it — only signed events travel over the bridge.