torly - a TinyGo Nostr relay
git clone https://git.smesh.lol/torly.git
A Nostr relay compiled by TinyGo, ported clean-room from the relay semantics of orly onto the storage model used by morly: an append-only write-ahead log for immutable signed events, and sorted flat-file indexes over it. No Badger, no SQLite, no gRPC, no Neo4j, no cgo.
torly 0.1.0
listening on http://127.0.0.1:3334
EVENT, REQ, CLOSE, EOSE, OK, NOTICE, CLOSED), NIP-09 deletion, NIP-11 relay information, NIP-40 expiration,
NIP-42 authentication, NIP-45 COUNT, NIP-50 search, NIP-70 protected
events, replaceable and addressable event semantics.
epg/peg/eeg/gee/ppg/gpp indexes and the _graph filter extension. pp traverses kind-3 follows, pe the
pubkey↔event edges (authored events and p-tag mentions), and ee the e-tag
thread graph. Results are returned as relay-signed kind-39000 events.
check) and BUD-12 (list by pubkey), with kind:24242 authorization.
fixed-width index files. Crash recovery replays the log after the last checkpoint; NIP-09 tombstones survive restarts. Graph edges are stored in both directions, and a reply that arrives before its root is backfilled from the tag index when the root is saved.
HTTP/1.1 and RFC 6455 parsing live in the reader domain; only the writer
writes and closes. TinyGo's net/http Hijack is unusable, so the
WebSocket path is implemented directly over the socket.
tiny-spawn. Each domain owns its state and communicates only over channels bound at spawn; there is no shared mutable state between goroutines.
Out of scope, deliberately: Tor, WireGuard, ACME, SQL databases, Badger, Neo4j, gRPC, the payment/bridge/bunker subsystems, and the browser UI (which can be a TinyGo WASM application later; the relay already serves a static directory).
TinyGo still needs the Go toolchain for its standard-library sources. On this machine both are provisioned locally:
. ./scripts/env.sh # puts ~/.local/goroot/bin on PATH
make build # -> ./torly, built by tinygo
Requirements: TinyGo 0.43+ (built with Go 1.27), Go 1.26+.
There are no external modules and no vendor directory. The implementations
live in this repository: pkg/tinyspawn and pkg/tinyactor for the run model,
pkg/crypto/btcec and pkg/crypto/decred/secp256k1 (copied ISC code) for
BIP-340, and pkg/unix for the socket/poll calls the acceptor uses. go.mod
has no require block; a clone builds offline. There is no cgo and no
SQL/mmap dependency.
The relay is a fixed tree, spawned once at startup and cancelled by one breaker. No domain is created per connection or per job.
| Domain | Owns |
|---|---|
root (main) | configuration copy, channel wiring, signal handling, shutdown |
| acceptor | the listening fd, non-blocking poll, connection assignment |
| reader[0..N-1] | read side + HTTP/WebSocket parsing for one connection at a time |
| writer[0..N-1] | write side, framing, and the close of that connection |
| net | connections, subscriptions, authentication, pending queries |
| db | the store engine, WAL, indexes, expiry ticker |
| verify[0..V-1] | one BIP-340 check at a time, rotating from net |
| blossom | blob index, blob files, BUD authorization |
N is TORLY_CONN_WORKERS (default 64) and V is
TORLY_VERIFY_WORKERS (default 2). A reader and its paired writer split the
socket capability: only the reader reads, only the writer writes and closes.
Messages are values whose slice/map ownership moves with the message; the
sender never touches them again. Package variables are read-only constants or
immutable configuration. On native Moxie the same tree becomes processes; on
TinyGo it is a fixed set of cooperative goroutines.
TORLY_DATA_DIR=./data TORLY_PORT=3334 ./torly serve
To load events from a JSONL export without starting the relay:
torly import --data-dir ./data --file events.jsonl
torly import writes the WAL and indexes in bulk and builds the social graph
once at the end.
Blossom has offline backup and live restore commands:
torly blossom-backup --data-dir ./data --out blossom.tar
torly blossom-restore --data-dir ./data --file blossom.tar
See docs/DEPLOYMENT.md for the torly.smesh.lol
deployment: service layout, Caddy, the five-minute health monitor, and
Blossom backup/restore.
Configuration is by environment variable. Every TORLY_* name also accepts
an ORLY_* fallback, so an existing deployment only has to change its binary.
| Variable | Default | Meaning |
|---|---|---|
TORLY_APP_NAME | torly | NIP-11 name |
TORLY_DESCRIPTION | torly - a TinyGo Nostr relay | NIP-11 description |
TORLY_CONTACT / TORLY_ICON | empty | NIP-11 contact and icon |
TORLY_LISTEN | 127.0.0.1 | bind address |
TORLY_PORT | 3334 | bind port |
TORLY_DATA_DIR | ~/.local/share/torly | database and blobs |
TORLY_STATIC_DIR | empty | optional web root |
TORLY_RELAY_URL | derived | URL used by NIP-42 checks |
TORLY_SECRET_KEY | DATA_DIR/relay.key | relay signing key, 64 hex chars |
TORLY_AUTH_REQUIRED | false | require NIP-42 AUTH to publish |
TORLY_MAX_CONNECTIONS | 0 | cap on concurrent connections, 0 = worker count |
TORLY_CONN_WORKERS | 64 | fixed reader/writer pairs |
TORLY_VERIFY_WORKERS | 2 | signature verification domains |
TORLY_BLOSSOM_ENABLED | true | serve Blossom |
TORLY_BLOSSOM_MAX_BLOB_MB | 50 | largest accepted blob |
TORLY_MAX_EVENT_BYTES | 131072 | largest event JSON |
TORLY_MAX_SUBS_PER_CONN | 64 | subscriptions per connection |
TORLY_MAX_FILTERS | 16 | filters per REQ/COUNT |
TORLY_MAX_LIMIT | 5000 | largest accepted filter limit |
TORLY_QUERY_LIMIT | 5000 | cap for unbounded filters |
TORLY_MAX_FUTURE_SECS | 900 | allowed created_at skew |
TORLY_EXPIRY_INTERVAL | 60 | seconds between NIP-40 purges |
TORLY_LOG_LEVEL | info | error, warn, info, debug |
torly version prints the version, torly help prints the same table.
torly/
cmd/torly/ entry point, signal handling, tree startup/shutdown
pkg/config/ environment configuration and NIP-11 document
pkg/nostr/ events, tags, copied JSON codec, filters, _graph types
pkg/crypto/ copied BIP-340 implementation + torly's wrapper
pkg/crypto/btcec/ copied btcec/decred schnorr and curve code
pkg/crypto/decred/ copied decred secp256k1 field/scalar/group code
pkg/tinyspawn/ copied tiny-spawn run model
pkg/tinyactor/ copied tiny-actor lifecycle primitives
pkg/unix/ the socket/poll calls the acceptor needs
pkg/store/ WAL, sorted index runs, query planner, graph traversal, recovery
pkg/domain/db/ database domain (sole owner of the store)
pkg/domain/verify/ signature verification workers
pkg/domain/net/ protocol state: connections, subs, auth, routing
pkg/domain/io/ acceptor, reader/writer workers, HTTP/WS parsing
pkg/domain/blob/ Blossom domain
pkg/wire/ value messages shared between domains
pkg/httpsrv/ HTTP request parser (transport-neutral)
pkg/ws/ RFC 6455 reader, handshake and frame builder
pkg/relay/ spawn-tree root and integration tests
deploy/ systemd units, env file and Caddy site block
scripts/torly-monitor.sh health and NIP-11 check
pkg/blossom/ blob store, index log, BUD endpoints
pkg/sigtrap/ TinyGo prompt-signal workaround
docs/GRAPH.md `_graph` request/response and edge semantics
docs/GRAPH-COMPATIBILITY.md comparison with the original orly graph code
docs/DEPLOYMENT.md service layout, Caddy, monitor and Blossom backup
scripts/e2e.sh end-to-end check against the `nak` client
Data directory:
data/
relay.key relay signing key (TORLY_SECRET_KEY overrides)
db/
wal/seg-000.dat append-only event log
idx/*.dat merged sorted indexes
idx/*.dat.buf.NNNNNN immutable sorted runs (not yet merged)
checkpoint.dat last durable WAL position + counters
blossom/
blobs/<xx>/<sha256> blob content
index.log ownership and content-type log
make test # unit and integration tests with the Go toolchain
make test-tinygo # the same suite compiled and run by TinyGo
make e2e # builds with TinyGo and drives it with a real client (nak)
106 test functions in 17 files. End-to-end coverage with -coverpkg=./... is
74.5%; the domain packages are exercised through the relay integration tests
rather than direct unit tests.
| Package | Coverage | What is exercised |
|---|---|---|
pkg/crypto/secp256k1 | 88.8% | official BIP-340 vectors, signing round trips, tamper rejection |
pkg/nostr | 80.4% | canonical JSON vs Node, ids/signatures, parsing failures, tags, filters, _graph parsing |
pkg/config | 88.2% | defaults, ORLY_* fallback and TORLY_* precedence, bounds, booleans |
pkg/store | 77.0% | save/query planner, replacement, deletion, expiry, torn WAL tail, stale checkpoint, multi-run reopen, result caps, graph edges and traversal |
pkg/httpsrv | 72.9% | request parsing, bodies, duplicate headers, 100-continue, malformed requests, oversized bodies, hijack |
pkg/ws | 46.2% (reader/frame code; integration covers the rest) | handshake accept/reject, masked frames, fragmentation, ping/pong, close, UTF-8 and size limits, frame lengths |
pkg/relay | 74.3% | NIP-01/09/40/42/45/50/70 flows, duplicate and ephemeral events, limits, malformed messages, live delivery, concurrency, Blossom, static files, signed _graph responses |
pkg/blossom | 72.9% | BUD auth matrix, upload/check/download/list/delete, ownership, index reopen |
pkg/domain/*, pkg/wire | end-to-end | exercised by the relay integration suite through real sockets |
cmd/torly | 34.6% | importer and reindex-graph unit tests; start/publish/restart/SIGTERM/SIGKILL covered end to end |
pkg/logx, pkg/sigtrap | — | thin wrappers; the signal path is covered by make e2e |
make e2e drives the TinyGo binary with nak: publish, query, count, search,
replace, delete, upload/remove a blob, graceful restart, then SIGKILL and WAL
recovery.
Deliberately not unit-tested: disk-full and permission faults, physical network loss, and features that are not implemented (NIP-77 sync, NIP-86 management, NIP-46 bunker, payments). Those would test the operating system, not torly.
services requests sequentially; verification runs in separate domains and the network domain never blocks on it. The connection limit is the fixed worker pool: a reader/writer pair serves one connection at a time.
TORLY_LISTEN must be an IPv4 address (or empty/0.0.0.0).
btcec's pure-Go 5×52 BIP-340 implementation: about 0.3 ms per event under
TinyGo here, versus 6.4 ms for the math/big reference it replaced. The
reference remains for BIP-340's variable-length message vectors and as the
independent cross-check exercised by the tests.
COUNT with several filters sums their counts and marks the replyapproximate; there is no NIP-77 negentropy, NIP-29 groups, NIP-46 bunker, NIP-86 management API, payments or media proxy.
_graph accepts the current orly four-element array and its object form. pp is kind-3 follows, pe is one hop by design, and ee seeds from the
events authored by the given pubkey. A graph filter is answered once; any
other filters in the same REQ are ignored. COUNT rejects _graph.
ids/authors are not supported; filters must carry full64-character hashes.
is parked in netpoll. pkg/sigtrap calls the runtime's checkSignals
from a 200 ms ticker so SIGTERM shuts the relay down promptly.
way Moxie's codec does. The boundaries are documented in pkg/wire, and
the tests run under -race.
AGPL-3.0-or-later. See LICENSE.