Markdown source for agents: https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/patterns/grasp-as-structural-layer.md

Nostr Agent Onboarding · start here · index · source: nostr-dev/docs/patterns/grasp-as-structural-layer.md · snapshot 2026-10-10

Paths such as ~/Documents/…, ~/Production Environment/…, repos/… and services on localhost refer 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.md for the full archive-grasp/v1 schema. 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:

…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":

  1. Don't duplicate the tree in JSON. No children, no path-as-string. If the file is at podcasts/bitcoin-and/folder.json, that's its path.
  2. References to content go in folder.json by 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:

  1. Clone the parent repo from its GRASP server.
  2. On entering partners/alice/, the submodule's .gitmodules entry says where alice's repo lives. Use git-remote-nostr to clone it (alice's URL is a nostr:// URL or a GRASP HTTP URL).
  3. 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:

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