Documentation
¶
Overview ¶
Package mocrelay implements a Nostr relay as a composable Go library.
mocrelay provides a middleware-composable architecture for building Nostr relays. The core abstraction is the Handler interface, which processes a single WebSocket connection's lifetime. Handlers can be composed using Middleware to add features like authentication, rate limiting, and content filtering.
Architecture ¶
The key types form a layered architecture:
- Relay serves HTTP/WebSocket and manages connection lifecycles
- Handler processes messages for a single connection
- Middleware wraps a Handler to add cross-cutting concerns
- Storage persists and queries events
- Router routes events between connected clients in real-time
Handlers ¶
mocrelay provides several built-in handlers:
- NewStorageHandler wraps a Storage to handle EVENT and REQ messages
- NewRouterHandler wraps a Router to route events between clients
- NewMergeHandler runs multiple handlers in parallel and merges responses
- NewNopHandler is a minimal handler for testing
Most handlers can be implemented using SimpleHandlerBase, which provides a simpler message-at-a-time interface instead of managing channels directly.
Middleware ¶
Middleware is built on the SimpleMiddlewareBase interface. Multiple middleware bases are composed into a single pipeline via NewSimpleMiddleware:
handler := NewSimpleMiddleware(
NewMaxSubscriptionsMiddlewareBase(20),
NewMaxLimitMiddlewareBase(500, 100),
NewKindDenylistMiddlewareBase([]int64{4, 1059}),
)(innerHandler)
Built-in middleware corresponds to NIP-11 limitation and retention fields, providing a declarative way to configure relay policies.
Storage ¶
The Storage interface uses Go iterators (iter.Seq) for streaming query results:
events, errFn, closeFn := storage.Query(ctx, filters)
defer closeFn()
for event := range events {
// process event
}
if err := errFn(); err != nil {
// handle error
}
InMemoryStorage is provided for testing. For production use, see PebbleStorage which wraps a caller-owned CockroachDB Pebble LSM-tree *pebble.DB. Full-text search (NIP-50) is available via BleveIndex, which wraps a caller-owned bleve.Index.
Typical usage ¶
A typical relay combines storage, routing, middleware, and metrics. The caller owns the underlying pebble.DB (and bleve.Index, if used), which keeps pebble.DB.Metrics and database lifecycle in the caller's hands:
db, _ := pebble.Open("/path/to/db", nil)
defer db.Close()
storage := NewPebbleStorage(db, nil)
router := NewRouter(nil)
handler := NewMergeHandler(
[]Handler{
NewStorageHandler(storage, nil),
NewRouterHandler(router),
},
nil,
)
handler = NewSimpleMiddleware(
NewMaxSubscriptionsMiddlewareBase(20),
NewMaxLimitMiddlewareBase(500, 100),
)(handler)
relay := NewRelay(handler, nil)
http.ListenAndServe(":7447", relay)
Index ¶
- Constants
- func BuildIndexMapping() *mapping.IndexMappingImpl
- func ConnIDFromContext(ctx context.Context) string
- func ContextWithLogger(ctx context.Context, logger *slog.Logger) context.Context
- func LoggerFromContext(ctx context.Context) *slog.Logger
- type AuthMiddlewareOptions
- type BleveIndex
- type BleveIndexOptions
- type ClientMsg
- type CompositeStorage
- type CompositeStorageOptions
- type Event
- func (e *Event) Address() string
- func (e *Event) EventType() EventType
- func (e Event) MarshalJSONTo(enc *jsontext.Encoder) error
- func (e *Event) Serialize() ([]byte, error)
- func (e *Event) UnmarshalJSONFrom(dec *jsontext.Decoder) error
- func (e *Event) Valid() bool
- func (e *Event) Verify() (bool, error)
- type EventType
- type Handler
- type HandlerFunc
- type InMemoryStorage
- type MergeHandlerOptions
- type MetricsStorage
- type Middleware
- type PebbleStorage
- type PebbleStorageOptions
- type Relay
- type RelayFee
- type RelayFees
- type RelayInfo
- type RelayLimitation
- type RelayOptions
- type RelayPublicationFee
- type RelayRetention
- type RelaySubscriptionFee
- type ReqFilter
- type RestrictedWritesMode
- type Router
- type RouterOptions
- type SearchIndex
- type ServerMsg
- func NewServerAuthMsg(challenge string) *ServerMsg
- func NewServerClosedMsg(subID string, message string) *ServerMsg
- func NewServerCountMsg(subID string, count uint64, approximate *bool) *ServerMsg
- func NewServerEOSEMsg(subID string) *ServerMsg
- func NewServerEventMsg(subID string, event *Event) *ServerMsg
- func NewServerNoticeMsg(message string) *ServerMsg
- func NewServerOKMsg(eventID string, accepted bool, message string) *ServerMsg
- type SimpleHandlerBase
- type SimpleMiddlewareBase
- func NewAuthMiddlewareBase(relayURL string, opts *AuthMiddlewareOptions) SimpleMiddlewareBase
- func NewAuthRateLimitMiddlewareBase(rate float64, burst int) SimpleMiddlewareBase
- func NewCountRateLimitMiddlewareBase(rate float64, burst int) SimpleMiddlewareBase
- func NewCreatedAtLimitsMiddlewareBase(lowerLimit, upperLimit int64) SimpleMiddlewareBase
- func NewEventRateLimitMiddlewareBase(rate float64, burst int) SimpleMiddlewareBase
- func NewExpirationMiddlewareBase() SimpleMiddlewareBase
- func NewKindAllowlistMiddlewareBase(kinds []int64) SimpleMiddlewareBase
- func NewKindDenylistMiddlewareBase(kinds []int64) SimpleMiddlewareBase
- func NewMaxContentLengthMiddlewareBase(maxLen int) SimpleMiddlewareBase
- func NewMaxEventTagsMiddlewareBase(maxTags int) SimpleMiddlewareBase
- func NewMaxLimitMiddlewareBase(maxLimit, defaultLimit int64) SimpleMiddlewareBase
- func NewMaxSubidLengthMiddlewareBase(maxLen int) SimpleMiddlewareBase
- func NewMaxSubscriptionsMiddlewareBase(maxSubs int) SimpleMiddlewareBase
- func NewMinPowDifficultyMiddlewareBase(minDifficulty int, checkCommitment bool) SimpleMiddlewareBase
- func NewProtectedEventsMiddlewareBase() SimpleMiddlewareBase
- func NewReqRateLimitMiddlewareBase(rate float64, burst int) SimpleMiddlewareBase
- func NewRestrictedWritesMiddlewareBase(mode RestrictedWritesMode, pubkeys []string) SimpleMiddlewareBase
- type Storage
- type StorageHandlerOptions
- type Tag
Constants ¶
const ( // Client-to-relay message types. MsgTypeEvent = "EVENT" // Submit an event MsgTypeReq = "REQ" // Subscribe with filters MsgTypeClose = "CLOSE" // Unsubscribe MsgTypeAuth = "AUTH" // NIP-42 authentication MsgTypeCount = "COUNT" // NIP-45 event counting // Relay-to-client message types. MsgTypeOK = "OK" // Event submission result MsgTypeEOSE = "EOSE" // End of stored events MsgTypeClosed = "CLOSED" // Subscription closed by relay MsgTypeNotice = "NOTICE" // Human-readable message )
Nostr message type labels as defined in NIP-01.
const DefaultMergeHandlerBroadcastTimeout = 30 * time.Second
DefaultMergeHandlerBroadcastTimeout is the default per-child broadcast timeout used by the merge handler when [MergeHandlerOptions.BroadcastTimeout] is zero.
const DefaultStorageHandlerSlowQueryThreshold = 1 * time.Second
DefaultStorageHandlerSlowQueryThreshold is the default threshold for logging a slow REQ / COUNT subscription when [StorageHandlerOptions.SlowQueryThreshold] is zero.
const MergeHandlerOKLostHandlerMessage = "error: a downstream handler is unavailable, please retry"
MergeHandlerOKLostHandlerMessage is the OK message text returned to the client when the merge handler cannot account for every downstream handler's verdict on an EVENT — typically because a child timed out on broadcast or exited before responding. The OK is forced to accepted=false so a well-behaved client retries the event, which can then be re-fanned to any downstream that has since recovered.
Variables ¶
This section is empty.
Functions ¶
func BuildIndexMapping ¶ added in v0.6.0
func BuildIndexMapping() *mapping.IndexMappingImpl
BuildIndexMapping returns the Bleve mapping.IndexMappingImpl that BleveIndex expects. Use it when opening a bleve.Index to pass to NewBleveIndex:
idx, _ := bleve.NewMemOnly(mocrelay.BuildIndexMapping()) searchIndex := mocrelay.NewBleveIndex(idx, nil)
It configures:
- "content" as a text field analyzed with the CJK analyzer (bigram tokenization, covers Japanese / Chinese / Korean).
- "id" as stored-only (indexed=false) so hits can be mapped back to event IDs without bloating the index.
- "pubkey" as a keyword (exact match) field.
- "kind" and "created_at" as numeric fields.
Callers who need a customized mapping can build on top of the returned value, but Index / Search / Delete rely on the document shape above.
func ConnIDFromContext ¶
ConnIDFromContext returns the connection ID from the context. If no connection ID is found, it returns an empty string.
func ContextWithLogger ¶
ContextWithLogger returns a new context with the given logger.
Types ¶
type AuthMiddlewareOptions ¶
type AuthMiddlewareOptions struct {
// CreatedAtTolerance is the maximum age of auth events.
// Default: 10 minutes (as recommended by NIP-42)
CreatedAtTolerance time.Duration
}
AuthMiddlewareOptions configures the middleware returned by NewAuthMiddlewareBase. All fields are optional; the zero value gives sensible defaults.
Metrics are not part of this struct. Auth metrics are constructed by Relay from [RelayOptions.Registerer] and injected into the request context; the auth middleware reads them internally without holding a direct reference, mirroring the rejection counter / Logger pattern.
type BleveIndex ¶
type BleveIndex struct {
// contains filtered or unexported fields
}
BleveIndex implements SearchIndex using Bleve with CJK analyzer.
The bleve.Index is owned by the caller: BleveIndex does not open or close it. Callers should create the index with BuildIndexMapping (or a customized mapping built on top of it) so the document schema matches what Index / Search / Delete expect, then close the underlying index when done. BleveIndex itself has no Close method.
func NewBleveIndex ¶
func NewBleveIndex(idx bleve.Index, opts *BleveIndexOptions) *BleveIndex
NewBleveIndex wraps an already-open bleve.Index as a SearchIndex.
The caller retains ownership of idx: they are responsible for opening it (typically with BuildIndexMapping) and for closing it when done. BleveIndex itself has no Close method.
opts can be nil; there are currently no configurable options.
func (*BleveIndex) Delete ¶
func (b *BleveIndex) Delete(ctx context.Context, eventID string) error
Delete implements SearchIndex.Delete.
type BleveIndexOptions ¶
type BleveIndexOptions struct{}
BleveIndexOptions is reserved for future BleveIndex configuration. It is currently empty but exists so that adding options later is not a breaking API change. Pass nil for now.
type ClientMsg ¶
type ClientMsg struct {
Type string
// EVENT, AUTH: the event being submitted
Event *Event
// REQ, CLOSE, COUNT: subscription identifier
SubscriptionID string
// REQ, COUNT: filters for the subscription
Filters []*ReqFilter
}
ClientMsg represents a message from client to relay. This is a union type - check Type field to determine which fields are valid.
func ParseClientMsg ¶
ParseClientMsg parses a JSON message from client.
type CompositeStorage ¶
type CompositeStorage struct {
// contains filtered or unexported fields
}
CompositeStorage combines a primary Storage with a SearchIndex. The primary storage (e.g., PebbleStorage) is the source of truth. The search index (e.g., BleveIndex) provides full-text search capabilities.
Query behavior:
- If filter has Search field: use SearchIndex to get event IDs, then fetch from primary
- Otherwise: use primary storage directly
Store behavior:
- Store in primary first (source of truth)
- If successful, index in search index (best effort)
Both the search path and the index path swallow their backend's errors from the caller's perspective (search falls back to primary; index is fire-and-forget). Wire [CompositeStorageOptions.Metrics] to make those failures visible via [CompositeStorageMetrics.SearchErrors] / [CompositeStorageMetrics.IndexErrors]; a structured Warn log is emitted alongside each failure via LoggerFromContext.
func NewCompositeStorage ¶
func NewCompositeStorage(primary Storage, search SearchIndex, opts *CompositeStorageOptions) *CompositeStorage
NewCompositeStorage creates a new CompositeStorage. primary is the source of truth for all data. search provides full-text search capabilities (can be nil to disable search). opts can be nil; see CompositeStorageOptions for configurable options.
type CompositeStorageOptions ¶ added in v0.7.0
type CompositeStorageOptions struct {
// Registerer is the Prometheus registry that CompositeStorage's
// internal search / indexing counters are registered with
// (mocrelay_search_total, mocrelay_search_errors_total,
// mocrelay_index_total, mocrelay_index_errors_total). If nil, no
// metrics are collected (zero runtime cost at each instrument site).
//
// The underlying Bleve / SearchIndex resource state itself (doc count,
// index size, batch statistics) lives on the caller's [bleve.Index]
// and should be collected directly from idx.StatsMap() by the caller.
Registerer prometheus.Registerer
}
CompositeStorageOptions configures CompositeStorage. Pass nil for defaults.
type Event ¶
type Event struct {
ID string `json:"id"`
Pubkey string `json:"pubkey"`
CreatedAt time.Time `json:"created_at,format:unix"`
Kind int64 `json:"kind"`
Tags []Tag `json:"tags"`
Content string `json:"content"`
Sig string `json:"sig"`
}
Event represents a Nostr event as defined in NIP-01.
func (*Event) Address ¶
Address returns the address for replaceable/addressable events. Format: "kind:pubkey:" for replaceable, "kind:pubkey:d-tag" for addressable. Returns empty string for regular/ephemeral events.
func (Event) MarshalJSONTo ¶
MarshalJSONTo implements json.MarshalerTo to ensure NIP-01 compliant output. Tags is always marshaled as [] (never null).
func (*Event) Serialize ¶
Serialize returns the canonical JSON representation for signing/verification. Format: [0, pubkey, created_at, kind, tags, content]
func (*Event) UnmarshalJSONFrom ¶
UnmarshalJSONFrom implements json.UnmarshalerFrom to validate field count and field value types. Nostr events must have exactly 7 fields, all non-null.
type EventType ¶
type EventType int
EventType represents the category of a Nostr event based on its kind.
const ( // EventTypeRegular represents standard events (kind 1, 2, 4-44, 1000-9999). // All regular events are stored. EventTypeRegular EventType = iota // EventTypeReplaceable represents events where only the latest is kept per (pubkey, kind). // Kinds: 0, 3, 10000-19999. EventTypeReplaceable // EventTypeEphemeral represents events that are not stored. // Kinds: 20000-29999. EventTypeEphemeral // EventTypeAddressable represents events where only the latest is kept per (pubkey, kind, d-tag). // Kinds: 30000-39999. EventTypeAddressable )
type Handler ¶
type Handler interface {
ServeNostr(ctx context.Context, send chan<- *ServerMsg, recv <-chan *ClientMsg) error
}
Handler is the interface for processing Nostr client connections. It receives client messages and sends server messages through channels.
The handler runs for the lifetime of a single WebSocket connection. When the handler returns:
- error == nil: normal termination (client disconnected gracefully)
- error != nil: abnormal termination (connection will be closed, error logged)
The decision to shut down the entire relay is NOT the handler's responsibility. That should be handled at the HTTP handler level.
func NewMergeHandler ¶
func NewMergeHandler(handlers []Handler, opts *MergeHandlerOptions) Handler
NewMergeHandler returns a Handler that fans messages out to every handler in handlers and merges their responses. Pass nil for opts to use defaults.
func NewNopHandler ¶
func NewNopHandler() Handler
NewNopHandler returns a Handler that accepts every EVENT and immediately returns EOSE for every REQ. It is useful for connection testing and as a starting point for custom handlers.
func NewRouterHandler ¶
NewRouterHandler returns a Handler backed by router that broadcasts events between clients. The connection is registered with router on start and unregistered on return, so subscription lifecycle is managed automatically.
func NewSimpleHandler ¶
func NewSimpleHandler(base SimpleHandlerBase) Handler
NewSimpleHandler wraps a SimpleHandlerBase as a Handler. See SimpleHandlerBase for the lifecycle contract.
func NewStorageHandler ¶
func NewStorageHandler(storage Storage, opts *StorageHandlerOptions) Handler
NewStorageHandler returns a Handler that serves EVENT and REQ messages against storage. Pass nil for opts to use defaults.
Behavior:
- EVENT: Store the event, return OK
- REQ: Query the storage, return EVENT messages + EOSE
- CLOSE: No-op — the handler does not manage subscriptions; pair with NewRouterHandler via NewMergeHandler to deliver real-time events
- COUNT: Query the storage, return COUNT with the count
Once EOSE is sent for a REQ, the handler's job is done for that subscription.
type HandlerFunc ¶
HandlerFunc is an adapter to allow the use of ordinary functions as Handler.
func (HandlerFunc) ServeNostr ¶
func (f HandlerFunc) ServeNostr(ctx context.Context, send chan<- *ServerMsg, recv <-chan *ClientMsg) error
ServeNostr calls f(ctx, send, recv).
type InMemoryStorage ¶
type InMemoryStorage struct {
// contains filtered or unexported fields
}
InMemoryStorage is a simple in-memory implementation of Storage. This is a "brute force" implementation: O(n) for most operations. Suitable for testing and small datasets.
func NewInMemoryStorage ¶
func NewInMemoryStorage() *InMemoryStorage
NewInMemoryStorage creates a new in-memory storage.
type MergeHandlerOptions ¶
type MergeHandlerOptions struct {
// BroadcastTimeout caps how long MergeHandler waits when forwarding a
// control message (REQ / COUNT / CLOSE) to a single child handler whose
// recv channel is full. EVENT messages are always best-effort and are
// not affected by this timeout (a dropped EVENT is signalled to the
// client via OK accepted=false, prompting a retry).
//
// When the timeout fires, the slow child is treated as dead: any pending
// OK / EOSE / COUNT it owed is advanced as if the handler had exited.
// The remaining healthy handlers continue to serve the client.
//
// A zero value selects [DefaultMergeHandlerBroadcastTimeout] (30s).
BroadcastTimeout time.Duration
// Registerer is the Prometheus registry that merge-handler saturation
// and error signals are registered with (mocrelay_merge_lost_children_total,
// mocrelay_merge_broadcast_timeouts_total,
// mocrelay_merge_event_drops_total). If nil, no metrics are collected.
Registerer prometheus.Registerer
}
MergeHandlerOptions configures a merge handler returned by NewMergeHandler.
All fields are optional. A nil *MergeHandlerOptions is equivalent to a zero-valued MergeHandlerOptions and means "use defaults for everything".
type MetricsStorage ¶
type MetricsStorage struct {
// contains filtered or unexported fields
}
MetricsStorage wraps a Storage and collects Prometheus metrics for each Store / Query call (mocrelay_events_stored_total, mocrelay_store_duration_seconds, mocrelay_query_duration_seconds, mocrelay_store_errors_total, mocrelay_query_errors_total).
Because mocrelay's Storage interface has multiple implementations (InMemory / Pebble / Composite), MetricsStorage is a decorator rather than an Options-style constructor: wrap whatever Storage you want to measure.
func NewMetricsStorage ¶
func NewMetricsStorage(storage Storage, reg prometheus.Registerer) *MetricsStorage
NewMetricsStorage creates a MetricsStorage wrapping storage. Metrics are registered against reg; if reg is nil, all metric calls no-op and the decorator is effectively a pass-through (no registration happens and no Prometheus collectors are created).
type Middleware ¶
Middleware wraps a Handler to add functionality.
func NewSimpleMiddleware ¶
func NewSimpleMiddleware(bases ...SimpleMiddlewareBase) Middleware
NewSimpleMiddleware creates a Middleware from one or more SimpleMiddlewareBase.
When multiple bases are provided, they are composed into a single pipeline that uses only one goroutine, regardless of the number of bases. Bases are listed in outermost-first order (like alice.New):
NewSimpleMiddleware(logging, auth)(handler) // message flow: client → logging → auth → handler
Internally uses a unified select loop with nil channel pattern to handle both recv and send directions in a single goroutine, avoiding deadlocks without requiring buffered channels.
type PebbleStorage ¶
type PebbleStorage struct {
// contains filtered or unexported fields
}
PebbleStorage implements Storage using Pebble (LSM-tree based KV store).
The *pebble.DB is owned by the caller: PebbleStorage does not open or close the database. The caller is responsible for opening the DB with whatever pebble.Options are appropriate (cache size, bloom filters, event listeners, etc.) and for closing it when done. This lets the caller expose pebble.DB.Metrics directly and pin their own Pebble version independent of mocrelay's go.mod.
func NewPebbleStorage ¶
func NewPebbleStorage(db *pebble.DB, opts *PebbleStorageOptions) *PebbleStorage
NewPebbleStorage wraps an already-open *pebble.DB as a Storage.
The caller retains ownership of db: they are responsible for opening it (with their own pebble.Options) and for calling db.Close() when done. PebbleStorage itself has no Close method.
opts can be nil; there are currently no configurable options.
type PebbleStorageOptions ¶
type PebbleStorageOptions struct{}
PebbleStorageOptions is reserved for future PebbleStorage configuration. It is currently empty but exists so that adding options later is not a breaking API change. Pass nil for now.
type Relay ¶
type Relay struct {
// contains filtered or unexported fields
}
Relay wraps a Handler to serve it over HTTP/WebSocket. It implements http.Handler.
func NewRelay ¶
func NewRelay(handler Handler, opts *RelayOptions) *Relay
NewRelay creates a new Relay with the given handler. opts can be nil for default options.
func (*Relay) ServeHTTP ¶
func (r *Relay) ServeHTTP(w http.ResponseWriter, req *http.Request)
ServeHTTP implements http.Handler.
type RelayFee ¶
type RelayFee struct {
Amount int64 `json:"amount"`
Unit string `json:"unit"` // e.g., "msats", "sats"
}
RelayFee represents a basic fee amount.
type RelayFees ¶
type RelayFees struct {
Admission []*RelayFee `json:"admission,omitzero"`
Subscription []*RelaySubscriptionFee `json:"subscription,omitzero"`
Publication []*RelayPublicationFee `json:"publication,omitzero"`
}
RelayFees represents NIP-11 fee schedules.
type RelayInfo ¶
type RelayInfo struct {
// Basic information
Name string `json:"name,omitzero"`
Description string `json:"description,omitzero"`
Banner string `json:"banner,omitzero"`
Icon string `json:"icon,omitzero"`
Pubkey string `json:"pubkey,omitzero"` // 32-byte hex, admin contact
Self string `json:"self,omitzero"` // 32-byte hex, relay's own pubkey
Contact string `json:"contact,omitzero"`
SupportedNIPs []int `json:"supported_nips,omitzero"`
Software string `json:"software,omitzero"`
Version string `json:"version,omitzero"`
PrivacyPolicy string `json:"privacy_policy,omitzero"`
TermsOfService string `json:"terms_of_service,omitzero"`
// Server limitations
Limitation *RelayLimitation `json:"limitation,omitzero"`
// Event retention policies
// TODO: kinds field has union type: int | [int, int]
// For now, use a simplified representation
Retention []*RelayRetention `json:"retention,omitzero"`
// Content limitations
RelayCountries []string `json:"relay_countries,omitzero"` // ISO 3166-1 alpha-2
// Community preferences
LanguageTags []string `json:"language_tags,omitzero"` // IETF language tags
Tags []string `json:"tags,omitzero"` // e.g., "sfw-only", "bitcoin-only"
PostingPolicy string `json:"posting_policy,omitzero"`
// Pay-to-Relay
PaymentsURL string `json:"payments_url,omitzero"`
Fees *RelayFees `json:"fees,omitzero"`
}
RelayInfo represents NIP-11 Relay Information Document. All fields are optional and omitted from JSON when zero-valued.
type RelayLimitation ¶
type RelayLimitation struct {
MaxMessageLength int64 `json:"max_message_length,omitzero"`
MaxSubscriptions int `json:"max_subscriptions,omitzero"`
MaxLimit int `json:"max_limit,omitzero"`
MaxSubidLength int `json:"max_subid_length,omitzero"`
MaxEventTags int `json:"max_event_tags,omitzero"`
MaxContentLength int `json:"max_content_length,omitzero"`
MinPowDifficulty int `json:"min_pow_difficulty,omitzero"`
AuthRequired bool `json:"auth_required,omitzero"`
PaymentRequired bool `json:"payment_required,omitzero"`
RestrictedWrites bool `json:"restricted_writes,omitzero"`
// created_at limits in seconds
// Lower: events older than (now - lower_limit) are rejected
// Upper: events newer than (now + upper_limit) are rejected
CreatedAtLowerLimit int64 `json:"created_at_lower_limit,omitzero"`
CreatedAtUpperLimit int64 `json:"created_at_upper_limit,omitzero"`
DefaultLimit int `json:"default_limit,omitzero"`
}
RelayLimitation represents NIP-11 limitation object.
type RelayOptions ¶
type RelayOptions struct {
// Logger is the structured logger for connection and error events.
// Default: slog.Default()
Logger *slog.Logger
// MaxMessageLength is the maximum size of a WebSocket message in bytes.
// Set to a negative value to disable the limit.
// Default: 100_000 (100 KB)
MaxMessageLength int64
// PingInterval is the interval between WebSocket pings.
// Set to a negative value to disable pings.
// Default: 30 seconds
PingInterval time.Duration
// PingTimeout is the timeout for WebSocket ping responses and write operations.
// If a pong or write does not complete within this duration, the connection is closed.
// Default: 10 seconds
PingTimeout time.Duration
// ReadRate is the maximum messages-per-second the readLoop will pull
// off the WebSocket on a single connection. When the per-connection
// budget is exhausted the readLoop STALLS -- conn.Read is simply not
// invoked until a token refills -- so back-pressure propagates to the
// client through the TCP receive window. The relay never spends parse
// or signature-verification CPU on throttled traffic.
//
// This is the readLoop-level total cap and is independent of the
// per-message-type rate limit middlewares (Event/Req/Count/Auth),
// which run after parse and let operators tune category-specific
// budgets. Both layers compose: readLoop acts first as a coarse
// floodgate, middlewares then refine.
//
// Zero or negative disables the limit (default).
ReadRate float64
// ReadBurst is the bucket capacity for the readLoop rate limit. Must
// be >= 1 if ReadRate > 0; the constructor panics on misconfiguration
// rather than silently degrading. The bucket starts full at OnStart
// so a freshly-connected client can burst up to ReadBurst messages
// immediately, then is throttled to ReadRate long-term.
//
// Ignored when ReadRate <= 0.
ReadBurst int
// Info is the NIP-11 Relay Information Document.
// If set, the relay will respond to HTTP requests with
// Accept: application/nostr+json header.
Info *RelayInfo
// Registerer is the Prometheus registry that mocrelay's internal
// metrics are registered with. Setting this single field turns on every
// metric mocrelay exports from the Relay's own scope:
//
// - Connection / message / event counters on the Relay itself
// (mocrelay_connections_*, mocrelay_messages_*, mocrelay_events_received_*,
// mocrelay_ws_parse_errors_*, mocrelay_ws_write_errors_*,
// mocrelay_ws_write_duration_seconds).
// - The unified rejection counter
// (mocrelay_rejections_total{middleware, reason}), automatically
// installed into the request context so that every middleware's
// logRejection call increments it.
// - Auth middleware counters (mocrelay_auth_*,
// mocrelay_auth_authenticated_connections_current), also installed
// into the request context and read by the auth middleware.
//
// Metrics owned by types that compose *into* Relay (Router, Storage,
// CompositeStorage, MergeHandler, MetricsStorage) are registered from
// their own constructors; pass the same Registerer to each of those
// Options structs (typically prometheus.DefaultRegisterer) to collect
// everything under one registry.
//
// If nil, no metrics are registered from this Relay and all internal
// instrumentation no-ops.
Registerer prometheus.Registerer
// ConnIDFunc generates a unique connection ID string.
// If nil, a default monotonic counter ("1", "2", ...) is used.
ConnIDFunc func() string
}
RelayOptions configures Relay behavior. All fields are optional; the zero value gives sensible defaults.
type RelayPublicationFee ¶
type RelayPublicationFee struct {
Kinds []int64 `json:"kinds,omitzero"`
Amount int64 `json:"amount"`
Unit string `json:"unit"`
}
RelayPublicationFee represents a publication fee for specific kinds.
type RelayRetention ¶
type RelayRetention struct {
// Simplified: list of single kinds (not ranges)
// TODO: Support kind ranges like [30000, 39999]
Kinds []int64 `json:"kinds,omitzero"`
// Time in seconds. nil means infinity.
// 0 means the event will not be stored at all.
Time *int64 `json:"time,omitzero"`
// Maximum number of events to keep
Count *int64 `json:"count,omitzero"`
}
RelayRetention represents NIP-11 retention policy. TODO: The "kinds" field in NIP-11 can contain:
- Single integers: 0, 1, 4
- Ranges as tuples: [5, 7], [40, 49], [40000, 49999]
Example from NIP-11:
{"kinds": [0, 1, [5, 7], [40, 49]], "time": 3600}
For now, we use a simplified representation with separate fields. A full implementation would need custom JSON marshaling.
type RelaySubscriptionFee ¶
type RelaySubscriptionFee struct {
Amount int64 `json:"amount"`
Unit string `json:"unit"`
Period int64 `json:"period"` // in seconds
}
RelaySubscriptionFee represents a subscription fee with period.
type ReqFilter ¶
type ReqFilter struct {
IDs []string `json:"ids,omitempty"`
Authors []string `json:"authors,omitempty"`
Kinds []int64 `json:"kinds,omitempty"`
Tags map[string][]string `json:"-"` // handled manually: #e, #p, etc.
Since *int64 `json:"since,omitempty"`
Until *int64 `json:"until,omitempty"`
Limit *int64 `json:"limit,omitempty"`
Search *string `json:"search,omitempty"` // NIP-50
}
ReqFilter represents a filter in REQ/COUNT messages.
func (*ReqFilter) MarshalJSON ¶
MarshalJSON implements json.Marshaler for ReqFilter.
func (*ReqFilter) UnmarshalJSONFrom ¶
UnmarshalJSONFrom implements json.UnmarshalerFrom for ReqFilter. This handles the dynamic tag fields (#e, #p, etc.) and rejects unknown fields. All field values must be non-null (NIP-01).
type RestrictedWritesMode ¶
type RestrictedWritesMode int
RestrictedWritesMode determines how the pubkey list is interpreted.
const ( // RestrictedWritesModeAllowlist only allows pubkeys in the list. RestrictedWritesModeAllowlist RestrictedWritesMode = iota // RestrictedWritesModeBlocklist blocks pubkeys in the list. RestrictedWritesModeBlocklist )
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router manages client connections and subscriptions. It routes events to clients based on their subscription filters.
func NewRouter ¶
func NewRouter(opts *RouterOptions) *Router
NewRouter creates a new Router. opts can be nil for default options.
func (*Router) Broadcast ¶
Broadcast sends an event to all matching subscriptions. This is best-effort: if a connection's channel is full, the message is dropped.
func (*Router) Register ¶
Register registers a new connection and returns the connection ID. The sendCh is used to send messages to this connection.
func (*Router) Subscribe ¶
Subscribe registers a subscription for a connection. If the subscription ID already exists, it will be replaced.
func (*Router) Unregister ¶
Unregister removes a connection and all its subscriptions.
func (*Router) Unsubscribe ¶
Unsubscribe removes a subscription from a connection.
type RouterOptions ¶
type RouterOptions struct {
// Registerer is the Prometheus registry that Router's internal metrics
// are registered with (mocrelay_router_messages_dropped_total and
// mocrelay_router_subscriptions_current). If nil, no metrics are
// collected.
Registerer prometheus.Registerer
}
RouterOptions configures Router behavior. All fields are optional; the zero value gives sensible defaults.
type SearchIndex ¶
type SearchIndex interface {
// Index adds or updates an event in the search index.
Index(ctx context.Context, event *Event) error
// Search returns event IDs matching the query, ordered by relevance.
// Results are limited to the specified count.
Search(ctx context.Context, query string, limit int64) (eventIDs []string, err error)
// Delete removes an event from the search index.
Delete(ctx context.Context, eventID string) error
}
SearchIndex defines the interface for full-text search indexing.
type ServerMsg ¶
type ServerMsg struct {
Type string
// EVENT: subscription ID and event
SubscriptionID string
Event *Event
// OK: event ID, success flag, and message
EventID string
Accepted bool
Message string
// COUNT: count result
Count uint64
Approximate *bool
}
ServerMsg represents a message from relay to client. This is a union type - check Type field to determine which fields are valid.
func NewServerAuthMsg ¶
NewServerAuthMsg creates an AUTH message (challenge).
func NewServerClosedMsg ¶
NewServerClosedMsg creates a CLOSED message.
func NewServerCountMsg ¶
NewServerCountMsg creates a COUNT message.
func NewServerEOSEMsg ¶
NewServerEOSEMsg creates an EOSE message.
func NewServerEventMsg ¶
NewServerEventMsg creates an EVENT message.
func NewServerNoticeMsg ¶
NewServerNoticeMsg creates a NOTICE message.
func NewServerOKMsg ¶
NewServerOKMsg creates an OK message.
func (*ServerMsg) MarshalJSON ¶
MarshalJSON implements json.Marshaler for ServerMsg.
type SimpleHandlerBase ¶
type SimpleHandlerBase interface {
// OnStart is called when a new connection is established.
// The returned context is passed to subsequent calls.
// The returned ServerMsg (if non-nil) is sent to the client (e.g., AUTH challenge).
// Return an error to reject the connection.
OnStart(ctx context.Context) (context.Context, *ServerMsg, error)
// OnEnd is called when the connection is closing.
// This is always called, even if HandleMsg returned an error.
// The returned ServerMsg (if non-nil) is sent to the client.
// Use this for cleanup (e.g., removing subscriptions).
OnEnd(ctx context.Context) (*ServerMsg, error)
// HandleMsg is called for each client message.
// Return an iterator of server messages to send back.
// Return an error to terminate the connection.
HandleMsg(ctx context.Context, msg *ClientMsg) (iter.Seq[*ServerMsg], error)
}
SimpleHandlerBase is the interface for handlers that process messages one at a time. This is easier to implement than Handler for most use cases.
The lifecycle is:
- OnStart is called when the connection is established
- HandleMsg is called for each client message
- OnEnd is called when the connection is closing (always called, even on error)
type SimpleMiddlewareBase ¶
type SimpleMiddlewareBase interface {
// OnStart is called when a new connection is established.
// The returned context is passed to subsequent calls.
// The returned ServerMsg (if non-nil) is sent to the client (e.g., AUTH challenge).
// Return an error to reject the connection.
OnStart(ctx context.Context) (context.Context, *ServerMsg, error)
// OnEnd is called when the connection is closing.
// This is always called, even if Handle* returned an error.
// The returned ServerMsg (if non-nil) is sent to the client.
// Use this for cleanup.
OnEnd(ctx context.Context) (*ServerMsg, error)
// HandleClientMsg processes a client message.
// Returns:
// - (out, nil, nil): pass out to downstream handler
// - (nil, resp, nil): send resp to client, don't pass downstream
// - (nil, nil, nil): drop the message
// - (_, _, err): error, connection will be terminated
HandleClientMsg(ctx context.Context, msg *ClientMsg) (out *ClientMsg, resp *ServerMsg, err error)
// HandleServerMsg processes a server message from downstream.
// Returns:
// - (out, nil): send out to client
// - (nil, nil): drop the message
// - (_, err): error, connection will be terminated
HandleServerMsg(ctx context.Context, msg *ServerMsg) (out *ServerMsg, err error)
}
SimpleMiddlewareBase is the interface for middlewares that process messages one at a time. This is easier to implement than writing a full Middleware for most use cases.
The lifecycle is:
- OnStart is called when the connection is established
- HandleClientMsg is called for each client message (recv direction)
- HandleServerMsg is called for each server message (send direction)
- OnEnd is called when the connection is closing (always called, even on error)
Message handling rules:
- HandleClientMsg returns (out, resp, err):
- out != nil: pass out to downstream handler
- resp != nil: send resp to client (without passing to downstream)
- out == nil && resp == nil: drop the message
- HandleServerMsg returns (out, err):
- out != nil: send out to client
- out == nil: drop the message
func NewAuthMiddlewareBase ¶
func NewAuthMiddlewareBase(relayURL string, opts *AuthMiddlewareOptions) SimpleMiddlewareBase
NewAuthMiddlewareBase creates a middleware base that requires NIP-42 authentication.
relayURL is the URL of this relay (used for relay tag validation). opts can be nil for default options.
func NewAuthRateLimitMiddlewareBase ¶ added in v0.16.0
func NewAuthRateLimitMiddlewareBase(rate float64, burst int) SimpleMiddlewareBase
NewAuthRateLimitMiddlewareBase returns a middleware base that rate-limits incoming AUTH (NIP-42) messages on a per-connection basis using a token bucket.
rate is the long-term sustained rate of AUTH messages allowed per connection, in AUTHs per second; must be positive. burst is the maximum number of AUTH messages a connection may submit in a tight window before rate-limiting kicks in. The bucket starts full at connection start; must be positive.
When the bucket is empty, the offending AUTH is dropped and a `["OK", <event_id>, false, "rate-limited: too many auth attempts"]` is sent to the client. NIP-42 specifies AUTH responses are OK messages, so the response shape matches a successful AUTH except for accepted=false.
EVENT / REQ / CLOSE / COUNT messages pass through untouched -- compose with the matching per-type middleware (Event/Req/Count) if you need to limit those independently.
Use this middleware to defend against AUTH challenge-response brute force; a typical setting is a low rate (e.g. 0.1 = one attempt every 10 seconds long-term) with a small burst (e.g. 3) so legitimate reconnects still work but credential stuffing is throttled.
Each connection gets its own token bucket via OnStart + context.
Panics if rate <= 0 or burst < 1.
func NewCountRateLimitMiddlewareBase ¶ added in v0.16.0
func NewCountRateLimitMiddlewareBase(rate float64, burst int) SimpleMiddlewareBase
NewCountRateLimitMiddlewareBase returns a middleware base that rate-limits incoming COUNT (NIP-45) messages on a per-connection basis using a token bucket.
rate is the long-term sustained rate of COUNT messages allowed per connection, in COUNTs per second; must be positive. burst is the maximum number of COUNT messages a connection may submit in a tight window before rate-limiting kicks in. The bucket starts full at connection start; must be positive.
When the bucket is empty, the offending COUNT is dropped and a `["CLOSED", <sub_id>, "rate-limited: too many counts"]` is sent to the client. EVENT / REQ / CLOSE / AUTH messages pass through untouched -- compose with the matching per-type middleware (Event/Req/Auth) if you need to limit those independently.
COUNT can be expensive to compute (it scans the same indexes a REQ would but cannot stream early), so a tighter rate than REQ is often appropriate. Each connection gets its own token bucket via OnStart + context.
Panics if rate <= 0 or burst < 1.
func NewCreatedAtLimitsMiddlewareBase ¶
func NewCreatedAtLimitsMiddlewareBase(lowerLimit, upperLimit int64) SimpleMiddlewareBase
NewCreatedAtLimitsMiddlewareBase returns a middleware base that limits the created_at timestamp of events.
lowerLimit is the maximum age in seconds (how far into the past is allowed). upperLimit is the maximum seconds into the future allowed. Both must be non-negative.
func NewEventRateLimitMiddlewareBase ¶ added in v0.16.0
func NewEventRateLimitMiddlewareBase(rate float64, burst int) SimpleMiddlewareBase
NewEventRateLimitMiddlewareBase returns a middleware base that rate-limits incoming EVENT messages on a per-connection basis using a token bucket.
rate is the long-term sustained rate of EVENT messages allowed per connection, in events per second; must be positive. burst is the maximum number of EVENT messages a connection may submit in a tight window before rate-limiting kicks in. The bucket starts full at connection start; must be positive.
When the bucket is empty, the offending EVENT is dropped and a `["OK", <id>, false, "rate-limited: too many events"]` is sent to the client (NIP-01 standard prefix). REQ / CLOSE / AUTH / COUNT messages pass through untouched -- compose with the matching per-type middleware (Req/Count/Auth) if you need to limit those independently.
The token bucket is created in OnStart and stored in the request context, so each connection has its own independent bucket. Counters are not shared across connections.
Panics if rate <= 0 or burst < 1.
func NewExpirationMiddlewareBase ¶
func NewExpirationMiddlewareBase() SimpleMiddlewareBase
NewExpirationMiddlewareBase returns a middleware base implementing NIP-40 (Expiration Timestamp). Events with an "expiration" tag are rejected if expired on receipt, and dropped (not delivered) if expired on send.
func NewKindAllowlistMiddlewareBase ¶ added in v0.15.0
func NewKindAllowlistMiddlewareBase(kinds []int64) SimpleMiddlewareBase
NewKindAllowlistMiddlewareBase returns a middleware base that rejects events whose kind is NOT in kinds. This is useful for restricting a relay to a well-known set of kinds (e.g., the kinds documented in the NIPs README).
An empty allowlist rejects all events. Range-style kinds (e.g., NIP-90 Job Request 5000-5999) are expanded into individual entries by the caller:
kinds := []int64{0, 1, 3}
for k := int64(5000); k <= 5999; k++ {
kinds = append(kinds, k)
}
mw := NewKindAllowlistMiddlewareBase(kinds)
func NewKindDenylistMiddlewareBase ¶
func NewKindDenylistMiddlewareBase(kinds []int64) SimpleMiddlewareBase
NewKindDenylistMiddlewareBase returns a middleware base that rejects events whose kind is in kinds. This is useful for legal compliance (e.g., rejecting DM-related kinds for Japanese telecommunications law compliance).
func NewMaxContentLengthMiddlewareBase ¶
func NewMaxContentLengthMiddlewareBase(maxLen int) SimpleMiddlewareBase
NewMaxContentLengthMiddlewareBase returns a middleware base that limits the number of Unicode characters (not bytes) in event content.
maxLen must be a positive integer.
func NewMaxEventTagsMiddlewareBase ¶
func NewMaxEventTagsMiddlewareBase(maxTags int) SimpleMiddlewareBase
NewMaxEventTagsMiddlewareBase returns a middleware base that limits the number of tags in an event. maxTags must be a positive integer.
func NewMaxLimitMiddlewareBase ¶
func NewMaxLimitMiddlewareBase(maxLimit, defaultLimit int64) SimpleMiddlewareBase
NewMaxLimitMiddlewareBase returns a middleware base that clamps the limit value in REQ filters and applies a default when unset.
maxLimit is the maximum allowed limit value (must be positive). defaultLimit is applied when no limit is specified (must be positive and <= maxLimit).
func NewMaxSubidLengthMiddlewareBase ¶
func NewMaxSubidLengthMiddlewareBase(maxLen int) SimpleMiddlewareBase
NewMaxSubidLengthMiddlewareBase returns a middleware base that limits the length of subscription IDs. maxLen must be a positive integer.
func NewMaxSubscriptionsMiddlewareBase ¶
func NewMaxSubscriptionsMiddlewareBase(maxSubs int) SimpleMiddlewareBase
NewMaxSubscriptionsMiddlewareBase returns a middleware base that limits the number of concurrent subscriptions per connection. maxSubs must be a positive integer.
func NewMinPowDifficultyMiddlewareBase ¶
func NewMinPowDifficultyMiddlewareBase(minDifficulty int, checkCommitment bool) SimpleMiddlewareBase
NewMinPowDifficultyMiddlewareBase creates a middleware base that validates Proof of Work (NIP-13).
Parameters:
- minDifficulty: minimum number of leading zero bits required in event ID
- checkCommitment: if true, also validates the nonce tag's target difficulty. This rejects events whose committed target is below minDifficulty even if their actual difficulty is sufficient, preventing spammers from getting lucky with low-target mining.
func NewProtectedEventsMiddlewareBase ¶
func NewProtectedEventsMiddlewareBase() SimpleMiddlewareBase
NewProtectedEventsMiddlewareBase returns a middleware base implementing NIP-70 (Protected Events): events with a ["-"] tag can only be published by their authenticated author.
This middleware requires NIP-42 authentication to be enabled upstream. If no authentication state is found in the context, protected events are always rejected.
func NewReqRateLimitMiddlewareBase ¶ added in v0.16.0
func NewReqRateLimitMiddlewareBase(rate float64, burst int) SimpleMiddlewareBase
NewReqRateLimitMiddlewareBase returns a middleware base that rate-limits incoming REQ messages on a per-connection basis using a token bucket.
rate is the long-term sustained rate of REQ messages allowed per connection, in REQs per second; must be positive. burst is the maximum number of REQ messages a connection may submit in a tight window before rate-limiting kicks in. The bucket starts full at connection start; must be positive.
When the bucket is empty, the offending REQ is dropped and a `["CLOSED", <sub_id>, "rate-limited: too many subscriptions"]` is sent to the client. EVENT / CLOSE / AUTH / COUNT messages pass through untouched -- compose with the matching per-type middleware (Event/Count/Auth) if you need to limit those independently.
Each connection gets its own token bucket via OnStart + context.
Panics if rate <= 0 or burst < 1.
func NewRestrictedWritesMiddlewareBase ¶
func NewRestrictedWritesMiddlewareBase(mode RestrictedWritesMode, pubkeys []string) SimpleMiddlewareBase
NewRestrictedWritesMiddlewareBase returns a middleware base that restricts which pubkeys can write events.
mode determines whether the list is an allowlist or blocklist. pubkeys is the list of pubkeys to allow or block.
type Storage ¶
type Storage interface {
// Store saves an event and returns whether it was actually stored.
// Returns false for duplicates, older replaceable events, or deleted events.
Store(ctx context.Context, event *Event) (stored bool, err error)
// Query returns an iterator over events matching any of the filters.
// Results are sorted by created_at DESC, then id ASC (lexical).
// The first filter's limit is applied globally (NIP-01).
//
// Returns:
// - events: iterator over matching events (use with for-range)
// - err: call after iteration to check for errors
// - close: call to release resources (use with defer)
//
// Example:
// events, errFn, closeFn := storage.Query(ctx, filters)
// defer closeFn()
// for event := range events {
// // process event
// }
// if err := errFn(); err != nil {
// return err
// }
Query(ctx context.Context, filters []*ReqFilter) (events iter.Seq[*Event], err func() error, close func() error)
}
Storage defines the interface for event storage.
type StorageHandlerOptions ¶ added in v0.9.0
type StorageHandlerOptions struct {
// SlowQueryThreshold sets the total-duration threshold above which a REQ
// or COUNT subscription is logged at Warn via [LoggerFromContext]. The
// emitted log carries a breakdown (scan vs send-wait time for REQ) so
// operators can distinguish a genuinely heavy query from a client that
// is failing to drain its send buffer — the two are entangled because
// iter.Seq is pull-driven and pauses the storage iterator while yield
// is blocked, so having both numbers side-by-side is the most reliable
// signal of which side owns the stall.
//
// A zero value selects [DefaultStorageHandlerSlowQueryThreshold] (1s).
// A negative value disables slow-query logging entirely.
SlowQueryThreshold time.Duration
// QueryTimeout aborts a REQ or COUNT whose total duration exceeds it.
// When the timeout fires, the storage handler stops iterating, sends a
// CLOSED message with reason "error: query timeout", and returns —
// neither the trailing EOSE (for REQ) nor the COUNT reply is emitted.
// The slow-query log (if enabled) still fires with completed=false, so
// the abort is recorded alongside other slow subscriptions.
//
// The context passed to Storage.Query is wrapped with
// [context.WithTimeout], so any Storage implementation that respects
// ctx can short-circuit its own scan; implementations that don't (e.g.
// Pebble) are stopped at the next event boundary where the iteration
// loop polls ctx.Err.
//
// The primary use case is a live but slow-consuming client — one that
// keeps the WebSocket alive (so ping_timeout never fires) yet drains
// EVENTs so slowly that a broad REQ can stall the handler's iterator
// for hours. Setting QueryTimeout gives the relay a hard ceiling.
//
// A zero value disables the timeout (default, preserves prior
// behavior). A negative value also disables it. When non-zero, it
// should typically be set well above SlowQueryThreshold so the
// threshold logs fire first and give operators a chance to observe
// slow REQs before they are aborted.
QueryTimeout time.Duration
}
StorageHandlerOptions configures the storage handler returned by NewStorageHandler.
All fields are optional. A nil *StorageHandlerOptions is equivalent to a zero-valued StorageHandlerOptions and means "use defaults for everything".
type Tag ¶
type Tag []string
Tag represents a tag in a Nostr event. The first element is the tag name, followed by optional values.
func (*Tag) UnmarshalJSONFrom ¶
UnmarshalJSONFrom implements json.UnmarshalerFrom to reject null elements. NIP-01 requires tags to be "arrays of non-null strings".
Source Files
¶
- bleve_index.go
- composite_storage.go
- doc.go
- event.go
- filter.go
- handler.go
- kind_label.go
- logger.go
- merge_handler.go
- message.go
- metrics.go
- metrics_storage.go
- middleware.go
- middleware_auth.go
- middleware_auth_rate_limit.go
- middleware_count_rate_limit.go
- middleware_created_at_limits.go
- middleware_event_rate_limit.go
- middleware_expiration.go
- middleware_kind_allowlist.go
- middleware_kind_denylist.go
- middleware_max_content_length.go
- middleware_max_event_tags.go
- middleware_max_limit.go
- middleware_max_subid_length.go
- middleware_max_subscriptions.go
- middleware_min_pow_difficulty.go
- middleware_protected_events.go
- middleware_req_rate_limit.go
- middleware_restricted_writes.go
- nop_handler.go
- pebble_storage.go
- pow.go
- rate_limit.go
- relay.go
- relay_info.go
- router.go
- router_handler.go
- storage.go
- storage_handler.go
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
examples/kitchen-sink
command
Kitchen-sink relay example.
|
Kitchen-sink relay example. |
|
examples/minimal
command
Minimal relay example.
|
Minimal relay example. |
|
examples/nop
command
Nop relay example.
|
Nop relay example. |