Pförtner (German for "doorman") is a modular Nostr proxy library for the Deno runtime. It sits between Nostr clients and upstream relays, enabling relay administrators to apply policies that filter, rewrite, or inject data in both directions.
Create custom policies to control message flow between clients and relays. Policies can:
- Filter messages by type, content, or metadata
- Rewrite or modify messages on-the-fly
- Inject additional data into the stream
- Accept, reject, or pass messages to the next policy in the pipeline
Built-in support for NIP-42 AUTH handling:
- AUTH messages are terminated at the proxy level (never forwarded upstream)
- Track authenticated client public keys
- Stash and replay messages after successful authentication
- Flexible authorization policies based on authenticated identity
Subscribe to lifecycle events for fine-grained control:
- Connection events (
clientConnect,serverConnect, etc.) - Message events (
clientMsg,serverMsg, etc.) - Authentication events (
authSuccess,authFailed) - Protocol-specific events (
clientEvent,serverEvent,serverOk, etc.)
Pförtner is a Deno library. No installation is required—simply import it directly:
import { acceptPolicy, eventSifterPolicy, pfortnerInit } from 'https://deno.land/x/pfortner/mod.ts';- Clone the repository or create a new Deno project
- Copy
.env.sampleto.envand configure your environment variables:
cp .env.sample .env-
Edit
.envwith your settings (see Environment Variables) -
Run the example server:
deno task serveThe server will start on the port specified in APP_PORT (default: 3000).
A complete example is available in scripts/serve.ts, which demonstrates:
- NIP-42 authentication enforcement for DMs (kind 4)
- Message stashing before authentication completes
- Re-sending stashed messages after successful authentication
Configure Pförtner using these environment variables in your .env file:
| Variable | Required | Description | Example |
|---|---|---|---|
APP_PORT |
No | Server listen port | 3000 |
UPSTREAM_RELAY |
Yes | WebSocket URL of the upstream relay | wss://relay.example.com |
UPSTREAM_RAW_URL |
No | HTTP URL of the upstream relay (for relay info endpoint) | https://relay.example.com |
X_FORWARDED_FOR |
No | Whether to forward client IP addresses | true or false |
A policy is a function that examines a message and decides what to do with it:
import { type Policy } from 'https://deno.land/x/pfortner/mod.ts';
const myPolicy: Policy = (message, connectionInfo, options?) => {
// Examine the message
const [messageType, ...rest] = message;
// Make a decision
if (shouldAccept(message)) {
return { message, action: 'accept' }; // Forward and stop pipeline
} else if (shouldReject(message)) {
return {
message,
action: 'reject',
response: '["NOTICE","Access denied"]', // Optional response to client
};
} else {
return { message, action: 'next' }; // Pass to next policy
}
};'accept'— Forward the message to its destination and stop the pipeline'reject'— Drop the message (optionally send a response to the client) and stop the pipeline'next'— Pass the message to the next policy in the chain
const pfortner = pfortnerInit(UPSTREAM_RELAY, options);
// Client → Relay pipeline
pfortner.registerClientPipeline([
myClientPolicy,
[parameterizedPolicy, { option1: 'value' }],
acceptPolicy,
]);
// Relay → Client pipeline
pfortner.registerServerPipeline([
myServerPolicy,
acceptPolicy,
]);- acceptPolicy — Pass-through policy that accepts all messages
- eventSifterPolicy — Filter relay→client EVENT messages by kind (allow/deny lists)
Build the Docker image:
docker build -t pfortner .Run the container:
docker run -p 3000:3000 \
-e UPSTREAM_RELAY=wss://relay.example.com \
-e APP_PORT=3000 \
pfortnerOr use Docker Compose:
version: '3.8'
services:
pfortner:
build: .
ports:
- '3000:3000'
environment:
- UPSTREAM_RELAY=wss://relay.example.com
- APP_PORT=3000
- UPSTREAM_RAW_URL=https://relay.example.com
- X_FORWARDED_FOR=trueCreates a proxy instance.
Parameters:
upstreamAddress(string): WebSocket URL of the upstream relayoptions(object, optional):clientIp(string): Client IP addresssendAuthOnConnect(boolean): Send AUTH challenge on connectionupstreamRawAddress(string): HTTP URL for relay info endpointallowedAuthTimeDuration(number): Maximum allowed time difference for AUTH events in the past (seconds, default: 600)allowedAuthFutureTimeDuration(number): Maximum allowed time difference for AUTH events in the future (seconds, default: 60)maxAuthAttempts(number): Maximum number of AUTH attempts per connection (default: 10)idleTimeout(number): Idle timeout duration in seconds (default: 600)
Returns: An object with:
createSession(req)— Upgrade HTTP request to WebSocket sessionregisterClientPipeline(policies)— Register client→relay policiesregisterServerPipeline(policies)— Register relay→client policieson(event, handler)— Subscribe to lifecycle eventsoff(event, handler)— Unsubscribe from eventsconnectionInfo— Current connection state (auth status, pubkey, etc.)sendmessageToClient(message)— Send a message directly to the clientsendmessageToServer(message)— Send a message directly to the upstream relay
Subscribe to these events using pfortner.on(event, handler):
Connection Events:
clientConnect,clientDisconnect,clientErrorserverConnect,serverDisconnect,serverError
Authentication Events:
authSuccess(event)— Client successfully authenticatedauthFailed(reason)— Client authentication failed
Message Events:
clientMsg(message)— Any message from clientserverMsg(message)— Any message from serverclientEvent(event)— EVENT message from clientserverEvent(subscriptionId, event)— EVENT message from serverclientRequest(subscriptionId, filters)— REQ message from clientclientClose(subscriptionId)— CLOSE message from clientserverOk(eventId, accepted, message)— OK message from serverserverEose(subscriptionId)— EOSE message from serverserverClosed(subscriptionId, message)— CLOSED message from serverserverNotice(message)— NOTICE message from server
# Development server with file watching
deno task dev
# Run tests
deno test --allow-env
# Format code
deno fmt
# Lint code
deno lintPförtner is licensed under the MIT License. See LICENSE for details.