> **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [index](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/llms.txt) · 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:

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

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:

- **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 `r` tag. 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:

```bash
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`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/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`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/sources/blossom.md) — content layer that pairs with this
  structural layer.
- [`docs/known-issues.md`](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/docs/design/known-issues.md) — Khatru's `kind:30617` eviction quirk that
  affects multi-repo archivers.
