Nostr Agent Onboarding · start here · index · source:
nostr-dev/docs/patterns/grasp-as-structural-layer.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: GRASP repo as the structural layer for non-code content
Source: developed and proven in
~/work/nostr-archive/(sibling agent's work). See~/work/nostr-archive/docs/archive-grasp-repo.mdfor the fullarchive-grasp/v1schema. This doc generalizes the pattern beyond archives.
NIP-34 GRASP repos are typically used for source code. They generalize: a GRASP repository is a useful structural layer for any content collection whose organizing tree changes over time and whose authorship matters.
When this pattern applies
If your problem looks like:
- "I have a curated set of items that lives somewhere else (Blossom blobs, third-party URLs, IPFS, local files)."
- "The set evolves: items added, renamed, moved, federated from other curators."
- "Each curatorial change should be attributed and history-preserving."
- "The set has a folder hierarchy or graph structure that's organizer-controlled."
…then a GRASP repo is probably a better structural layer than a Nostr addressable event (kind 30000-range) holding a JSON tree.
Why a GRASP repo beats a JSON-blob TOC
| Concern | JSON blob in Blossom + addressable event | GRASP repo |
|---|---|---|
| Atomic updates | Re-upload blob, re-publish event, hope the two land together | One commit |
| History | None (replaceable event evicts older versions) | git log is the history |
| Author attribution | Only on the wrapper event | On every commit |
| Federation | Custom: include other archivers' addresses in your blob | Native: Git submodule pointing at their repo |
| Concurrent maintainers | Hard | NIP-34's maintainers tag + signed pushes |
| Tooling | None standard | Every git client, every IDE, web UI (gitworkshop.dev) |
Minimal schema sketch
The archive project chose this layout, which is reusable verbatim for anything similar:
<repo-root>/
├── <root-metadata>.json # schema version + collection-level metadata
├── README.md # human-readable description
├── <folder>/
│ └── folder.json # per-folder metadata + references to content
│ (hierarchy is the on-disk directory tree — no `children` field)
└── ...
Two rules that fall out of "use Git's tree as the structure":
- Don't duplicate the tree in JSON. No
children, nopath-as-string. If the file is atpodcasts/bitcoin-and/folder.json, that's its path. - References to content go in
folder.jsonby Nostr coordinate (["nevent", "<id>"],["naddr", "<addr>"]) or sha256 (["x", "<sha256>"]) or external URL (["r", "<url>"]). The structural layer references; it doesn't embed.
Federation via Git submodules
The cleanest part of this pattern. A folder that's "really another archiver's collection mirrored here" is a Git submodule:
<my-repo>/
├── archive.json
└── partners/
└── alice/ # submodule → alice's GRASP repo at commit X
└── archive.json # alice's signed root, mounted under partners/alice/
Walking the consumer's side:
- Clone the parent repo from its GRASP server.
- On entering
partners/alice/, the submodule's.gitmodulesentry says where alice's repo lives. Usegit-remote-nostrto clone it (alice's URL is anostr://URL or a GRASP HTTP URL). - The pinned commit hash in the parent's tree object is alice's signed tree at that moment — the parent attests "I have seen this exact tree of alice's and chosen to include it here."
No new schema needed. Submodules already mean exactly this.
Storage layer remains independent
The structural repo references content; it doesn't host it. Content goes to:
- Blossom — when the items are arbitrary binary blobs you need to pin (audio, video, PDFs). Reference by sha256.
- Nostr — when the items are short-form, addressable, or already on
Nostr (kind:1 notes, long-form articles, etc.). Reference by
nevent/naddr. - External URLs — when the items are web resources you don't own.
Reference by
rtag. Consider also pinning a Blossom snapshot.
Local mechanics on this machine
The local GRASP server (http://localhost:8081, see
configs/local-services.json) accepts arbitrary repos under
/<npub>/<repo>.git. Same workflow as for code:
cd <repo-working-tree>
# init nostr identity for the repo (publishes kind:30617)
NSEC=$(jq -r .nsec /dev/shm/nostr-dev-session/keys.json)
ngit init -d --name "<name>" --identifier "<d-tag>" \
-g ws://localhost:8081 --repo-relay-only -n "$NSEC"
# routine commits, atomic per change
git commit -m "add: podcasts/<name>"
git push origin main # via the nostr:// remote
# release the credential
ngit account logout
The pre-receive hook on ngit-relay validates pushes against the on-relay kind:30617 announcement, so the same identity model that protects code repos protects the structural layer.
Trade-offs / when not to use this
- Single-document state. If your structure is one settings JSON and it has no history value, kind 10000-range replaceable is simpler.
- High write velocity. A commit per change is cheap, but if you're appending O(thousand) tiny changes per day, you're better off with a log of regular events and a periodic compaction. GRASP repos shine for human-paced curation, not high-frequency state.
- No federation requirement. If you're a single archiver and never expect a second one, the submodule story is unused; the simpler approach is also fine.
Related concepts in this workspace
docs/design-synthesis.md§3 — event-kind taxonomy and why defaulting to regular events composes with this pattern (curatorial changes are themselves regular kind:1115/1116-style events that the structural layer references).docs/sources/blossom.md— content layer that pairs with this structural layer.docs/known-issues.md— Khatru'skind:30617eviction quirk that affects multi-repo archivers.