Gopherholes served from Nostr relays.
Why gopher on Nostr · Getting started · Docs · FAQ · gopherkind.com · Support
Gopherspace has one endemic disease: holes die. The hobby box behind
your favourite phlog loses power, the domain lapses, and fifteen years
of writing are gone. gopherkind moves the hole off the box. Documents are
signed Nostr events (kind 31436, named for RFC 1436), published to relays
the author chooses, and any bridge that can retrieve a copy can serve them to
any gopher client written since 1991. lynx, VF-1, Lagrange, an Amiga. All fine.
The hole belongs to your npub, not to a hostname. If a bridge disappears, another bridge can serve the same signed events from the relays that still carry them. That removes the hostname as a single point of failure; it does not promise that relays will retain every event forever. Profiles, follows and the rest of the author's Nostr identity remain available to readers because the documents use that same pubkey.
Cameron Kaiser's essay on why gopher still matters (read it:
gopherkind read gopher://gopher.floodgap.com/0/gopher/relevance.txt)
lands on one idea: gopher divorces interface from information. Every
hole is menus and text, navigated the same way, rendered by anything
with a TCP stack. Sites stand on the strength of their content, not
the glitz of their bling.
Gopher's one structural weakness is that a hole is a host. The protocol never had a notion of authorship, so your writing lives and dies with a hostname and a power supply, and nothing in the wire format can prove who wrote what. That is precisely the shape of problem Nostr solves: identity is a keypair, documents are signed events, and the author can place copies on more than one relay.
The trade works in both directions. Nostr's clients are timelines, built for the scroll; long-form writing gets buried an hour after it is posted. Gopher is the opposite temperament: a reading room, where structure is mandatory and nothing fights for your attention. Kind 31436 gives gopher documents that are independent of one host, prove their author, and remain recoverable while a relay retains a copy. It gives Nostr a calm, hierarchical surface that a 1991 client can browse.
And because every npub already renders as a virtual hole, gopherspace quietly grows by the entire Nostr userbase. Kaiser wrote that gopher and the web should coexist. Add Nostr, and all three are reading the same content.
Two things in one small repo: a publisher that turns a directory of text files into signed events, and a bridge daemon that speaks gopher on one side (RFC 1436, default port 7070) and Gemini on the other (default 1965, TLS), both fed from the same events. Type 7 search works in gopher, the status 10 input flow in Gemini. NIP-40 expiry is enforced after replacement selection, so an expired latest revision makes the path absent rather than deliberately selecting older content when both revisions are visible.
The part I like most: every npub is already a hole. If someone has
never heard of gopherkind, the bridge builds their hole out of what they
have anyway. Kind 0 profile becomes /profile.txt, top-level notes
become a phlog at /notes, replies and mentions get their own views,
thread pages add context, NIP-23 long-form articles land under /articles,
and /feed.xml is an Atom feed. Point lynx at any npub and start reading. Authored
documents shadow the virtual paths, so taking over your own hole is
just publishing.
Needs Node 24 or newer.
npm install --global gopherkind
# The case for all this, in your terminal
gopherkind why
# Browse gopherspace and Nostr interactively
gopherkind
# Pair your existing NIP-46 signer. Your secret key never enters gopherkind.
gopherkind pair 'bunker://...'
# Publish the example hole through that signer.
gopherkind publish examples/hole
# Serve gopherspace. Gopher on 7070, Gemini on 1965, HTTP on 8070.
gopherkind serve
# Browse your hole, or anyone else's npub, published or not
lynx gopher://127.0.0.1:7070/1/npub1yourkey...From a clone, npm install once and use node src/cli.ts in place
of gopherkind; day-to-day source development needs no build step.
The public reference bridge is available on all three frontends:
- Gopher:
gopher://gopherkind.com/ - Gemini:
gemini://gopherkind.com/ - Web: https://gopherkind.com/
Every bridge explains itself at /about on all three, built from the same
document the terminal prints for gopherkind why. A reader who has never heard
of Nostr needs no account to get the point.
gopherkind has an identity of its own rather than borrowing the maintainer's,
because a hole belongs to a key. Its pages live in this repository's hole/
directory and are published from there, so the documentation is readable
through the thing it documents:
gopherkind read npub18y4d9g6gjc8n6vkqdq0wphh5zzt6zna9tleyvhwzw063pvr5p4fsv05p0s
gopherkind read gopherkind@gopherkind.com # the same hole, by NIP-05 nameUntil the first publication reaches relays, that npub renders as an ordinary empty virtual hole, which is itself the point: the address exists before the content does.
The web endpoint terminates TLS on the bridge host, so remote visitors can connect a NIP-07 browser extension or pair a NIP-46 signer. Port 8070 is not publicly reachable. The bridge is one route to relay-held events, not a promise of uptime or relay retention.
Nothing needs to be published for that to work. Point it at any npub and a hole is already there, generated from the events that account already has:
$ gopherkind read npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2
TheCryptoDonkey
===============
TheCryptoDonkey
a virtual hole generated from Nostr events
Bitcoin, freedom, decentralisation, liberty advocate.
Profile
/npub1mgvlrnf.../profile.txt
Notes
/npub1mgvlrnf.../notes
Articles (long-form)
/npub1mgvlrnf.../articles
Follows
/npub1mgvlrnf.../follows
Followers
/npub1mgvlrnf.../followers
Search
/_gopherkind/search/npub1mgvlrnf...
(npubs abbreviated above; the real output prints them in full.)
Long streams page rather than stopping dead, on every surface:
$ gopherkind read npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2/notes
...
Older
/npub1mgvlrnf.../notes/before/1774317715/7e4a...c912
Follow that link and you get the next page, plus a way back to the top. Same on gopher, Gemini and in lynx.
publish prints your hole's root selector when it finishes. Add
--dry-run to inspect the signed events without sending anything,
or --expire 30d for documents that should vanish on their own
(NIP-40; s/m/h/d/w all work).
Publishing discovers the author's current NIP-65 write relays, uses their union with the configured relays, spreads an existing signed relay list alongside the documents, and reads each document back from every destination. Acceptance and read-back are reported separately; a document rejected everywhere or not readable anywhere makes the command fail.
gopherkind inspect npub1... performs the read side of that check again later.
It shows how many current documents are readable from each configured,
hinted and author write relay, including stale and missing paths. Add --json
for a versioned machine-readable report suitable for monitoring and recovery
checks. Either form is a snapshot of retrievability now, not a retention
guarantee.
To recover a hole into editable files:
gopherkind export npub1... ./recovered-hole
gopherkind publish ./recovered-holeThe export includes .gopherkind.json, which maps each source file back to
its exact path, item type and title. The manifest is authoritative when that
directory is published, so coordinates which cannot be represented safely by
filename conventions still round-trip. Export refuses a non-empty directory;
use --force only when deliberately replacing an earlier snapshot.
To take something down, gopherkind unpublish /about.txt (or --all)
sends a NIP-09 deletion request. Honest caveat: relays are free to
ignore those. For deliberately temporary material, use --expire from
its first publication; the bridge refuses to serve an expired event it
has received.
One subtlety matters for revised documents: a relay may delete an expired
latest event while still retaining an older revision. A reader arriving later
cannot know that missing replacement existed. --expire is therefore not a
durable tombstone for a path with older history; coordinate deletion is still
needed if that distinction matters, and deletion itself remains best-effort.
A hole is a directory. Text files become type 0 documents.
index.map (or classic gophermap) becomes the menu for its
directory, and any other *.map file becomes a menu at its own path.
index.map is a kindmap, which is a gophermap with the host and
port columns chopped off, because your documents don't have a host.
Plain lines are info text. Linked lines are <type><display>, a tab,
then the link:
Welcome to my gopherkind
0About this hole /about.txt
1Phlog /phlog
7Search this hole /
1A friend's hole npub1friend...
0Their best article naddr1...
1Floodgap (legacy gopherspace) gopher://gopher.floodgap.com/1/
hMy website https://example.com
Links can be same-hole paths, Nostr entities (npub, nprofile, or
naddr pointing at a kind 31436 document or a kind 30023 article), a
gopher:// URL (kept on its real host), or a web URL (served as an
h item). Pasting in an old gophermap mostly just works; the extra
columns are ignored.
Pages are editable after the fact. Kind 31436 events are addressable:
the path is the d tag, so editing a file and running publish
again replaces the document at that path everywhere, no dead links,
no cache busting. The same applies to kind 30023 articles, which you
can keep editing in any long-form Nostr client; every hole serves the
latest version.
Paths are exact identifiers. A bridge does not trim trailing spaces,
collapse slashes, case-fold, or Unicode-normalise a signed d. In a URL,
/a b becomes /a%20b, while the different document /a%20b becomes
/a%2520b.
gopherkind serve --host 0.0.0.0 --port 7070 --public-port 70 \
--gemini-port 1965 --no-identity --no-local-trust \
--hostname gopher.example.org \
--relay wss://relay.damus.io --relay wss://nos.lol \
--pin npub1somehole...That direct-public profile is deliberately read-only over HTTP. To put the HTTP frontend behind a same-host TLS reverse proxy and allow visitors to pair their own signer, keep port 8070 private and make the trust boundary explicit:
gopherkind serve --host 0.0.0.0 --port 7070 --public-port 70 \
--hostname gopher.example.org --no-local-trust \
--http-url https://gopher.example.org --http-behind-proxyThe proxy must be the only route to port 8070, preserve Host, set
X-Forwarded-Proto: https, and replace X-Forwarded-For with the single
client address. The bridge then emits canonical and social-preview metadata
for the HTTPS origin and rate-limits visitors separately. NIP-05 names work in
the HTTP opener and as direct paths, but redirect to an npub URL so links are
portable between bridges.
Default relays are Damus, nos.lol, Primal and relay.trotters.cc, the
project's own. That last one is there because a general-purpose relay may
refuse an unfamiliar kind: Damus rejects every kind 31436 document, so a hole
published only to the popular set can be unreadable through them. --relay (repeatable)
replaces them. --hostname and --public-port control the address
written into gopher menus, which matters behind NAT or a port
redirect. --pin puts holes on the welcome menu under their profile
names. --home <npub> goes further and serves that hole's own root menu in
place of the welcome page on every frontend, so a bridge's front door is
content rather than an explanation of what a front door is. Reading anyone
else and managing your own pages stay one line below it, and an unreachable
home falls back to the generic menu rather than an error.
Gemini needs TLS, so the bridge generates a self-signed cert with
openssl into --state-dir (default ~/.gopherkind) on first run.
Bring your own with --cert and --key, or skip the whole frontend
with --no-gemini. --no-virtual turns virtual holes off if you
only want authored content.
Bridges are meant to be boring to operate: no database. The state directory contains the TLS certificate, NIP-46 pairing records and local bookmarks. Relay responses are cached for a few minutes (longer for immutable things like note bodies), and each IP gets a token bucket (burst of 20, refills 1/s) so a scraper can't turn your bridge into a relay cannon. Search uses NIP-50 where a relay supports it and quietly falls back to grepping fetched events where it doesn't.
Reads follow the author's NIP-65 relay list (kind 10002), so a hole
published to someone's own relays is found even when your bridge does
not carry those relays, not silently replaced by their virtual hole.
A relay hint inside an nprofile or naddr link is honoured the same
way. Hinted relays and public bunker relays are checked again during DNS
lookup at socket connection time, and loopback, private and link-local answers
are refused. The built-in gopher proxy applies the same network boundary, and
/robots.txt keeps crawlers out of it.
The HTTP listener exposes GET /healthz for service checks and the process
closes its listeners and relay pool on SIGTERM or SIGINT. The container,
reverse-proxy and rollback details are in the
operations guide.
Release tarballs and checksums are attached to GitHub releases even while the first npm registry publication remains an owner-controlled bootstrap. The exact release procedure and registry gate are in the release guide.
Once the bridge is up, tell Nostr clients it exists:
gopherkind announce --hostname gopher.example.org \
--http-url https://gopher.example.org --dry-runThat builds a NIP-89 handler announcement (kind 31990) saying this
bridge opens kind 31436. Drop --dry-run to sign and publish it.
The proposed event format is deliberately small: SPEC.md
defines kind 31436, exact paths and kindmap. Virtual holes, pagination,
search and frontend routing live separately in the
bridge profile.
Every hole has these paths, whether or not anyone published anything:
| Path | What it is |
|---|---|
/ |
root menu |
/profile.txt |
kind 0 profile |
/notes, /notes/<id> |
recent top-level notes |
/replies |
kind 1 replies which tag the hole owner |
/mentions |
top-level kind 1 mentions which tag the hole owner |
/threads/<id> |
note context and replies found on the bridge relays |
/articles, /articles/<naddr> |
NIP-23 long-form |
/feed.xml |
Atom feed of recent notes and articles, using nostr: IDs |
/follows |
who they follow, each linking to that person's hole |
/followers |
who follows them (a sample of what relays carry) |
So gopherkind read npub1.../follows walks the social graph from the
terminal, and the same paths are links in lynx, Lagrange, or any gopher
client. Your own feed lives at /me/feed over gopher, /feed over
HTTP and Gemini, or gopherkind feed in the terminal.
gopherkind is a gopher client as well as a gopher server, so old holes render through it too:
lynx http://localhost:8070/gopher/gopher.floodgap.com/1/Menus, text files and type 7 searches all work, and links inside a
proxied menu stay inside the proxy, so you can wander Floodgap and the
rest of gopherspace from a browser that has never heard of gopher.
Kindmap gopher:// links route through it automatically. lynx can
of course also just speak gopher directly.
Traditional menus which use ANSI SGR colours or text attributes retain that styling in the graphical web frontend. Other terminal controls stay inert; cursor movement, terminal hyperlinks and clipboard commands are never passed through as browser behaviour.
lynx is not only a gopher client, it speaks HTTP, so the bridge serves
a third frontend built for it: plain HTML and real forms whose complete
NIP-46 path needs no JavaScript. Start gopherkind serve and point lynx at
http://localhost:8070/.
Requests from loopback are treated as you, the operator, using the
remote signer that gopherkind pair stored. No login, no cookies, no
certificates: open lynx on your own
machine and you are already signed in. You get menus and documents,
search forms, your feed, a note composer and a page workspace at /me.
The workspace lists your authored paths and lets you add a page, open it,
edit and republish it in place, or publish a signed deletion request. Your own
notes also carry a deletion button.
HTTP and Gemini search lives at /_gopherkind/search/<npub>, outside
the authored hole namespace. A document published at /search is
therefore served normally rather than being intercepted by the bridge.
Remote visitors can get the same pages, pair a NIP-46 signer through a form or
connect a standard NIP-07 browser extension, and carry a session cookie when
the listener sits behind TLS. A non-loopback bind
requires --http-behind-proxy, an HTTPS --http-url, and
--no-local-trust; the HTTP port must not be exposed directly. Identity is
disabled automatically on an ordinary public bind, so direct plaintext
deployment is read-only. Turn the frontend off entirely with --no-http.
Operator trust is decided by connection origin (loopback), so behind a
reverse proxy every request would look local. Pass --no-local-trust for any
proxied deployment. A same-host proxy can reach a 127.0.0.1 bind; a
container-network proxy uses the explicit --http-behind-proxy contract.
--trust-loopback-anyway is only for a proxy that cannot be reached by anyone
but you. State-changing forms carry a CSRF token and reject cross-site origins,
and the loopback operator is recognised only on a loopback Host, so a stray
browser tab or a rebound domain cannot post as you.
Gopher over the network is read-only, and always will be: the protocol has no authentication and no encryption, so any credential sent over it would be public.
On loopback none of that applies, because no credential is sent at all.
The bridge knows the request came from this machine, and uses the same
signer the CLI uses. So a local gopher client gets a personal menu at
/me:
lynx gopher://127.0.0.1:7070/1/meFeed, your notes with delete links, who you follow, your followers, and posting. Writes use type 7 items, whose search string is the only input channel RFC 1436 ever offered, so you compose a note in the same prompt gopher used for Veronica queries in 1992. Deleting asks you to type the word "delete" to confirm.
Remote clients asking for /me get a polite type 3 error, and the
welcome menu only advertises it locally. --no-local-trust turns it
off entirely.
You do not need a Gemini client, a browser, or a certificate to use gopherkind. Run it bare and you get an interactive browser in the VF-1 tradition that speaks both gopherspace and Nostr:
$ gopherkind
gopherkind
======
a gopher client that speaks nostr
somewhere to start:
[1] Floodgap, the heart of gopherspace
[2] Why is gopher still relevant?
[3] Veronica-2, search all of gopherspace (?)
home> go npub1mgvlrnf...
TheCryptoDonkey
===============
[1] Profile
[2] Notes
[3] Articles (long-form)
[4] Follows
[5] Followers
[6] Search (?)
npub1mgvlrnf...> 2
Type a number to follow a link, back, up and reload to move,
mark to bookmark the page you are on. Links marked (?) prompt for
a search query: Veronica-2 for gopherspace, NIP-50 for holes. A menu
from Floodgap and a menu from an npub are the same thing to the
browser, and when a traditional gophermap links into a gopherkind bridge
(an npub selector, or a nostr: URL), the client follows it natively
through your own relays instead of someone else's bridge.
With a signer paired, feed renders notes from who you follow as a
navigable menu, and post <text> signs and broadcasts a note without
leaving the browser. Long documents go through $PAGER.
One-shot commands cover both worlds too, and reading needs no identity at all:
gopherkind read npub1... # someone's hole, virtual or authored
gopherkind read npub1.../notes # their phlog
gopherkind read someone@example.org # NIP-05 names work anywhere a target does
gopherkind read gopher://gopher.floodgap.com/1/ # traditional gopherspace
gopherkind search npub1... gopher # search a hole
gopherkind search gopher://gopher.floodgap.com/7/v2/vs nostr # Veronica-2Writing always uses a remote NIP-46 signer. GOPHERKIND_BUNKER provides a
one-off bunker URI; otherwise gopherkind uses the pairing that gopherkind pair stored. Local secret keys and nsec input are never accepted.
gopherkind pair bunker://... # once; stored in the state dir
gopherkind whoami
gopherkind post "hello gopherspace" # signed by your signer, broadcast
gopherkind feed # notes from who you follow
gopherkind delete <id|note1|nevent1> # NIP-09 deletion request
gopherkind unpairgopherkind delete --wide also sends the request to a broader relay set
than you read from, which matters because content spreads further than
your own relay list. Deletion is a request: relays may ignore it, and
clients keep local caches. If what you deleted contained a secret,
rotate the secret too.
gopherkind post --dry-run prints the signed event without sending it.
Every command accepts --relay (repeatable) and --state-dir.
Open /account in Chrome or another graphical browser and gopherkind detects
the standard window.nostr interface supplied by a NIP-07 extension. Choose
Connect browser extension and approve the public-key and connection
requests in the extension. Bark and other conforming providers use the
same interface; gopherkind does not contain extension-specific integration.
The connection request is a fresh NIP-98 event scoped to the exact bridge URL and HTTP method. The bridge verifies its id, signature, author, timestamp and scope, refuses replay, and then issues a short-lived HTTP-only session cookie. Posting, publishing and deletion are signed inside the extension. Only the public signed event crosses to gopherkind, which verifies the session author and exact event template before sending anything to relays. A NIP-07 session can never borrow the bridge's NIP-46 signer.
Public browser identity requires the documented HTTPS reverse-proxy mode. Direct plaintext public HTTP deliberately stays read-only. The base pages, NIP-46 forms and all reading continue to work without JavaScript in lynx.
Every HTTP page has a back link. In a graphical browser it follows real browser history, while a direct visit or a text browser safely falls back to Home.
The terminal and the HTTP frontend cover most needs. The Gemini frontend exists for people already living in Geminispace, where client certificates are the native identity mechanism.
gopherkind binds that certificate to a NIP-46 remote signer. Visit
/account, pair once, and you're signed in. Lagrange will mint a
certificate for you, though its identity UI takes some getting used
to; if that sounds like hard work, use lynx over HTTP or the CLI
instead.
Pairing speaks the same dialect as the rest of the Nostr signer world:
paste a bunker:// URI from Signet, Amber, nsecBunker or nsec.app, or
use /pair/connect for cross-device NostrConnect, where your phone
approves a URI the bridge displays. After that:
/postwrites a kind 1 note. The bridge builds the event, your signer signs it, relays get it. If your signer is a Heartwood ESP32, posting to gopherspace is literally pressing a physical button./publishguides you through a plain-text page or a menu page, including a working kindmap example, then writes one kind 31436 document after explicit confirmation that the exact path will replace its current revision. It uses the same NIP-65 relay plan and acceptance/read-back checks as the CLI publisher./feedrenders your follows (kind 3) as a menu of recent notes, each linking into the author's hole./unpaircuts the link, and you can revoke the session on the signer too.
The bridge never holds a user key. Not pasted, not derived, not
cached. It keeps one small file (pairings.json in the state dir,
mode 600) mapping certificate fingerprints to bunker connections, and
every signer conversation has a hard timeout so a powered-off signer
can't wedge a request. Turn the whole layer off with --no-identity.
/post and publish both refuse content that looks like a
credential: a bunker:// or nostrconnect:// URI, an nsec or
ncryptsec, a bare secret= token. The pair and post inputs sit close
together, a remote signer will cheerfully sign whatever you hand it,
and relays do not forget. If you ever do leak a bunker URI, rotate the
secret on the signer; a deletion request is a polite suggestion, not
a recall.
Notes are single-line and capped at a few hundred characters, because a Gemini input is one URL line and a Gemini request URL may not exceed 1024 bytes once the text is percent-encoded. That constraint is honest to the medium; gopherspace was never the place for essays with inline video.
One clarification for Bark users: in Chrome, Bark injects the standard
window.nostr provider and the NIP-07 button uses it directly. Lagrange is
not a browser, so Bark cannot inject there; pair the Heartwood bunker behind
Bark over NIP-46 instead. Same signer, different front door.
Gopher stays read-only forever. The protocol has no authentication and no encryption, so any credential sent over it would be public. 1991 gets to read; 2026 gets to write.
SPEC.md has the proposed kind's event format, exact path rules, replacement semantics and kindmap grammar. The selector namespace, virtual views, pagination and frontend mapping are deliberately separate in the bridge profile. The language-neutral fixture and its field definitions are in the conformance guide. llms.txt is the condensed implementation guide for tools and language models.
Kind 31436 is not yet an accepted NIP. The proposal was submitted to
nostr-protocol/nips as PR #2429
on 3 August 2026 and remains unmerged. docs/nip-submission.md
records the proposal and its independent fixture evidence.
| Document | What it covers |
|---|---|
| Why gopher on Nostr | The case, and its honest limits |
| Getting started | Install, read, pair, publish, verify, serve |
| FAQ | Retention, deletion, keys, tokens, what this is not |
| Troubleshooting | Error messages and what to do about them |
| Operations | Running a public bridge properly |
| Bridge profile | Normative application behaviour |
| SPEC.md | Kind 31436 itself |
| Support | What funding buys |
docs/index.md is the full map.
gopherkind is MIT-licensed, unfunded work under ForgeSworn. No company, no token, no ads, no telemetry, no custody of anyone's key, and nothing to sell you. It is paid for in evenings and a small hosting bill, so funding buys the only thing the project is made of: consecutive evenings.
- Zap:
npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2 - Lightning:
profusemeat89@walletofsatoshi.com - Ko-fi: https://ko-fi.com/brays
- Geyser: https://geyser.fund/project/forgesworn
docs/support.md says what the money actually does, what it will never do, and the several ways to help that cost nothing.
MIT