A Deno + Hono gateway that serves static websites published on Nostr (the nsite protocol).
Sites are identified by site manifest events (kind 15128 for root sites, kind
35128 for named sites, and kind 5128 for snapshots) and blobs served via
Blossom.
All configuration is done through the .env file. Start by copying the example
file and modifying it.
cp .env.example .env| Variable | Default | Description |
|---|---|---|
LOOKUP_RELAYS |
wss://user.kindpag.es,wss://purplepag.es |
Comma-separated relays used to look up a user's NIP-65 relay list (kind 10002) and blossom server list (kind 10063) |
NOSTR_RELAYS |
(none) | Extra relays added to every event query, supplemental to the user's own outbox relays |
CACHE_RELAYS |
(auto-detect ws://localhost:4869) |
Relays to persist all fetched events to (a local Nostr cache relay). Auto-detected if a relay is running on localhost:4869 |
BLOSSOM_SERVERS |
(none) | Comma-separated fallback blossom servers used when a user has no 10063 event and the manifest has no server tags |
BLOSSOM_PROXY |
(auto-detect http://localhost:24242) |
Optional upstream blossom proxy checked first for every blob (see Blossom Proxy). Auto-detected if a proxy is running on localhost:24242 |
MAX_FILE_SIZE |
128 MB |
Maximum blob size to serve (e.g. "2 MB"). Enforced via Content-Length header and during streaming |
CACHE_PATH |
(Deno default KV location) | File path for the persistent Deno KV store (e.g. ./data/cache). Omit to use Deno's default location |
CACHE_TIME |
3600 |
TTL in seconds for all KV cache entries (DNS lookups, blob server hints, user profiles) |
BLOB_SERVER_TTL |
604800 |
TTL in seconds for the preferred verified blob source cache (["blob-server", sha256]), allowing trusted sources to stick around longer than general cache entries |
BLOB_BAD_SOURCE_TTL |
86400 |
TTL in seconds for a bad (sha256, server) verification result before that source is eligible to be retried for the blob |
VERIFY_WORKER_POOL_MAX |
4 |
Maximum number of blob verification workers created dynamically to hash responses in parallel |
PUBLIC_DOMAIN |
(none) | The gateway's own public domain. When set, it is used for constructing canonical site URLs on the homepage and status pages |
NSITE_HOST |
0.0.0.0 |
IP address the server binds to |
NSITE_PORT |
3000 |
Port the server listens on |
ONION_HOST |
(none) | If set to a .onion URL, every nsite response includes an Onion-Location header pointing to the Tor mirror |
CURATION_USER |
(none) | Hex pubkey of a curator whose NIP-51 mute list (kind 10000) is loaded into the in-memory event store at startup and refreshed on a timer. Sites whose author pubkey appears in public p tags on that list are omitted from the home page (encrypted mutes in content are not applied). |
CURATION_REFRESH |
600 (10 min) |
How often to re-fetch the curator mute list, in seconds |
deno task startFor local development with file watching:
deno task devThe Deno tasks already include the required flags (--unstable-kv,
--env-file=.env, and the necessary permission flags).
If NOSTR_RELAYS is set, the gateway will bulk-fetch all known site manifests
(kinds 15128, 35128, and 5128) from those relays at startup,
pre-populating the in-memory event store, and then re-check those relays every
10 minutes for newer manifest events.
If CURATION_USER is set, the gateway loads that user's mute list via the same
event loader used elsewhere, keeps it in eventStore, and refreshes it on the
interval given by CURATION_REFRESH in seconds (independent of NOSTR_RELAYS).
The gateway uses Deno KV to cache:
- DNS resolution results (hostname → pubkey + site identifier)
- Blob server hints — the last successful server for each blob (tried first on subsequent requests)
- Blob server lists — the full ordered server list for each blob
- User profiles (kind
0) — author display names shown on the homepage and status pages
To enable persistent caching, set CACHE_PATH:
CACHE_PATH="./data/cache"If CACHE_PATH is omitted, Deno uses its default local KV location.
All fetched Nostr events are held in an in-memory event store for the process
lifetime. To persist events across restarts, point CACHE_RELAYS at a local
Nostr relay:
CACHE_RELAYS="ws://localhost:4869"If a relay is already running on localhost:4869, it will be detected and used
automatically.
All nsite responses include strong ETags (the blob's sha256 hash) and
Cache-Control: public, max-age=3600. Conditional requests with If-None-Match
are handled — matching ETags return 304 Not Modified without fetching the blob
at all.
You can run the published package without cloning this repository:
deno run --unstable-kv --env-file=.env --allow-env --allow-net --allow-read --allow-write jsr:@hzrd149/nsite-gatewayThe included docker-compose.yml sets up a full production stack:
- nsite-gateway — the gateway itself
- Caddy — TLS termination and reverse proxy
- flower-cache — local blossom proxy (wired as
BLOSSOM_PROXY)
git clone https://github.com/hzrd149/nsite-gateway.git
cd nsite-gateway
docker compose upPersistent Deno KV caching is enabled via a Docker volume mounted at /cache.
Note: You must create a
Caddyfilebefore starting the stack — thedocker-compose.ymlmounts./Caddyfileinto the Caddy container but the file is not included in the repository.
Once running, the gateway is accessible at http://localhost:3000.
docker run --rm -it --name nsite -p 3000:3000 ghcr.io/hzrd149/nsite-gatewayNote: The default image CMD does not include
--unstable-kvor--allow-write, so Deno KV caching is inactive. Use a custom entrypoint or thedocker composesetup if you need persistent caching.
The gateway resolves incoming hostnames to a Nostr site using three strategies (in order):
- npub subdomain —
npub1abc....nsite.example.com: the leftmost label is a valid bech32npub, decoded to a hex pubkey. Serves the root site (kind15128). - Snapshot label —
v<50-char-base36-event-id>.nsite.example.com: the leftmost label starts withvand is followed by a 50-character base36 snapshot event id. Serves the exact snapshot event (kind5128). - Named site label — a 50-character base36-encoded pubkey followed by a
1–13 character site identifier (e.g.
<base36pubkey><identifier>.nsite.example.com). Serves a named site (kind35128). - CNAME resolution — if the hostname doesn't parse directly as an nsite
label, the gateway resolves CNAME records. This enables custom domains like
myblog.com → npub1abc....nsite.example.com.
The gateway serves a built-in homepage at the root domain that displays all currently cached sites as a card grid. Each card shows the site title, description, author name, path count, and last-updated time.
To replace the built-in homepage with your own, place an index.html file in
the public/ directory at the project root.
The gateway serves a built-in status dashboard at /status:
GET /status— table of all site manifests currently loaded in the event store, with titles, authors, path counts, and last-updated timestamps.GET /status/:address— detailed view for anynpub,naddr,nprofile, raw hex pubkey, orv<snapshotIdB36>snapshot id: site metadata, relays, blossom servers, full path table with cached server info, and the raw manifest JSON.
Status pages are always Cache-Control: no-store.
If you operate a Tor mirror, set ONION_HOST and the gateway will include an
Onion-Location header in every nsite response:
ONION_HOST="http://examplehiddenservice.onion"You can configure a BLOSSOM_PROXY server that will be checked first for all
blob requests before falling back to other servers. When set, the gateway will:
- Check the proxy first for each blob request
- Include BUD-10 discovery hints as query parameters:
xsparameters: domain names of all known blossom servers (server hints)asparameter: the author's pubkey (author hint)
This allows the proxy to locate blobs on other servers if it doesn't have them cached.
BLOSSOM_PROXY="https://blossom-proxy.example.com"The proxy URL is constructed as:
<BLOSSOM_PROXY>/<sha256>?xs=server1.com&xs=server2.com&as=<pubkey>
The blossom proxy specification is defined in BUD-11. For a reference implementation, see flower-cache.
If a proxy is already running on localhost:24242, it will be detected and used
automatically without setting BLOSSOM_PROXY.