Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Node API

REST and WebSocket API for ENC protocol nodes.


Table of Contents

API
  1. Overview
  2. Enclave API
  3. WebSocket API
  4. Proof Retrieval API
  5. Webhook Delivery
  6. Registry DataView API
Appendix

Overview

Base URL

https://<node_host>/

Content Type

All requests and responses use application/json.

Authentication

OperationMethod
CommitSignature over commit hash (Schnorr or ECDSA per alg)
QuerySession token (see Session)
PullSession token
WebSocket QuerySession token
WebSocket CommitSignature over commit hash (Schnorr or ECDSA per alg)

Endpoints Summary

Enclave API (all enclaves):
MethodPathDescription
POST/Submit commit, query, or pull request
WS/Real-time subscriptions

Request types: Commit, Query, Pull

Proof Retrieval API:
MethodPathAccessDescription
GET/:enclave/sthPublicCurrent signed tree head
GET/:enclave/consistencyPublicCT consistency proof
POST/inclusionRCT inclusion proof
POST/bundleRBundle membership proof
POST/stateRSMT state proof
Registry DataView API (Registry enclave only):
MethodPathDescription
GET/nodes/:seq_pubResolve node by public key
GET/enclaves/:enclave_idResolve enclave to node
GET/resolve/:enclave_idCombined enclave + node lookup
GET/identity/:id_pubResolve identity by public key

Enclave API


POST / (Commit)

Submit a commit to the enclave.

Detection: Request contains exp field.

Request:
{
  "hash": "<hex64>",
  "enclave": "<hex64>",
  "from": "<hex64>",
  "type": "<string>",
  "content": "<any>",
  "exp": 1706000000000,
  "tags": [["key", "value"]],
  "alg": "schnorr",
  "sig": "<hex128>"
}
FieldTypeRequiredDescription
hashhex64YesCBOR hash of commit (see spec.md)
enclavehex64YesTarget enclave ID
fromhex64YesSender's identity public key
typestringYesEvent type
contentanyYesEvent content (type-specific)
expuintYesExpiration timestamp (Unix milliseconds)
tagsarrayNoArray of [key, value] pairs
algstringNoSignature algorithm: "schnorr" (default) or "ecdsa". Omit for Schnorr.
sighex128YesSignature over hash (algorithm per alg)

Note: content_hash is NOT transmitted. The node computes content_hash = sha256(utf8_bytes(content)) for commit hash verification (see spec.md).

Response (200 OK): Receipt

{
  "type": "Receipt",
  "id": "<hex64>",
  "hash": "<hex64>",
  "timestamp": 1706000000000,
  "sequencer": "<hex64>",
  "seq": 42,
  "alg": "schnorr",
  "sig": "<hex128>",
  "seq_sig": "<hex128>"
}
FieldTypeDescription
typestringAlways "Receipt"
idhex64Event ID
hashhex64Original commit hash
timestampuintSequencer timestamp (Unix milliseconds) — recorded when sequencer finalizes the event, not client submission time
sequencerhex64Sequencer public key
sequintSequence number
algstringSignature algorithm from commit ("schnorr" or "ecdsa"). Omitted if "schnorr".
sighex128Client's signature (from commit, algorithm per alg)
seq_sighex128Sequencer's Schnorr signature over event

Note: Receipt omits enclave for privacy — client already knows which enclave it submitted to.

Errors:
CodeHTTPDescription
INVALID_COMMIT400Malformed commit structure
INVALID_HASH400Hash doesn't match CBOR encoding
INVALID_SIGNATURE400Signature verification failed
EXPIRED400exp < current time
DUPLICATE409Commit hash already processed
UNAUTHORIZED403Insufficient RBAC permissions
ENCLAVE_NOT_FOUND404Enclave doesn't exist
ENCLAVE_PAUSED403Enclave is paused
ENCLAVE_TERMINATED410Enclave is terminated
RATE_LIMITED429Too many requests

POST / (Query)

Query events from the enclave.

Detection: Request contains type: "Query" field.

Request:
{
  "type": "Query",
  "enclave": "<hex64>",
  "from": "<hex64>",
  "content": "<encrypted>"
}
FieldTypeRequiredDescription
typestringYesMust be "Query"
enclavehex64YesTarget enclave ID (plaintext for routing)
fromhex64YesRequester's identity public key
contentstringYesEncrypted payload (see Encryption)
Content (plaintext):
{
  "session": "<hex136>",
  "filter": { ... }
}
FieldTypeRequiredDescription
sessionhex136YesSession token (see Session)
filterobjectYesQuery filter (see Filter)

Note: enclave is plaintext for routing — node needs it before decryption.

Response (200 OK):
{
  "type": "Response",
  "content": "<encrypted>"
}
Response Content (plaintext):
{
  "events": [
    { "event": Event, "status": "active" },
    { "event": Event, "status": "updated", "updated_by": "<hex64>" },
    ...
  ]
}
FieldDescription
eventThe event object
status"active" — event is current; "updated" — superseded by Update event
updated_by(Present when status: "updated") Event ID of the superseding Update event

Note: Deleted events are NOT returned. To query deleted event IDs, use the SMT Event Status proof.

Errors:
CodeHTTPDescription
INVALID_QUERY400Malformed query structure
INVALID_SESSION400Session token verification failed
SESSION_EXPIRED401Session token expired
DECRYPT_FAILED400Cannot decrypt content
INVALID_FILTER400Malformed filter
UNAUTHORIZED403No read permission
ENCLAVE_NOT_FOUND404Enclave doesn't exist
RATE_LIMITED429Too many requests

WebSocket API

Real-time pub/sub for event subscriptions.

Endpoint: wss://<node_host>/

Subscription is automatic: First valid Query on a connection creates a subscription. Node assigns sub_id and begins streaming events.


Connection Model

DirectionTypeDescription
C → NQueryFirst valid Query subscribes, node assigns sub_id
C ← NEventStored events (encrypted)
C ← NEOSEEnd of stored events
C ← NEventLive updates (encrypted)
C → NCommitWrite event
C ← NReceiptWrite success
C ← NErrorWrite error
C → NCloseUnsubscribe from subscription
C ← NClosedSubscription terminated
C ← NNoticeInformational message

Client → Node

MessageFormatDescription
QuerySame as POST / (Query)First valid Query subscribes, node assigns sub_id
CommitSame as POST / (Commit)Write event, returns Receipt
Close{ "type": "Close", "sub_id": "<string>" }Unsubscribe from subscription

Note: Close unsubscribes from a single subscription. To close the entire WebSocket connection, close the WebSocket transport directly. Closing the transport terminates all active subscriptions.


Node → Client

Event (stored or live):
{
  "type": "Event",
  "sub_id": "<string>",
  "event": "<encrypted>"
}
End of Stored Events:
{
  "type": "EOSE",
  "sub_id": "<string>"
}

Write Success: Receipt (same as HTTP)

{
  "type": "Receipt",
  "id": "<hex64>",
  "hash": "<hex64>",
  "timestamp": 1706000000000,
  "sequencer": "<hex64>",
  "seq": 42,
  "alg": "schnorr",
  "sig": "<hex128>",
  "seq_sig": "<hex128>"
}
Write Error:
{
  "type": "Error",
  "code": "<CODE>",
  "message": "<reason>"
}
Subscription Closed:
{
  "type": "Closed",
  "sub_id": "<string>",
  "reason": "<reason_code>"
}
Closed Reasons:
ReasonDescriptionClient Action
access_revokedIdentity's read permission was revokedPermanent; re-subscribe if permission restored
session_expiredSession token expiredGenerate new session, re-subscribe
enclave_terminatedEnclave was terminatedPermanent; no recovery
enclave_pausedEnclave was pausedWait for Resume event, then re-subscribe
enclave_migratedEnclave migrated to new nodeQuery Registry for new node, re-subscribe there
Notice:
{
  "type": "Notice",
  "message": "<string>"
}

Node Processing

On Query:
  1. Verify session (same as HTTP)
  2. Check AC (read permission)
  3. Generate sub_id, store subscription
  4. Send matching events as Event messages (encrypted)
  5. Send EOSE message
  6. On new events matching filter → push Event to client
On Commit:

Same as HTTP, returns Receipt.

On Close:

Remove subscription. Terminate connection if none remain.


Connection Lifecycle

Termination conditions:
  • All subscriptions closed by client
  • All sessions expired
  • All access revoked
  • Client disconnects
Multi-key support:
  • One connection, multiple identities (from)
  • Each identity has own session
  • Session expiry only affects that identity's subscriptions

HTTP vs WebSocket

AspectHTTPWebSocket
QueryOne-time responseSubscribe + live updates
sub_idN/ANode assigns
CommitReceiptReceipt
SessionPer-requestCached per connection
StateStatelessSubscriptions

Proof Retrieval API

Endpoints for retrieving cryptographic proofs. Used by clients to verify events and state.

Access Control:
EndpointAccess
STHPublic
ConsistencyPublic
InclusionRequires R permission
StateRequires R permission

STH (Signed Tree Head)

GET /:enclave/sth

Returns the current signed tree head. Public endpoint for auditing.

Response (200 OK):
{
  "t": 1706000000000,
  "ts": 1000,
  "r": "<hex64>",
  "sig": "<hex128>"
}

See proof.md for STH structure and verification.


Consistency Proof

GET /:enclave/consistency?from=<tree_size>&to=<tree_size>

Returns consistency proof between two tree sizes. Public endpoint for auditing.

ParameterTypeDescription
fromuintEarlier tree size
touintLater tree size (omit for current)
Response (200 OK):
{
  "ts1": 500,
  "ts2": 1000,
  "p": ["<hex64>", ...]
}

See proof.md for verification algorithm.

Errors:
CodeHTTPDescription
INVALID_RANGE400from > to or invalid values
ENCLAVE_NOT_FOUND404Enclave doesn't exist

Inclusion Proof

POST /inclusion

Returns inclusion proof for a bundle. Requires R permission.

Request:
{
  "type": "Inclusion_Proof",
  "enclave": "<hex64>",
  "from": "<hex64>",
  "content": "<encrypted>"
}
Content (plaintext):
{
  "session": "<hex136>",
  "leaf_index": 42
}
Response (200 OK):
{
  "type": "Response",
  "content": "<encrypted>"
}
Response Content (plaintext):
{
  "ts": 1000,
  "li": 42,
  "p": ["<hex64>", ...],
  "events_root": "<hex64>",
  "state_hash": "<hex64>"
}
FieldDescription
tsTree size when proof was generated
liLeaf index
pInclusion proof path
events_rootMerkle root of event IDs in bundle
state_hashSMT root after bundle

See proof.md for verification algorithm.

Errors:
CodeHTTPDescription
INVALID_SESSION400Session token verification failed
SESSION_EXPIRED401Session token expired
UNAUTHORIZED403No read permission
LEAF_NOT_FOUND404Leaf index out of range
ENCLAVE_NOT_FOUND404Enclave doesn't exist

Bundle Membership Proof

POST /bundle

Returns bundle membership proof for an event. Requires R permission.

Request:
{
  "type": "Bundle_Proof",
  "enclave": "<hex64>",
  "from": "<hex64>",
  "content": "<encrypted>"
}
Content (plaintext):
{
  "session": "<hex136>",
  "event_id": "<hex64>"
}
Response Content (plaintext):
{
  "leaf_index": 42,
  "ei": 2,
  "s": ["<hex64>", ...],
  "events_root": "<hex64>"
}
FieldDescription
leaf_indexBundle's position in CT tree
eiEvent index within bundle
sSiblings for bundle membership proof
events_rootMerkle root of event IDs in bundle

See proof.md for verification algorithm.

Errors:
CodeHTTPDescription
INVALID_SESSION400Session token verification failed
SESSION_EXPIRED401Session token expired
UNAUTHORIZED403No read permission
EVENT_NOT_FOUND404Event doesn't exist
ENCLAVE_NOT_FOUND404Enclave doesn't exist

State Proof

POST /state

Returns SMT proof for a key. Requires R permission.

Request:
{
  "type": "State_Proof",
  "enclave": "<hex64>",
  "from": "<hex64>",
  "content": "<encrypted>"
}
Content (plaintext):
{
  "session": "<hex136>",
  "namespace": "rbac" | "event_status",
  "key": "<hex64>",
  "tree_size": 1000
}
FieldRequiredDescription
sessionYesSession token
namespaceYes"rbac" or "event_status"
keyYesIdentity public key (rbac) or event ID (event_status)
tree_sizeNoBundle index for historical state (omit for current)
Response Content (plaintext):
{
  "k": "<hex42>",
  "v": "<hex | null>",
  "b": "<hex42>",
  "s": ["<hex64>", ...],
  "state_hash": "<hex64>",
  "leaf_index": 999
}
FieldDescription
k, v, b, sSMT proof fields (see proof.md)
state_hashSMT root hash for verification
leaf_indexBundle index (0-based) containing this state
Verification Flow:

To fully verify a state proof is authentic and from the requested tree position:

  1. Verify SMT proof against state_hash (see proof.md)
  2. Request CT inclusion proof for leaf_index via POST /inclusion
  3. Verify CT inclusion: Recompute leaf as H(0x00, events_root, state_hash) and verify against signed CT root
  4. Verify STH signature to authenticate the CT root

This binds the SMT state to a specific, signed tree checkpoint. See proof.md for detailed algorithms.

Errors:
CodeHTTPDescription
INVALID_SESSION400Session token verification failed
SESSION_EXPIRED401Session token expired
INVALID_NAMESPACE400Unknown namespace
UNAUTHORIZED403No read permission
TREE_SIZE_NOT_FOUND404Historical state not available
ENCLAVE_NOT_FOUND404Enclave doesn't exist

Webhook Delivery

Node delivers Push messages via HTTPS POST to registered endpoints. See Appendix: Push/Notify for design rationale.


Grant with Push Endpoint

A Grant event with P (push) ops and an endpoint field registers a webhook endpoint. See spec.md for event structure and content fields.

Node maintains queue per (identity, url):

  • Aggregates events from all enclaves on this node where identity has P/N permission
  • Tracks single push_seq per queue
  • All enclaves in a Push delivery share the node's seq_priv for encryption
Multiple Endpoints:

An identity MAY register multiple webhook endpoints via separate Grant events with different endpoint values. Each (identity, url) pair has its own push_seq and event queue. Events are delivered independently to each endpoint. To replace an endpoint, submit a new Grant with the new URL; both endpoints remain active until explicitly revoked.

Ordering Guarantee:

Events within each enclave (push.enclaves[N].events) are ordered by sequence number ascending. Events from different enclaves have no guaranteed relative ordering. If global ordering is required, use per-enclave seq to reconstruct the timeline.

Endpoint Transition Atomicity:

When a Grant changes the webhook URL (new Grant with same role, different URL):

  1. Old and new endpoints operate as separate queues (no events lost)
  2. Old endpoint receives events finalized before the Grant
  3. New endpoint receives events finalized after the Grant
  4. To stop delivery to old endpoint, explicitly Revoke the role

Delivery Flow

1. Enclave submits Grant event with P ops + endpoint (grants role + registers url)
2. Node adds enclave to (identity, url) queue if not exists
3. On new event, node checks role's P/N permissions
4. Node aggregates events into (identity, url) queue
5. Node periodically POSTs Push to url
6. Node increments push_seq

Push

Webhook delivery containing full events (P permission) and/or event IDs (N permission).

HTTP Request:
POST <url>
Content-Type: application/json
Body:
{
  "type": "Push",
  "from": "<hex64>",
  "to": "<hex64>",
  "url": "<string>",
  "content": "<encrypted>"
}
FieldTypeDescription
typestringAlways "Push"
fromhex64Sequencer public key
tohex64Recipient identity
urlstringWebhook URL
contentstringEncrypted payload
Content (plaintext):
{
  "push_seq": 130,
  "push": {
    "enclaves": [
      { "enclave": "<hex64>", "events": [Event, ...] }
    ]
  },
  "notify": {
    "enclaves": [
      { "enclave": "<hex64>", "seq": 150 }
    ]
  }
}
FieldTypeDescription
push_sequintSequence number per (identity, url)
pushobjectFull events for enclaves with P permission
push.enclaves[].enclavehex64Enclave ID
push.enclaves[].eventsarrayArray of Event objects
notifyobjectLatest seq for enclaves with N permission
notify.enclaves[].enclavehex64Enclave ID
notify.enclaves[].sequintLatest sequence number in this enclave

Either push or notify may be omitted if empty.

Encryption: See Encryption.

Delivery semantics:
  • At-least-once delivery
  • Exponential backoff on failure
  • Recipient MUST dedupe by event.id (for push) or track last synced seq (for notify)

Expected response: 200 OK


Pull Fallback

If webhook delivery fails, recipient can pull missed batches.

Request:
{
  "type": "Pull",
  "enclave": "<hex64>",
  "from": "<hex64>",
  "content": "<encrypted>"
}

Note: enclave is required in the outer request for signer key derivation during decryption. Any enclave on the node where the identity has a registered webhook can be used.

Content (plaintext):
{
  "session": "<hex136>",
  "url": "<string>",
  "push_seq": { "start_after": 5, "end_at": 7 },
  "enclave": "<hex64>"
}
FieldTypeRequiredDescription
sessionhex136YesSession token
urlstringYesRegistered webhook endpoint
push_sequint, [uint, ...], or RangeYesBatch sequence(s) to retrieve
enclavehex64NoFilter results to single enclave
push_seq formats:
  • Single: 6 — returns batch 6
  • Array: [6, 7, 8] — returns batches 6, 7, 8
  • Range: { "start_after": 5, "end_at": 7 } — returns batches 6, 7
Response:
{
  "type": "Response",
  "content": "<encrypted>"
}
Content (plaintext):
[
  {
    "push_seq": 6,
    "push": { "enclaves": [...] },
    "notify": { "enclaves": [...] }
  },
  {
    "push_seq": 7,
    "push": { "enclaves": [...] },
    "notify": { "enclaves": [...] }
  }
]

Array of batches. Each batch has same structure as Push delivery content.

Range Handling:

When push_seq is a Range:

  • start_after / end_at are inclusive/exclusive as documented
  • Results are ordered by push_seq ascending
  • If enclave filter is provided, only events from that enclave are included in each batch
  • If enclave is omitted, all enclaves in the original batch are included
Retry Policy:

Node retries webhook delivery with exponential backoff per spec.md. After max retries exhausted, batch is moved to dead-letter queue. Recipient can recover via Pull fallback.

Encryption: Same as Query. See Encryption.


Registry DataView API

The Registry is an enclave with a DataView server that provides discovery endpoints. These endpoints are served by the Registry's DataView, not by the generic Enclave API.

Base URL: Registry node endpoint (discovered via bootstrap)


GET /nodes/

Resolve node by sequencer public key.

Path Parameters:
ParamTypeDescription
seq_pubhex64Sequencer public key
Request:
GET /nodes/a1b2c3...
Response (200 OK):
{
  "seq_pub": "<hex64>",
  "endpoints": [
    { "uri": "https://node.example.com", "priority": 1 },
    { "uri": "https://backup.example.com", "priority": 2 }
  ],
  "protocols": ["https", "wss"],
  "enc_v": 1
}
FieldTypeDescription
seq_pubhex64Sequencer public key
endpointsarrayEndpoints sorted by priority (1 = highest)
endpoints[].uristringEndpoint URI
endpoints[].priorityuintPriority (lower = preferred)
protocolsarraySupported protocols
enc_vuintENC protocol version
Errors:
CodeHTTPDescription
NODE_NOT_FOUND404Node not registered

GET /enclaves/

Resolve enclave to hosting node.

Path Parameters:
ParamTypeDescription
enclave_idhex64Enclave identifier
Request:
GET /enclaves/d4e5f6...
Response (200 OK):
{
  "enclave_id": "<hex64>",
  "sequencer": "<hex64>",
  "creator": "<hex64>",
  "created_at": 1706000000000,
  "app": "chat",
  "desc": "Team chat",
  "meta": {}
}
FieldTypeRequiredDescription
enclave_idhex64YesEnclave identifier
sequencerhex64YesCurrent sequencer public key
creatorhex64NoCreator's identity key
created_atuintNoCreation timestamp (Unix milliseconds)
appstringNoApplication identifier
descstringNoHuman-readable description
metaobjectNoApplication-defined metadata
Errors:
CodeHTTPDescription
ENCLAVE_NOT_FOUND404Enclave not registered

GET /resolve/

Combined lookup: enclave → node (convenience endpoint).

Path Parameters:
ParamTypeDescription
enclave_idhex64Enclave identifier
Request:
GET /resolve/d4e5f6...
Response (200 OK):
{
  "enclave": {
    "enclave_id": "<hex64>",
    "sequencer": "<hex64>",
    "creator": "<hex64>",
    "created_at": 1706000000000,
    "app": "chat",
    "desc": "Team chat",
    "meta": {}
  },
  "node": {
    "seq_pub": "<hex64>",
    "endpoints": [
      { "uri": "https://node.example.com", "priority": 1 }
    ],
    "protocols": ["https", "wss"],
    "enc_v": 1
  }
}
Errors:
CodeHTTPDescription
ENCLAVE_NOT_FOUND404Enclave not registered
NODE_NOT_FOUND404Sequencer node not registered

GET /identity/

Resolve identity by public key. Returns the identity's registered enclaves.

Path Parameters:
ParamTypeDescription
id_pubhex64Identity public key
Request:
GET /identity/a1b2c3...
Response (200 OK):
{
  "id_pub": "<hex64>",
  "enclaves": {
    "personal": "<enclave_id>",
    "dm": "<enclave_id>"
  }
}
FieldTypeDescription
id_pubhex64Identity public key
enclavesobjectMap of label → enclave_id
Errors:
CodeHTTPDescription
IDENTITY_NOT_FOUND404Identity not registered

Appendix


Session

Session tokens provide stateless authentication for queries.


Token Format

136 hex characters = 68 bytes

Bytes 0-31:   r          (Schnorr signature R value)
Bytes 32-63:  session_pub (x-only public key)
Bytes 64-67:  expires    (big-endian uint32, Unix seconds)

Client Derivation

1. expires = now + duration (max 7200 seconds)
2. message = "enc:session:" || be32(expires)
3. sig = schnorr_sign(sha256(message), id_priv)    // always Schnorr
4. r = sig[0:32]
5. s = sig[32:64]
6. session_priv = s
7. session_pub = point(s)
8. session = hex(r || session_pub || be32(expires))

Note: Session token derivation always uses Schnorr. The alg field does not apply to session tokens. Hardware signers that only support ECDSA (e.g., NFC/JavaCard) can submit commits but cannot create session tokens. Such devices require a software co-signer or proxy for query/subscription authentication.


Node Verification

O(1) EC math — no signature verification.

1. Parse: r, session_pub, expires from token
2. Check: expires > now - 60 (allow 60s clock skew)
3. Check: expires ≤ now + 7200 + 60 (allow 60s clock skew)
4. message = "enc:session:" || be32(expires)
5. expected = r + sha256(r || from || message) * from
6. Verify: session_pub == expected

Clock skew tolerance (±60 seconds) allows clients with slightly-off clocks to connect.

Curve Parameters:

All EC arithmetic uses secp256k1. The curve order is:

n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

All scalar operations (addition, multiplication) are performed modulo n.


Signer Derivation

Per-node signer for ECDH.

t = sha256(session_pub || seq_pub || enclave)
signer_priv = session_priv + t (mod n)
signer_pub = session_pub + t * G
Design Rationale:

The enclave ID is included in the signer derivation so that the same session_pub produces different signer keys for different enclaves. This provides per-enclave key isolation:

  • Session token is reusable across enclaves (same session_pub)
  • Derived encryption key (via signer_pub) is enclave-specific
  • A compromised signer key in Enclave A cannot decrypt messages for Enclave B

This is intentional and provides defense-in-depth.

Security Properties:
PropertyProtected?Reason
Cross-session reuseDifferent session_pub → different t → different signer
Cross-enclave reuseDifferent enclave → different t → different signer
Cross-node reuseDifferent seq_pub → different t → different signer
Same session + enclaveSame signerIntentional — enables session continuity

No additional replay protection is needed; the derivation inputs guarantee uniqueness.


Session Properties

PropertyValue
Max expiry7200 seconds (2 hours)
Timestamp unitSeconds (for uint32 compactness; API timestamps use milliseconds)
ReusableYes, until expiry
Per-node signerYes (different ECDH per node)
Multi-keyOne connection, multiple sessions

Filter

Query filter for event retrieval.


Structure

{
  "id": "<hex64> | [<hex64>, ...]",
  "seq": "<uint> | [<uint>, ...] | Range",
  "type": "<string> | [<string>, ...]",
  "from": "<hex64> | [<hex64>, ...]",
  "tags": { "<tag_name>": "<value> | [<value>, ...] | true" },
  "timestamp": "Range (Unix ms)",
  "limit": 100,
  "reverse": false
}

All fields are optional. Omitted field = no filter (match all).

Tags Filter:

The tags field filters events by tag presence or value:

  • { "r": "abc123..." } — events with r tag matching value
  • { "r": ["abc123...", "def456..."] } — events with r tag matching any value
  • { "auto-delete": true } — events with auto-delete tag (any value)
Default Sort Order:

Events are sorted by sequence number ascending unless reverse: true. This is the canonical enclave order.


Range

{
  "start_at": 100,      // >= 100
  "start_after": 100,   // > 100
  "end_at": 200,        // <= 200
  "end_before": 200     // < 200
}

Semantics

PatternMeaning
Top-level fieldsAND
Array valuesOR
Omitted fieldMatch all

Limits

FieldMax
id[]100
seq[]100
type[]20
from[]100
tags keys10
tags values per key20
limit1000

Examples

By type:
{ "type": "Chat_Message" }
By authors:
{ "type": "Chat_Message", "from": ["abc...", "def..."] }
Time range:
{ "timestamp": { "start_at": 1704067200000, "end_before": 1704153600000 } }
Resume from seq:
{ "seq": { "start_after": 150 }, "limit": 100 }
Newest first:
{ "type": "Chat_Message", "reverse": true, "limit": 20 }

Encryption

Client-node communication is encrypted using ECDH + XChaCha20Poly1305.


Overview

ContextClient KeyNode KeyHKDF Label
Query requestsigner_privseq_pub"enc:query"
Query responsesigner_privseq_pub"enc:response"
Pull requestsigner_privseq_pub"enc:query"
Pull responsesigner_privseq_pub"enc:response"
WebSocket eventsigner_privseq_pub"enc:response"
Push deliveryto (recipient)seq_priv"enc:push"

Query Encryption (Client → Node)

Client encrypts query content:

1. Derive signer from session:
   t = sha256(session_pub || seq_pub || enclave)
   signer_priv = session_priv + t (mod n)
 
2. Compute shared secret:
   shared = ECDH(signer_priv, seq_pub)
 
3. Derive key and encrypt:
   key = HKDF(shared, "enc:query")
   ciphertext = XChaCha20Poly1305_Encrypt(key, plaintext)

Node decrypts:

1. Derive signer_pub from session_pub:
   t = sha256(session_pub || seq_pub || enclave)
   signer_pub = session_pub + t * G
 
2. Compute shared secret:
   shared = ECDH(seq_priv, signer_pub)
 
3. Decrypt:
   key = HKDF(shared, "enc:query")
   plaintext = XChaCha20Poly1305_Decrypt(key, ciphertext)

Response Encryption (Node → Client)

Node encrypts response (HTTP and WebSocket):

shared = ECDH(seq_priv, signer_pub)
key = HKDF(shared, "enc:response")
ciphertext = XChaCha20Poly1305_Encrypt(key, plaintext)

Client decrypts:

shared = ECDH(signer_priv, seq_pub)
key = HKDF(shared, "enc:response")
plaintext = XChaCha20Poly1305_Decrypt(key, ciphertext)

Push Encryption (Node → Webhook)

Node encrypts webhook payload:

shared = ECDH(seq_priv, to)
key = HKDF(shared, "enc:push")
ciphertext = XChaCha20Poly1305_Encrypt(key, plaintext)

Recipient decrypts:

shared = ECDH(recipient_priv, seq_pub)
key = HKDF(shared, "enc:push")
plaintext = XChaCha20Poly1305_Decrypt(key, ciphertext)

Primitives

PrimitiveSpecification
ECDHsecp256k1
HKDFHKDF-SHA256
AEADXChaCha20Poly1305
HKDF Parameters:
IKM   = ECDH shared secret (32 bytes)
salt  = empty (no salt)
info  = UTF-8 encoded label string, NO null terminator
        e.g., "enc:query" = 9 bytes: 0x65 0x6E 0x63 0x3A 0x71 0x75 0x65 0x72 0x79
L     = 32 bytes (256-bit key)
XChaCha20Poly1305 Nonce:
nonce = random 24 bytes, prepended to ciphertext
ciphertext_wire = nonce || ciphertext || tag

Recipient extracts first 24 bytes as nonce before decryption.

Minimum Length: ciphertext_wire MUST be at least 40 bytes (24-byte nonce + 16-byte Poly1305 tag). Shorter values indicate malformed or truncated ciphertext — implementations MUST reject with DECRYPT_FAILED.


Error Codes

HTTP Status Mapping

HTTPCategory
400Client error (malformed request)
401Authentication error
403Authorization error
404Not found
409Conflict
410Gone
429Rate limited
500Internal server error
502Upstream unreachable
503Service temporarily unavailable

Error Response Format

{
  "type": "Error",
  "code": "<CODE>",
  "message": "<human readable>"
}

Error Codes

CodeHTTPDescription
INVALID_COMMIT400Malformed commit structure
INVALID_HASH400Hash doesn't match CBOR encoding
INVALID_SIGNATURE400Signature verification failed
INVALID_QUERY400Malformed query structure
INVALID_SESSION400Session token verification failed
INVALID_FILTER400Malformed filter
DECRYPT_FAILED400Cannot decrypt content
SESSION_EXPIRED401Session token expired
EXPIRED400Commit exp < current time
UNAUTHORIZED403Insufficient RBAC permissions
ENCLAVE_PAUSED403Enclave is paused
DUPLICATE409Commit hash already processed
NODE_NOT_FOUND404Node not registered
ENCLAVE_NOT_FOUND404Enclave not registered
IDENTITY_NOT_FOUND404Identity not registered
ENCLAVE_TERMINATED410Enclave is terminated
ENCLAVE_MIGRATED410Enclave has migrated to another node
RATE_LIMITED429Too many requests
INTERNAL_ERROR500Internal server error
WebSocket Errors:

WebSocket errors use the same JSON format as HTTP errors. HTTP status codes do not apply to WebSocket; use the application-level code field instead.


Push/Notify

Problem

A DataView server may have P/N permissions across hundreds of enclaves on the same node. Naive approach — one HTTP request per event per enclave — creates massive overhead.

Solution

Node aggregates events into a single queue per (identity, url) pair.

Enclave A ──┐
Enclave B ──┼──► Queue (identity, url) ──► Single POST to url
Enclave C ──┘

Why This Is Efficient

AspectBenefit
BatchingOne POST delivers events from many enclaves
Single sequenceOne push_seq for gap detection across all enclaves
Periodic aggregationNode batches events instead of instant push per event
Unified messagePush and Notify combined in single delivery

Pull Fallback Efficiency

When webhook delivery fails, recipient uses Pull to recover. The single sequence number makes this extremely efficient:

Without unified sequence (naive):
Recipient must track:
  - Enclave A: last_seq = 42
  - Enclave B: last_seq = 17
  - Enclave C: last_seq = 103
  ...hundreds of enclaves...
 
Recovery requires:
  - One Query per enclave
  - Complex state management
  - N round trips for N enclaves
With unified push_seq:
Recipient tracks:
  - push_seq = 130
 
Recovery requires:
  - One Pull request with Range
  - Returns all missed batches
  - Single round trip
Gap detection is trivial:
  • Received push_seq 5, then 8 → know you missed 6, 7
  • Pull with push_seq: { "start_after": 5, "end_at": 7 } returns both batches in one request

This is why the queue is per (identity, url) not per enclave — it enables O(1) state tracking regardless of how many enclaves you're subscribed to.

Push vs Notify (within same message)

A single Push delivery contains both:

  • push.enclaves[] — full events for enclaves with P permission
  • notify.enclaves[] — latest seq for enclaves with N permission
pushnotify
ContentFull eventsLatest seq only
Use caseReal-time syncLightweight alerts

Permissions

PermissionWhat it grants
P (Push)Receive full event content in push.enclaves[]
N (Notify)Receive latest seq in notify.enclaves[]
R (Read)Query full event content from enclave

Important: P delivers full content directly. N only delivers the latest seq — to fetch full events, you need R permission on that enclave.

Example:
Identity has:
  - Enclave A: P permission → full events in push.enclaves[]
  - Enclave B: N permission → seq in notify.enclaves[]
  - Enclave C: N + R permissions → seq in notify.enclaves[], can Query full events

If you only have N (no R), you know new events exist but cannot read their content. This is useful for:

  • Mobile push notifications (just alert user)
  • Protocol-level sync signals (trigger sync via other means)
  • Audit logging (record that activity occurred)

Typical Pattern

  1. Receive Push with notify.enclaves[].seq
  2. Query events with { "seq": { "start_after": last_synced_seq } } (requires R permission)
  3. Full events arrive directly in push.enclaves[] if you have P permission

Node Internal Table

Key:   (identity, url)
Value: { push_seq, enclaves[] }

For each enclave, node tracks which events to include based on role permissions.