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

Production Deployment Guide

Deploy an ENC node to Cloudflare Workers.

Prerequisites

1. Generate node keypair

Every node needs a secp256k1 keypair. The private key signs tree heads (STH) and manifest commits. The public key is the node's identity — clients use it to verify STH signatures.

yarn keygen

Output:

NODE_PRIVATE_KEY=<64 hex chars>
NODE_PUBLIC_KEY=<64 hex chars>

Save the private key. You will set it as a Cloudflare secret. The public key is returned to clients when they create enclaves (seq_pub field).

2. Set the secret

npx wrangler secret put NODE_PRIVATE_KEY

Paste the 64-character hex private key when prompted. This is stored encrypted in Cloudflare — it never appears in your code or config files.

3. Configure wrangler.toml

The default config deploys to enc-node.<your-subdomain>.workers.dev:

name = "enc-node"
main = "js/node/worker/index.js"
compatibility_date = "2024-01-01"
workers_dev = true
 
[durable_objects]
bindings = [
  { name = "ENCLAVE", class_name = "EnclaveDO" }
]
 
[[migrations]]
tag = "v1"
new_sqlite_classes = ["EnclaveDO"]

To change the worker name (and thus the URL), edit the name field.

To use a custom domain, add:

routes = [
  { pattern = "node.yourdomain.com", custom_domain = true }
]

4. Deploy

yarn deploy

Output:

Total Upload: ~213 KiB / gzip: ~50 KiB
Worker Startup Time: 5 ms
Uploaded enc-node
Deployed enc-node triggers
  https://enc-node.<your-subdomain>.workers.dev

5. Verify deployment

Health check

curl https://enc-node.<your-subdomain>.workers.dev/

Expected: {"error":"invalid_route","message":"Expected POST / or /enclave/:id[/:action]"} — this confirms the worker is running (the root route is not a valid endpoint).

Create an enclave

curl -X POST https://enc-node.<your-subdomain>.workers.dev/create-enclave \
  -H 'Content-Type: application/json' \
  -d '{
    "manifest": {
      "enc_v": 2,
      "nonce": 1,
      "RBAC": {
        "use_temp": "none",
        "schema": [
          {"event": "post", "role": "owner", "ops": ["C","U","D"]},
          {"event": "*", "role": "Public", "ops": ["R"]}
        ],
        "states": [],
        "traits": ["owner(0)"],
        "initial_state": {"owner": ["YOUR_PUB_KEY_HEX"]}
      }
    }
  }'

Expected: {"enclave_id":"...64 hex chars...","seq_pub":"...node public key..."}

API reference

Endpoints

MethodPathAuthDescription
POST/create-enclaveNoneCreate new enclave with manifest
POST/Signed commitSubmit commit (detected by exp field)
POST/NonePull events (type: "Pull", after_seq, limit)
POST/ECDHQuery events (type: "Query", encrypted)
GET/enclave/:id/sthNoneSigned tree head
GET/enclave/:id/inclusion?seq=NNoneInclusion proof for event N
GET/enclave/:id/consistency?size1=A&size2=BNoneConsistency proof
GET/enclave/:id/state?key=KNoneSMT state proof

Commit format

Commits are submitted as POST to / with the exp field present:

{
  "enclave": "64-hex-enclave-id",
  "from": "64-hex-public-key",
  "type": "post",
  "content": "{\"body\":\"hello\"}",
  "content_hash": "64-hex",
  "hash": "64-hex",
  "exp": 1700000000000,
  "tags": [],
  "sig": "128-hex-schnorr-signature"
}

Use mkCommit() and signCommit() from sdk/core/event.js to construct these.

Pull format (unauthenticated)

{
  "enclave": "64-hex-enclave-id",
  "type": "Pull",
  "after_seq": -1,
  "limit": 100
}

STH response

{
  "t": 1700000000000,
  "ts": 42,
  "r": "64-hex-root-hash",
  "sig": "128-hex-schnorr-signature"
}

Fields: t = timestamp, ts = tree size, r = root hash, sig = Schnorr signature over (t, ts, r).

Verify with: verifySTH(t, ts, hexToBytes(r), sig, nodePublicKeyBytes) from sdk/core/crypto.js.

RBAC manifest format

{
  "enc_v": 2,
  "nonce": 1234567890,
  "RBAC": {
    "use_temp": "none",
    "schema": [
      {"event": "post", "role": "owner", "ops": ["C", "U", "D"]},
      {"event": "post", "role": "Sender", "ops": ["U", "D"]},
      {"event": "*", "role": "Public", "ops": ["R"]},
      {"event": "Grant(admin)", "role": "owner", "ops": ["C"]},
      {"event": "Revoke(admin)", "role": "owner", "ops": ["C"]},
      {"event": "Pause", "role": "owner", "ops": ["C"]},
      {"event": "Resume", "role": "owner", "ops": ["C"]},
      {"event": "Terminate", "role": "owner", "ops": ["C"]}
    ],
    "states": ["MEMBER"],
    "traits": ["owner(0)", "admin(1)"],
    "initial_state": {"owner": ["64-hex-owner-pubkey"]}
  }
}
  • schema: RBAC rules. event is the commit type, role is the identity's role, ops is allowed operations (C=create, R=read, U=update, D=delete).
  • states: Named states (mutually exclusive). Identities are in exactly one state.
  • traits: Named traits with bit positions. owner(0) means the "owner" trait is at bit offset 0 from FIRST_TRAIT_BIT. Identities can have multiple traits.
  • initial_state: Map of trait names to lists of public keys that start with that trait.
  • Grant/Revoke: Use Grant(traitName) as the event type with {identity: "pubkey"} as content. Must be explicitly permitted in the schema.

Updating

To redeploy after code changes:

yarn build:js          # regenerate JS from Lean
yarn deploy            # push to Cloudflare

Durable Object state (enclaves, events, SMT, CT) persists across deployments. The SQLite storage in each Durable Object is not affected by redeployments.

Multiple nodes

To deploy multiple nodes (e.g., staging + production):

# Staging
npx wrangler deploy --name enc-node-staging
 
# Production
npx wrangler deploy --name enc-node
 
# Each needs its own NODE_PRIVATE_KEY
npx wrangler secret put NODE_PRIVATE_KEY --name enc-node-staging
npx wrangler secret put NODE_PRIVATE_KEY --name enc-node

Monitoring

View real-time logs:

npx wrangler tail

View in Cloudflare dashboard:

  • Workers & Pages → enc-node → Logs