Production Deployment Guide
Deploy an ENC node to Cloudflare Workers.
Prerequisites
- Cloudflare account
- Wrangler CLI authenticated
npx wrangler login - Node built (see local.md)
yarn build:js
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 keygenOutput:
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_KEYPaste 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 deployOutput:
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.dev5. 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
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /create-enclave | None | Create new enclave with manifest |
| POST | / | Signed commit | Submit commit (detected by exp field) |
| POST | / | None | Pull events (type: "Pull", after_seq, limit) |
| POST | / | ECDH | Query events (type: "Query", encrypted) |
| GET | /enclave/:id/sth | None | Signed tree head |
| GET | /enclave/:id/inclusion?seq=N | None | Inclusion proof for event N |
| GET | /enclave/:id/consistency?size1=A&size2=B | None | Consistency proof |
| GET | /enclave/:id/state?key=K | None | SMT 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.
eventis the commit type,roleis the identity's role,opsis 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 fromFIRST_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 CloudflareDurable 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-nodeMonitoring
View real-time logs:
npx wrangler tailView in Cloudflare dashboard:
- Workers & Pages → enc-node → Logs