name: p2p-design-gate description: Mandatory gate for any feature design involving data entities (tables, models, routes, sync messages) OR identity/agency/role/capability framing. Forces P2P-native thinking — DHT entry types, content addressing, source-of-truth classification, and identity-ontology framing (imago-dei, not crypto self-sovereignty) — before proposing design approaches. Use when brainstorming any feature that creates, stores, references, or syncs data entities, or that names an identity/agency tier. metadata: author: elohim-protocol version: 1.1.0
P2P Design Gate
This skill is a mandatory checkpoint during feature design. It fires between brainstorming (understanding what we need) and design proposal (how we build it). No data entity may be proposed — no table, no model, no route, no sync message — without passing through this gate first.
When This Gate Fires
This gate is not optional. It activates whenever a design conversation involves:
- Creating a new database table or migration
- Defining a new model, struct, or TypeScript interface for persistent data
- Adding an HTTP route that serves or mutates data
- Designing a sync/gossip message between peers
- Proposing a new "entity" of any kind
Sequence: The gate sits between step 2 (understanding the domain need) and step 3 (proposing a design). You must complete the gate output before writing any schema, migration, or route code.
If you find yourself reaching for CREATE TABLE or #[derive(Serialize)] before completing this gate, stop. Back up. Run the gate.
DHT Capacity Constraints (READ FIRST)
The Holochain DHT is a notary, not a database. Hard constraints shape every classification decision:
| Constraint | Limit | Current State |
|---|---|---|
| Entry types per DNA | ~100 | Lamad: ~73 (freed by mishpat split), Mishpat: 11, Imagodei: 28, Infrastructure: 6 |
| Total DHT entries | ~3000 before degradation | Designed for 100s-1000s |
| Entry size | <1KB target | Proofs only: who (agent key), what (content hash), when (timestamp) |
| Query capability | None — link traversal only | No SQL, no pagination, no filtering |
| Gossip latency | 200-2000ms | Unacceptable for real-time reads |
Before proposing ANY new DHT entry type: Check if the DNA has headroom. Lamad at 73/100 means you have some breathing room after the mishpat split, but still check existing entry types first. Mishpat (governance) is at 11/~100 with room for growth. Most entities that need notarization already have entry types, not create one. Most entities that need notarization already have entry types — the gap is usually the missing dht_anchor_hash in the storage projection, not a missing entry type.
Step 1: Entity Classification Decision Tree
Every data entity falls into exactly one of five categories. Walk the tree for each entity in the design.
Category A: Notarized (existing entry type)
Examples: content items, economic events (REA), attestations, stewardship allocations, governance proposals, relationships between agents.
Test: Would the protocol be lying if this data were silently changed or deleted? AND does a Holochain entry type already exist for this?
Requirements:
- Uses an EXISTING Holochain DHT entry type (do NOT create new ones without checking DNA capacity)
- MUST have
dht_anchor_hash NOT NULLin the SQLite storage projection - Source of truth is Holochain DHT — the SQLite row is a read-optimized projection, not the canonical record
- Post-commit signal projects the entry to elohim-storage for fast query
Category A2: Derived (anchored via link to existing entry type)
Examples: content_tags (link metadata on Content entry), path chapters/steps (links on LearningPath entry), stewardship_allocations (link metadata on Agreement entry).
Test: Does this data need notarization, but it's really a relationship or attribute of an already-notarized entity — not a standalone entity?
Requirements:
- Does NOT need its own DHT entry type — anchored via Holochain Link on an existing entry
- Link tag carries the metadata (type, weight, role — small, <256 bytes)
- Storage projection has
dht_anchor_hashpointing to the PARENT entry's ActionHash - Storage projection denormalizes for query convenience, but the link is the truth
When to use A2 instead of A: If the entity has no meaning without its parent (a tag without content, a step without a path, an allocation without an agreement), it's derived, not standalone.
Category B: Agent-Scoped (private)
Examples: user preferences, display settings, schedules, session state, draft content, personal bookmarks.
Test: Does this data belong to one agent and only matter to them? Would other peers never need to validate it?
Requirements:
- Private source-chain entry on Holochain (not gossipped to DHT)
- Linked to notarized content by
EntryHashwhere applicable - SQLite projection exists for fast local query only — it is not the source of truth
- If the agent migrates devices, this data travels via source-chain export/import
- No HTTP route exposes this to other agents (only the owning agent's UI reads it)
Category B2: Agent-Scoped with Notarized Attestation
Examples: content mastery (private progress, but gates governance participation), votes (private ballot, but tally must be verifiable), assessment responses (private attempt, but credential is public).
Test: Does this data belong to one agent, BUT does its effect need to be verifiable by peers?
Requirements:
- Raw data is a private source-chain entry (Category B)
- When the raw data produces a verifiable result (mastery level, vote tally, credential), a signed Attestation is issued on the DHT (using the existing Attestation entry type in imagodei)
- The Attestation is the public proof. The raw data stays private.
- Storage projection for the raw data is agent-scoped. Storage projection for the attestation has
dht_anchor_hash.
Pattern: Agent records private data → system evaluates → system issues Attestation → Attestation is notarized. This avoids putting granular data (every quiz answer, every scroll event) on the DHT while still providing verifiable proofs of outcomes.
Category C: Operational
Examples: cache entries, materialized views, temporary computation state, rate-limit counters, connection pool metadata.
Test: Could this data be deleted and reconstructed from notarized or agent-scoped sources? Is it ephemeral?
Requirements:
- SQLite-only is acceptable
- MUST document in a code comment why this entity is operational (not notarized or agent-scoped)
- No
dht_anchor_hashcolumn - Must declare a reconstruction strategy (how to rebuild from source-of-truth data if lost)
Decision Flowchart
Does the community need to witness/verify this data?
YES → Does a DHT entry type already exist for this?
YES → NOTARIZED (Category A)
NO → Is it a relationship/attribute of an existing entry?
YES → DERIVED (Category A2 — use Link, not new entry type)
NO → Is there DNA headroom? (Lamad: ~73/~100, Mishpat: 11/~100)
YES → NOTARIZED (Category A — create entry type)
NO → STOP. Refactor existing types or split DNA.
NO → Does this data belong to a single agent privately?
YES → Does its EFFECT need peer verification?
YES → AGENT-SCOPED + ATTESTATION (Category B2)
NO → AGENT-SCOPED (Category B)
NO → Is it reconstructable from other sources?
YES → OPERATIONAL (Category C)
NO → Go back. You missed something. It's probably A or A2.
Step 2: Content Address Strategy
For each entity, declare which addressing strategy applies. There are exactly three options.
Option 1: Content-Derived (CID)
The identity of the entity IS a hash of its content. If the content changes, the address changes — you get a new version, not a mutation.
Use when: The entity represents immutable content (articles, assessments, media, attestations). The canonical format is CIDv1 (bafkrei...).
Implication: No UPDATE semantics. New version = new CID. Version chains link CIDs together.
Canonical address forms — CID IS the address; sha256 is only the hash inside it
The recurring mistake this kills: exposing a bare sha256-<hex> (or a UUID) as a content/blob address, or putting a sha256-<hex> value in a field named cid. A CID is not a different hash — it is the same sha2-256 of the bytes, wrapped in a self-describing multihash + codec. "Use a CID" means stop exposing the bare hash; expose the CID that wraps it.
| What is being addressed | Canonical form | How it is minted | Example |
|---|---|---|---|
| Atom / DAG-CBOR content (EPR heads & atoms, manifests, content-set fingerprints, projection digests) | CIDv1, dag-cbor codec → bafyrei… |
Cid::new_v1(0x71 dag-cbor, Sha2_256(canonical-bytes)) — see elohim-storage/src/epr_codec.rs (DAG_CBOR_CODEC) |
bafyrei… |
| Raw blob bytes | CIDv1, raw codec → bafkrei… |
Cid::new_v1(0x55 raw, Sha2_256(bytes)) — see doorway/src/routes/blob.rs; the same sha256 you already compute, wrapped |
bafkrei… |
| Agent / action identity | Holochain hash → uhCAk… (agent key), action hash for actions |
conductor-minted; NOT a CID, NOT a bare sha | uhCAk… |
NOT addresses — keep these as bare sha256. The discriminator: does something resolve / dereference / fetch it? If no, it is not an address — leave it.
- Dedup / fingerprint keys — e.g.
fp = sha256(node|class|provenance)[:12](findings / runtime sentinels). An internal index key, never fetched. - Byte-equality verification — e.g.
sha256-verify the ts-rs codegen diff, blob-arrival integrity checks. Comparing bytes, not naming content. - Cite fingerprints —
cites:frontmattersha256:<hex>is thecite-gentool's content-address of a citation; it is tool-generated, a SEPARATE system — never hand-edit, never migrate to CID.
Legacy / in-migration: the bare sha256-<hex> blob wire marker (currently described in elohim-storage/CLAUDE.md / doorway/CLAUDE.md, on the /blob/<hash> path) is the legacy form; the canonical target is the wrapping CID bafkrei…. Moving the blob plane (/blob, BlobStore, inventory-gossip wire, seeder) from bare-hash to CID is a named downstream migration arc — describe current behavior accurately, design new surfaces CID-first.
Option 2: Agent-Scoped Composite
The identity is a tuple of (AgentPubKey, ContentEntryHash, type_discriminator). The agent's relationship to a piece of content is the identity.
Use when: The entity represents an agent's stance toward content — a vote, a bookmark, an assessment attempt, a stewardship claim. Two agents holding the same stance toward the same content produce two different entries.
Implication: Uniqueness is enforced by the tuple. Lookup is always "agent X's relationship to content Y of type Z."
Option 3: Slug or UUID
A human-readable slug or a random UUID serves as the identifier.
Use when: Neither content-derived nor agent-scoped composite applies. This is rare in the Elohim Protocol. You MUST justify why Options 1 and 2 do not apply.
Common justifications:
- Operational entity with no content to hash (e.g., a session token)
- Human-navigable identifier required before content exists (e.g., a community slug for URL routing)
- External system integration where the external ID is the canonical reference
Step 3: API Design Order
Design the API layers in this exact sequence. Do not skip ahead.
3a. Holochain Coordinator Function
What zome function creates or reads this entry?
coordinator zome: {zome_name}
create_{entity}(input: Create{Entity}Input) -> EntryHash
get_{entity}(hash: EntryHash) -> Option<{Entity}>
// For agent-scoped: get_my_{entity}s() -> Vec<Link>
Define the entry type, its validation, and its links FIRST.
3b. Post-Commit Signal and Storage Projection
What signal does the post-commit hook emit? What does elohim-storage do when it receives it?
post_commit: emit Signal::{Entity}Created { entry_hash, entry }
storage handler: INSERT INTO {table} (..., dht_anchor_hash) VALUES (..., ?)
For Notarized entities, the storage projection is a read-optimized cache. For Agent-Scoped entities, the storage projection is a local convenience index.
3c. HTTP Route (LAST)
Only after 3a and 3b are defined, design the HTTP route that exposes the projection.
GET /api/{entity}/{id} -> StorageProjection
POST /api/{entity} -> Calls coordinator create, returns EntryHash
The HTTP route serves the projection, not the source of truth. The route is the thinnest possible layer — validation and business logic belong in the coordinator zome.
Why this order matters: Starting with HTTP routes produces REST-shaped designs where the database is the source of truth. Starting with DHT entry types produces P2P-native designs where the network is the source of truth and everything else is a projection.
Anti-Pattern Catalog
These are known regressions — design choices that have caused real bugs or architectural debt in this codebase. Check every entity against this table.
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| UUID primary key for a notarized entity | The EntryHash IS the identity. A UUID creates a second identity that can drift out of sync with the DHT. | Use dht_anchor_hash as the logical primary key. SQLite rowid is internal only. |
| REST route as the design starting point | Produces server-centric designs where the database is truth. Holochain becomes an afterthought bolted on later. | Start with the DHT entry type. The HTTP route is the last layer designed. |
| CID stored as a relational foreign key | The entity IS its content address. Storing a CID as an FK in another table creates a dangling reference when the content is versioned. | Use Holochain links between EntryHashes. Storage projections denormalize for query convenience. |
| Standalone table for agent state | Agent preferences/bookmarks/drafts in a shared table leak private data and create P2P sync conflicts. | Private source-chain entry with local storage projection. No shared table. |
| Three address formats left undefined | The same entity referenced by CID in one place, UUID in another, and slug in a third. Conversion bugs everywhere. | Declare one canonical address format per entity. Document it. All other formats are display aliases resolved at the edge. |
Bare sha256-<hex> exposed as a content/blob address, or a cid: field holding a sha256-<hex> |
A bare hash is not self-describing (no codec, no hash-fn tag) and silently competes with the CID it should be. Calling a sha a "cid" is the conflation that recurs. | Expose the CID that wraps the SAME bytes: bafyrei… (dag-cbor) for atoms/content, bafkrei… (raw) for blobs — sha2-256 is only the multihash inside it. Bare sha is for dedup keys / byte-verify / cite-fps only, never an address. See Step 2 "Canonical address forms." |
| Missing source-of-truth declaration | A table exists but nobody documented whether Holochain or SQLite is authoritative. Bugs appear when they disagree. | Every table's migration or schema file includes a comment: -- Source of truth: DHT or -- Source of truth: local (operational). |
| Creating new entry type when one already exists | Lamad DNA is at |
Check existing entry types first. Use Links (Category A2) for relationships. Only create new types if nothing fits and DNA has headroom. |
| Putting granular data on the DHT | Every quiz answer, scroll event, or preference on the DHT bloats gossip and exceeds the ~3000 entry budget. | Agent-scoped with attestation (Category B2): raw data stays private, signed proof of outcome is notarized. |
| Cross-namespace identity string-equality | The same agent has three identities (Holochain uhCAk… key, libp2p 12D3Koo… peer id, iroh NodeId). Joining/matching one against another by raw string silently empties the join (caused the all-zeros resilience card, repeatedly). |
Resolve through the canonical agent↔transport resolver (the AgentPeerBinding projection / peer_transport_manifest). Never string-compare identities across namespaces; pick agent_cid as the canonical join key and resolve transport ids TO it. |
self-sovereign / "true data sovereignty" as the apex identity, agency, or capability tier |
Imports the silicon-crypto sovereignty ontology the protocol explicitly subordinates to community governance. The apex-tier label reads as a neutral capability level ("more keys → more autonomy → higher") and sails past review as a load-bearing ontological claim. Also silently excludes everyone who holds the right through others (children, IDD, seniors, wards). | Frame the high-autonomy tier as community-grounded (e.g. node-stewardship standing), not self-sovereign. Key-location is a mechanical fact (custodial → on-device → always-on), not an ascent toward sovereignty. Sovereignty is the adversary frame, never the protocol's own apex. Confirm the corrected lexicon with the architect. See "Identity Ontology Guard." |
Identity & Transport-Identity Coherence
agent_cid (uhCAk…) is the canonical agent identity throughout the protocol. The libp2p peer id (12D3Koo…) and the iroh NodeId are transport-plane identities that resolve TO agent_cid — they are NOT interchangeable with it.
The resolution substrate is the notarized AgentPeerBinding DHT integrity entry (projected by ReconcileController::on_agent_peer_binding into the peer_identity_bindings table), materialized locally in peer_transport_manifest (elohim/elohim-storage/src/p2p_iroh/peer_map.rs).
Rule: any new entity that references a peer, provider, or steward identity must declare which namespace it stores, and must resolve through the canonical resolver when joining or matching across namespaces. Never raw-compare agent_cid against a transport id.
Identity Ontology Guard (imago-dei floor, not crypto self-sovereignty)
This gate guards the framing of identity, not only its addressing. A human's identity in this protocol is imago dei — an inviolable right backstopped by community and institutional expression, not a self-asserted cryptographic primitive. Individual sovereignty is subordinated to community-adjudicated governance; it is never the apex value. Canonical home: genesis/docs/architecture/stewardship-over-sovereignty.md (Canon status: Foundational, "read it first" — "We do not consider sovereignty itself to be the right framing"; §3 reserves stewardship / agency / authority with discipline and explicitly excludes "sovereignty") and its life-stage companion genesis/docs/architecture/cradle-to-grave-capability-gradient.md. The imagodei domain gospel echoes it: identity grounded in "demonstrated capability and community trust."
The recurring drift this kills: naming the top of an identity / agency / capability gradient self-sovereign, or celebrating "true data sovereignty" as an achievement. AI agents default to the silicon-crypto sovereignty ontology because it dominates the training data, and it slips past review at tier-naming — an apex tier called "self-sovereign" reads as a neutral capability level ("more keys → more autonomy → higher") rather than the load-bearing ontological claim it actually is. This is the identity-vocabulary sibling of the relational-default mistakes above.
Rules when designing ANY identity, agency, role, capability, or key-location entity (enum, struct, schema, tier ladder — or the prose that documents one):
- Never make
sovereignty/self-sovereignthe positive apex of a gradient. Frame the high-autonomy tier as community-grounded — e.g. node-stewardship standing, not self-sovereign. Key-location is a mechanical fact (custodial→ on-device → always-on); do not dress it as an ascent toward sovereignty. - Sovereignty as an adversary frame is correct. Quoting "the crypto sovereignty frame" as the thing being resisted, or modeling state/platform sovereignty as a threat, is fine. Asserting it as the protocol's own top value is the drift.
- Model the whole human life. The ontology must hold for those who exercise the right with/through others — children, IDD, seniors, legal wards. The protocol expresses this as graduated, mediated agency (ward = "mediated agency, guardian co-authors"; voice-retention for seniors; supported decision-making) — the canonical §2 life-stage transition map is
genesis/docs/architecture/cradle-to-grave-capability-gradient.md. An identity model whose apex is "full autonomy, keys on device" silently excludes them. Note: this is canon-written, concrete ward/guardian specs pending — there is no guardian/ward DHT entity built yet, andcustodialkey-holding is device-convenience, NOT incapacity-guardianship; do not conflate the two. - Cryptography is an ACCELERATOR of community recovery, never the gate — Shamir, threshold signatures, hardware-rooted attestation all speed the recovery paths but their absence must never prevent recovery (
stewardship-over-sovereignty.md§4;project_socially_derived_security). So "harden a high-risk user" (e.g. a dissident under state duress) means optional cryptographic hardening layered onto the social-recovery floor — NOT elevating them to a "more sovereign" tier. The protocol's standing defense against duress is the non-firable elohim-counsel (project_elohim_as_counsel), not stronger keys.
If a design names a tier self-sovereign, proposes sovereignty-as-achievement, or treats individual autonomy as the protocol's ceiling: stop, reclassify the framing, and confirm the corrected apex lexicon with the architect before writing the enum/schema. This gate stops new design-time bleeding; the existing leaks (the live agency ladder, hardware-spec.md "true data sovereignty") are a separate de-drift/rename pass.
Output Format
When the gate is complete, present the result in this format before proceeding to design proposals.
## P2P Design Gate: {Feature Name}
### Entity: {EntityName}
- **Classification**: Notarized (A) | Derived (A2) | Agent-Scoped (B) | Agent-Scoped+Attestation (B2) | Operational (C)
- **Justification**: {1-2 sentences on why this classification}
- **Content Address Strategy**: Content-Derived (CID) | Agent-Scoped Composite | Slug/UUID
- **Address Justification**: {Why this strategy, not the others}
- **Source of Truth**: Holochain DHT | Private Source Chain | SQLite (operational)
- **Coordinator Zome**: {zome_name}::{function_name}
- **Storage Projection**: {table_name} (dht_anchor_hash: yes/no)
- **HTTP Route**: {method} {path}
- **Anti-Pattern Check**: {Confirmed none apply, or list which were caught and corrected}
### Entity: {NextEntityName}
... (repeat for each entity)
### Design Constraints Discovered
- {Any cross-entity relationships, ordering dependencies, or migration concerns found during the gate}
Only after this output is complete and reviewed should design proposals (schemas, migrations, component architecture) proceed.
Key Files
| File | Purpose |
|---|---|
elohim/elohim-storage/src/views.rs |
Rust view types with #[derive(TS)] — the Rust-to-TypeScript boundary |
elohim/elohim-storage/src/migrations/ |
SQLite migrations — every table must declare source of truth |
elohim/sdk/storage-client-ts/src/generated/ |
Auto-generated TypeScript types from Rust views |
app/elohim-app/src/app/elohim/adapters/ |
Adapters that add computed fields — never transform wire format |
doorway/doorway-service/src/routes/ |
HTTP routes — the thinnest layer, designed last |
genesis/docs/content/elohim-protocol/protocol-specification.md |
Full EPR protocol specification |