Documentation

Protocol Labs Ecosystem Integration

The storage and authorization layers of the platform are built from specifications that came out of Protocol Labs and the ecosystem around it. These are content addressing, IPFS itself, IPNS, the DAG-CBOR codec and UCAN. This page lists what the platform runs, where each piece sits, and what you would observe if you went looking for it. Every claim on this page can be checked against the API, the cluster or the published crates.

SpecificationWhere it runsWhat it does here
IPFSa private cluster of Kubo peers, reached through its proxyStores every sealed payload and every public metadata document. Pins on every peer on write.
CID (multiformats)every file, document and tokenCIDv1, SHA-256, base32. The address of a stored object is the hash of its bytes.
IPNSthe keystore of the peer behind the proxyA stable name for a dataset that gets revised. The version history lives in Postgres.
DAG-CBOR (IPLD)pq-ucan and the sealed envelopeThe canonical encoding of every UCAN delegation and invocation, and of the encryption envelope around each payload. A token's CID is its handle.
UCAN 1.0pq-ucan, inside the APIDelegations a wallet signs and invocations a recipient signs, validated as a chain with the service as executor.
Multikey / multibasedid:bio documentsEd25519 verification keys as publicKeyMultibase strings with a multicodec prefix, and ML-KEM-1024 encapsulation keys as multibase strings in a service entry.
EKAYANA PLATFORMwhat we buildEkayana APIaxum, PostgresBrowser appsigns with the walletbio-did-registrySolana programCratesdid:bio, UCAN, key wrappingevery component, the whole stackIPFS stack in use, reached through the cluster proxy, pq-ucan and did-bio-coreIPFS clusterprivate, Kubo peersCIDv1sha-256, base32IPNSkeystore, publishDAG-CBORtokens, envelopesUCAN 1.0pq-ucanMultikeydid:bio documentsThe private cluster is the only running service. The rest are specifications the crates implement.
The Ekayana API, the browser application, the registry program and the did:bio and UCAN crates over the parts of the IPFS stack they use. The stack in use is the private IPFS cluster for storage and IPNS, CIDv1 addressing, DAG-CBOR encoding, UCAN 1.0 and Multikey. The cluster is the only external service. The rest are specifications the crates implement.

IPFS as a private cluster

IPFS runs as a private cluster. Several Kubo peers share a swarm key, so they connect only to each other and never to the public DHT, and IPFS Cluster peers keep one shared pinset and replicate every pin across them. The API speaks the Kubo RPC dialect to a single endpoint, IPFS_API_URL. In the cluster deployment that endpoint is the cluster's IPFS proxy, which pins and unpins on every peer and forwards everything else to the peer it fronts.

Ekayana APIKubo RPC dialectIPFS_API_URLPrivate IPFS cluster, swarm key, no public DHTCluster proxyadd, pin/rm cluster wideadd, pin/rmIPFS Cluster peersone CRDT pinset, x3pin on every peercat, key/gen, name/publish, name/resolveKubo peer 0fronted by the proxyKubo peer 1replicaKubo peer 2replicaswarm keyGateways :8080internal, envelopes onlyEvery add is pinned on all peers. IPNS keys live on the fronted peer. Nothing dials the public network.
The private IPFS cluster behind the API. The API speaks the Kubo RPC dialect to one endpoint, the IPFS Cluster proxy. Inside a closed network marked by a swarm key, the proxy sends adds and pin removals to the cluster peers, which keep one shared pinset and pin every CID on all three Kubo peers, and forwards reads, key generation and IPNS publishing to the peer it fronts. The gateways stay inside the network. No peer dials the public DHT.
RPC callUsed for
add with pin=true and cid-version=1Storing a payload or a metadata document. The proxy pins it on every peer as part of the write.
catReading a payload back from the fronted peer before it is opened for the caller
pin/rmReleasing a CID no record references any more, on every peer
key/gen (Ed25519)Creating the key behind an IPNS name, in the fronted peer's keystore
name/publishPointing an IPNS name at a CID
name/resolveResolving a name the database does not know

GET /api/ready reports whether the endpoint answers, next to the database check. Two kinds of object reach the cluster.

  • Every file uploaded through POST /api/upload is a sealed payload. It is encrypted under a fresh content key before add is called, and the key is wrapped to the service's ML-KEM-1024 key inside the same envelope. The CID names the envelope. See Privacy Controls.
  • Every DID has a public metadata document. When a DID is created or its metadata changes, the API writes a metadata.json holding the title, researchers, keywords, license, related identifiers, funding, the DID, its kind and its DOI, and pins it. The previous version is unpinned. The DID document points at the current one through a service entry of type BioMetadata whose endpoint is ipfs://<cid>.

An upload returns the CID of the envelope.

bash
curl -X POST https://api.ekayana.io/api/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@results.csv"
json
{ "cid": "bafkrei...", "name": "results.csv", "size": 20480,
  "content_type": "text/csv", "timestamp": "2026-07-24T10:00:00Z", "user_id": 42 }

The peers never join the public network, so a public IPFS gateway cannot fetch that CID at all. The platform's own gateway, if the operator exposes one, returns the envelope rather than the file. GET /api/download/{cid} is the only call that opens it, for the owner or for someone presenting a valid pass.

Content identifiers

Objects are addressed by CIDv1 with SHA-256, rendered in base32, so every identifier the API returns starts with baf. A single block, such as a metadata document, is bafkrei..., and a file large enough to be chunked is bafybei.... The address is the hash of the stored bytes. A fetched object can be checked against its own name, and a tampered block does not match.

Sealing changes one thing IPFS users expect. Two uploads of the same file produce two different CIDs, because each upload gets a fresh content key and a fresh nonce and the envelopes differ even when the plaintext does not. A CID therefore has exactly one owner, and GET /api/pins lists the CIDs of your uploads and no one else's. Metadata documents are added in the clear, so two identical documents would share a CID. In practice each carries its own DID and timestamps.

IPNS

A CID names exact bytes. For a dataset that gets revised, POST /api/ipns/create asks the peer behind the proxy for a new Ed25519 key and publishes the first record under it. Later revisions are published under the same name through POST /api/ipns/update. The peer publishes into the private swarm in the background with a 24 hour record lifetime and a one hour TTL. The Postgres row is the authoritative pointer. GET /api/ipns/{name}/resolve answers from the database and falls back to the peer's name/resolve only for names the database does not hold. Every version is retained. The name is published inside the private swarm, so it resolves through the API or the platform's gateway and not on the public IPNS network. See IPNS Publishing.

DAG-CBOR and IPLD

The platform uses one part of IPLD. DAG-CBOR is the encoding of its signed and sealed objects.

  • pq-ucan encodes every UCAN delegation and invocation as DAG-CBOR, using the cid and ipld-core crates. The signing_bytes that POST /api/share/file/prepare returns are the DAG-CBOR payload the wallet signs. The token_hash returned afterwards is the CID of the token, a bafyrei... identifier under the dag-cbor codec. Revocation names the token by that CID.
  • Each stored payload is a DAG-CBOR map with four keys. v is the envelope version, currently 1. iv is the AES-GCM nonce. ct is the ciphertext. key is the wrapped content key in the IPLD form that pq-space defines. Decoding the bytes from ipfs cat shows that structure and nothing readable.

The platform does not build a general IPLD graph of research objects. Papers, datasets and authors are linked through DID documents, DOIs and the knowledge graph rather than through IPLD links. Knowledge Graph Integration describes how those links are stored.

UCAN

UCAN is the authorization model. A share is a UCAN 1.0 delegation the owner's wallet signs. A read is an invocation the recipient's browser session key signs. pq-ucan validates both with the service's did:bio as executor. The envelope format, the DAG-CBOR encoding and the varsig headers are the ones the wider ecosystem uses, with ML-DSA principals added behind a feature flag. The format, the policy language and the validation rules are on UCAN Authorization. The flow a user sees is on Sharing & Delegation.

Multikey and multibase

did:bio documents encode Ed25519, X25519 and secp256k1 verification keys as Multikey methods. The publicKeyMultibase value is the multicodec prefix naming the key type, followed by the key, rendered in base58btc with the z prefix, so a wallet's key reads z6Mk.... did-bio-core produces and parses these, and that lets a resolver verify a signature from the identifier alone. ML-DSA-87 keys take the other form the DID data model allows, a JsonWebKey method with an AKP JWK, because no final multicodec mapping exists for them yet. The same multibase convention carries an ML-KEM-1024 encapsulation key, multicodec mlkem-1024-pub with code 0x120d, in a service entry typed SpaceEncapsulationKey. That entry is how pq-space publishes the key a content key can be wrapped to.

Checking it yourself

  • GET /api/ready shows the ipfs check next to database.
  • Resolve any anchored DID with GET /api/did/resolve/{did}, take the #metadata service endpoint, and fetch its CID through the platform's gateway. You get the public JSON document. A public IPFS gateway cannot reach it, because the peers never join the public network.
  • Fetch a file CID through the same gateway. You get DAG-CBOR bytes with the keys v, iv, ct and key, and nothing else.
  • Decode a token from GET /api/ucan/token/{cid} as DAG-CBOR and hash it. The CID is the token_hash you were given.

Resources