Nostr Agent Onboarding · start here · index · source:
nostr-dev/docs/patterns/going-public.md· snapshot 2026-10-10Paths such as
~/Documents/…,~/Production Environment/…,repos/…and services onlocalhostrefer to the author's workstation and are not available to you — read them as worked examples of a setup you can recreate.
Pattern: Going public — exposing local Nostr-native artifacts on the open internet
Source: the runbook
~/work/nostr-archive/docs/going-public.md(sibling agent's work). That document covers an archive specifically; this one generalizes the recipe so any artifact built on this workspace's local stack can be made publicly reachable without exposing this machine to inbound traffic.
You built something against the local GRASP / relay / Blossom
(http://localhost:8081). It works in this environment. Now you want
anyone with a browser to be able to use it, ideally without poking holes
in your firewall, NAT, or DNS.
The pattern is publish to public infrastructure. Local stays canonical for development; public is a mirror. The visitor's browser only ever talks to public services. Bytes are content-addressed, events are signed — replicating to public losses no provenance.
Three things to mirror
- Events (kind:1, kind:30617, anything addressable) → public Nostr relays.
- Blobs (Blossom) → public Blossom servers.
- Git repos (GRASP
nostr://URLs) → a public GRASP host.
If your artifact is a static web client, there's a fourth piece:
- The static site itself → publish as an nsite (NIP-5A, kind 15128;
the old per-file kind 34128 is dead). An nsite gateway (
<npub>.nsite.lol) serves it from public Blossom. Full recipe:nsite-publishing.md.
The recipe
Adapted, generalized, and condensed from ~/work/nostr-archive/docs/going-public.md.
Every step is idempotent — re-running won't hurt.
1. Mirror events to public relays
Pick the kinds your artifact uses, pull from local relays, push to public:
PUBHEX=$(jq -r .pub_hex /dev/shm/nostr-dev-session/keys.json)
PUBLIC=("wss://relay.ngit.dev" "wss://nos.lol" "wss://relay.damus.io")
# Replace 30617 with whatever kinds your artifact uses
{ for k in 30617 30618 1; do
nak req -k "$k" -a "$PUBHEX" ws://localhost:8081 2>/dev/null
done
} | jq -c 'select(.id != null)' | sort -u > /tmp/events.jsonl
while IFS= read -r ev; do
echo "$ev" | nak event "${PUBLIC[@]}" >/dev/null 2>&1
done < /tmp/events.jsonl
Expect 2-of-3 success on any given event. Public relays sometimes rate-limit, content-policy, or blip. As long as one accepts each event, discovery still works.
2. Mirror Blossom blobs to a public server
NSEC=$(jq -r .nsec /dev/shm/nostr-dev-session/keys.json)
# Collect every sha256 your events reference (`x` tags, `primary`, `ots`,
# whatever your kinds use):
SHAS=$(jq -r 'select(.kind==<your-kind>) | .tags[] | select(.[0]=="x") | .[1]' \
/tmp/events.jsonl | sort -u)
for sha in $SHAS; do
curl -sLf -o /tmp/blob "http://localhost:8081/$sha"
nak blossom --sec "$NSEC" -s https://blossom.primal.net upload /tmp/blob >/dev/null
done
rm /tmp/blob
unset NSEC
Recommended public Blossom servers as of writing:
- https://blossom.primal.net — large, reliable, no auth quirks
- https://blossom.band — index-friendly
- Run blossom-audit against a server before relying on it: npx blossom-audit audit https://server bitcoin --sec <ephemeral nsec>
3. Push the GRASP repo to a public host
relay.ngit.dev runs the same ngit-relay software as the local stack
and accepts pushes from any pubkey, validated against on-relay
kind:30617 events. So the same ngit init workflow you used locally
also works against the public GRASP — just point it there:
cd <repo-working-tree>
NSEC=$(jq -r .nsec /dev/shm/nostr-dev-session/keys.json)
ngit init -d \
--name "<repo-name>" \
--identifier "<d-tag>" \
--description "Public mirror." \
-g wss://relay.ngit.dev \
-g ws://localhost:8081 \
-n "$NSEC"
unset NSEC
After this, the kind:30617 announcement on relay.ngit.dev lists both clone URLs — public first, local as fallback. A browser visiting the public URL clones from relay.ngit.dev. A consumer on this machine can still use localhost.
4. (If applicable) Publish the client as an nsite
For a static-site artifact (HTML/CSS/JS/wasm), this step has its own pattern
doc and its own script — nsite-publishing.md and
bin/nsite-deploy.py. Don't hand-roll it; five
people already did and each hit the same three traps.
~/Documents/nostr-dev/bin/nsite-deploy.py --dir dist \
--keys ~/Documents/nostr-dev/secrets/<site>/keys.json \
--title "…" --description "…"
Result: anyone visits https://<your-npub>.nsite.lol/ and the gateway
fetches kind:15128, resolves each path's blob from public Blossom, and
serves it.
The three traps, in one line each: publish under a dedicated key (15128 is
replaceable per pubkey, so the wrong key destroys an existing site); the events
must reach wss://relay.nsite.lol; and https://nostr.download goes
first in the server list because blossom.primal.net serves every text/*
asset as text/plain, which silently kills stylesheets and service workers.
5. Update the client's defaults to point at public infrastructure
If your client has hardcoded ws://localhost:8081, switch to
wss://relay.ngit.dev (or whichever public relays apply) before
publishing. Keep the localhost URL as a fallback in the user's UI, not
the default.
What still requires this machine
- None of the above for visitors. Once mirrored, the artifact is publicly reachable.
- Producing new content. Whatever pipeline writes to the local
Blossom + signs events still runs here. Add
NAP_BLOSSOM_MIRRORS(or your equivalent env var) so each new blob auto-mirrors to public on the way out.
Failure modes to expect
| Symptom | Cause | Mitigation |
|---|---|---|
| Public Blossom 404 on a blob you uploaded yesterday | GC; some servers expire un-pinned blobs | Re-upload (idempotent — same sha256). Use a Blossom server that doesn't GC. |
| Public relay drops a kind:1 you sent | Rate limit or content policy | Mirror to ≥3 public relays. |
relay.ngit.dev is down |
Service outage | The kind:30617's clone tag lists local fallback (only useful from this network). For real production, mirror the repo to a second public GRASP. |
nsite.lol is down |
Gateway outage | Try <npub>.nsite-host.com or any other nsite gateway. The kind:15128 lives on relays; any gateway implementation can serve it. |
| Browser CORS error against the local GRASP | The local container's nginx + Khatru both emit CORS headers | See docs/known-issues.md § "ngit-relay double CORS headers" |
What this pattern doesn't solve
- Inbound producer access. If you want others to push to your machine, you need a tunnel or a real public IP. This pattern is specifically about making locally-produced artifacts consumable publicly.
- DMs and inbox-routed content. NIP-65 inbox semantics still mean you have to publish to the recipient's INBOX, not your own outbox. Going public for outbox-routed content is what this pattern handles.
- Bandwidth costs at scale. If the public Blossom server you mirror to charges for bandwidth, viral content is your problem. Mirror to multiple servers; if one buckles the others stay up.
Related
configs/local-services.json— where the local services livedocs/sources/blossom.md— Blossom mechanics (the blob mirror step)docs/sources/outbox-model.md— relay routing semantics (the event mirror step)docs/known-issues.md— the CORS quirk, Khatru kind:30617 eviction, and §7 the nsite trap set; all relevant when going publicpatterns/nsite-publishing.md— step 4 in full