Aleph Storage Schema
This document defines the wire format used by Aleph Cloud App to store project and deployment history on the Aleph network. It is sufficient to write a third-party recovery tool in any language.
Project schema version: 2 Deployment schema version: 4 (storeRef refactor — see "Artifact storeRef" below) Channel: ALEPH-CLOUDAPPAleph network: mainnet
Readers tolerating older records: the deployment replay path accepts pre-v4 records that carry cid directly in public (no storeRef); it emits a LEGACY_SCHEMA warning per such record but reconstructs the deployment from the inline cid. New writes always emit v4.
Message types
| Data | Aleph type | Signer | Frequency |
|---|---|---|---|
Projects (per wallet, key projects) | AGGREGATE | User wallet | Rare — on create/edit/delete |
Deployments (per wallet, content type aleph-cloud-deployment) | POST + amends | Backend delegate | Frequent — 2 per deployment |
User security (per wallet, key security) | AGGREGATE | User wallet | Once per wallet, at first project creation |
Projects AGGREGATE
{
"type": "AGGREGATE",
"channel": "ALEPH-CLOUDAPP",
"content": {
"address": "0xUser...",
"key": "projects",
"content": {
"proj_<nanoid12>": {
"id": "proj_<nanoid12>",
"schemaVersion": 1,
"public": {
"framework": "nextjs-static",
"deployTarget": "ipfs",
"createdAt": "<ISO 8601>",
"updatedAt": "<ISO 8601>",
"deleted": false
},
"encrypted": "<EncryptedEnvelope>"
}
},
"time": "<unix seconds>"
},
"signature": "<personal_sign of keccak256(canonical JSON)>",
"sender": "0xUser..."
}The encrypted blob decrypts to:
{
"name": "<string>",
"source": "github" | "upload",
"repo": "<owner/name>" | null,
"branch": "<string>" | null,
"installationId": "<number>" | null,
"buildCommand": "<string>",
"outputDir": "<string>"
}Tombstone: to delete a project, write the entry with public.deleted: true and an empty encrypted blob (all fields set to empty string).
Deployments POST + amends
Each deployment produces two POST messages: a creation and a terminal amend.
Creation POST:
{
"type": "POST",
"channel": "ALEPH-CLOUDAPP",
"content": {
"address": "0xUser...",
"type": "aleph-cloud-deployment",
"content": {
"deploymentId": "deploy_<nanoid12>",
"projectId": "proj_<nanoid12>",
"schemaVersion": 4,
"public": {
"status": "queued",
"storeRef": null,
"url": null,
"runId": "<number>" | null,
"runAttempt": "<number>" | null,
"createdAt": "<ISO 8601>",
"finishedAt": null
},
"encrypted": "<EncryptedEnvelope>"
},
"time": "<unix seconds>"
},
"signature": "<delegate signature>",
"sender": "0xBackendDelegate..."
}Terminal amend (status: "live" or "failed"): Same shape but content.type: "amend" and content.ref: <creation hash>. On a successful pin, the terminal amend's public.storeRef is set to the artifact STORE's item_hash. On failure, storeRef stays null.
Artifact storeRef
The artifact CID is not in the deployment POST. It lives in a separate STORE message, and the deployment POST references it via public.storeRef = <STORE item_hash>. To recover the CID:
- Read
public.storeReffrom the terminal amend POST. - Fetch the STORE:
GET <api>/api/v0/messages/<storeRef>(or via the SDK). - The CID is
response.content.item_hash(the IPFS protocol-level field).
This indirection exists so a single FORGET on the STORE removes the CID from Aleph's index without touching the deployment POSTs (which survive intact as audit trail).
Tombstoned STOREs (post-FORGET): the GET in step 2 returns 404. Recovery tools should set cid: null and surface a STORE_FORGOTTEN warning. The deployment metadata is preserved; the artifact bytes are unrecoverable through Aleph's index. (Bytes leaked to third-party IPFS gateways may persist indefinitely — encryption is the only durable defense for byte content. See the Data & Privacy section of the application docs.)
Backend-internal cache field: the backend's in-memory cache record additionally carries forgottenAt: ISO timestamp | null, set when the delegate FORGETs the STORE during a project delete. This field is NOT on the wire — recovery tools derive equivalent information by attempting the STORE fetch in step 2.
Project deletion + FORGET
Project deletion is a two-step protocol:
- The user signs an AGGREGATE tombstone:
public.deleted: trueon the project entry. The deployment POSTs (and their referenced STORE messages) remain on Aleph. - After the tombstone broadcasts, the backend's delegate broadcasts ONE
FORGETmessage per delete batching every deployment'sstoreRef:
{
"type": "FORGET",
"channel": "ALEPH-CLOUDAPP",
"content": {
"address": "0xUser...",
"hashes": ["<storeRef_1>", "<storeRef_2>", ...],
"time": "<unix seconds>"
},
"signature": "<delegate signature>",
"sender": "0xBackendDelegate..."
}FORGET failures are logged but do not roll back the AGGREGATE tombstone — the project deletion is honored even if STORE cleanup fails. Subsequent delete attempts on the same project re-issue FORGETs for any deployments still missing the cache's forgottenAt flag (idempotent).
The FORGET removes the bytes from every Aleph CCN within ~24-48h. Deployment POSTs remain readable post-FORGET — the storeRef hash itself is non-revealing without the dereferenced STORE.
The encrypted blob decrypts to:
{
"commit": "<sha>" | null,
"commitMessage": "<string>" | null,
"branch": "<string>" | null,
"actionsRunUrl": "<string>" | null,
"error": "<string>" | null
}Encryption envelope
interface EncryptedEnvelope {
v: 1;
alg: "aes-256-gcm";
ct: string; // base64 ciphertext
iv: string; // base64 (12 bytes)
tag: string; // base64 (16 bytes)
deks: {
user: string; // base64, ECIES-wrapped DEK to user's wallet pubkey
backend: string; // base64, ECIES-wrapped DEK to backend's pubkey
};
}Decryption algorithm
- Take
envelope.deks.user, base64-decode →wrappedDek - ECIES-decrypt
wrappedDekwith the user's secp256k1 private key →dek(32 bytes) - base64-decode
envelope.ct,envelope.iv,envelope.tag - AES-256-GCM decrypt with key=
dek, iv=iv, ciphertext=ct, authTag=tag - Result is UTF-8 JSON; parse to get the cleartext fields
Amend chain resolution
For each deployment, build a chain of POSTs:
- Find the creation POST (
type === "aleph-cloud-deployment") - Find amend POSTs whose
reffield equals the creation'sitem_hash - Follow forward: each amend's hash may be referenced by another amend; the leaf is the latest
- Use the leaf's content as the authoritative deployment state
When two amends share the same ref, pick the one with the largest time (last-write-wins).
Cryptographic primitives
- AES-256-GCM — Node
crypto.createCipheriv("aes-256-gcm", key, iv)semantics; 12-byte IV, 16-byte tag - ECIES — secp256k1 ECDH + HKDF-SHA256 + AES-256-GCM, as implemented by
eciesjs(https://github.com/ecies/js); same scheme is used byecies/pyandecies/rsfor cross-language compatibility - secp256k1 — same curve as Ethereum signing
- Address —
keccak256(uncompressed_pubkey[1:])[12:](standard Ethereum derivation)
Canonical signing digest
User-signed AGGREGATE messages use this canonical JSON form before hashing:
JSON.stringify({
type: msg.type,
channel: msg.channel,
content: {
address: msg.content.address,
...(key !== undefined ? { key } : {}),
...(type !== undefined ? { type } : {}),
...(ref !== undefined ? { ref } : {}),
content: msg.content.content,
time: msg.content.time,
},
})The 32-byte keccak256 of this string is then signed via Ethereum personal_sign (EIP-191), which wraps it in the \x19Ethereum Signed Message:\n prefix. Signature recovery on the reader side must apply the same wrapping.