Skip to content

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

DataAleph typeSignerFrequency
Projects (per wallet, key projects)AGGREGATEUser walletRare — on create/edit/delete
Deployments (per wallet, content type aleph-cloud-deployment)POST + amendsBackend delegateFrequent — 2 per deployment
User security (per wallet, key security)AGGREGATEUser walletOnce per wallet, at first project creation

Projects AGGREGATE

json
{
  "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:

json
{
  "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:

json
{
  "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:

  1. Read public.storeRef from the terminal amend POST.
  2. Fetch the STORE: GET <api>/api/v0/messages/<storeRef> (or via the SDK).
  3. 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:

  1. The user signs an AGGREGATE tombstone: public.deleted: true on the project entry. The deployment POSTs (and their referenced STORE messages) remain on Aleph.
  2. After the tombstone broadcasts, the backend's delegate broadcasts ONE FORGET message per delete batching every deployment's storeRef:
json
{
  "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:

json
{
  "commit": "<sha>" | null,
  "commitMessage": "<string>" | null,
  "branch": "<string>" | null,
  "actionsRunUrl": "<string>" | null,
  "error": "<string>" | null
}

Encryption envelope

typescript
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

  1. Take envelope.deks.user, base64-decode → wrappedDek
  2. ECIES-decrypt wrappedDek with the user's secp256k1 private key → dek (32 bytes)
  3. base64-decode envelope.ct, envelope.iv, envelope.tag
  4. AES-256-GCM decrypt with key=dek, iv=iv, ciphertext=ct, authTag=tag
  5. Result is UTF-8 JSON; parse to get the cleartext fields

Amend chain resolution

For each deployment, build a chain of POSTs:

  1. Find the creation POST (type === "aleph-cloud-deployment")
  2. Find amend POSTs whose ref field equals the creation's item_hash
  3. Follow forward: each amend's hash may be referenced by another amend; the leaf is the latest
  4. 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 by ecies/py and ecies/rs for cross-language compatibility
  • secp256k1 — same curve as Ethereum signing
  • Addresskeccak256(uncompressed_pubkey[1:])[12:] (standard Ethereum derivation)

Canonical signing digest

User-signed AGGREGATE messages use this canonical JSON form before hashing:

javascript
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.