Skip to main content

Storage and IPFS

Documents are too large and too personal for a blockchain. Certichain keeps them off chain on IPFS, in encrypted form, and ties them to the credential through the document hash. This page explains content addressing, pinning and gateways, and how Certichain uses a pinning service without having to trust it.

Content addressing​

Most web storage is location addressed: a URL says where a file is, and whoever runs that server can change what it returns. IPFS (the InterPlanetary File System) is content addressed: a file is named by a hash of its content, its CID (content identifier). Ask the network for a CID and any node that has the bytes can serve them; if the bytes were altered, they would have a different CID.

A CID has two common forms:

FormLooks likeNotes
CIDv0Qm followed by 44 base58 charactersThe original form, always SHA-256
CIDv1b followed by base32 charactersSelf-describing: it names its encoding and hash

Certichain accepts both, and nothing else, before a CID goes anywhere near a URL or a path:

const CID_PATTERN = /^(Qm[1-9A-HJ-NP-Za-km-z]{44}|b[a-z2-7]{58,120})$/;

(src/lib/cid.ts)

A CID is not the document hash

The CID is a hash of the stored bytes in IPFS's own chunked format, which here is the encrypted container. The documentHash in the credential is the SHA-256 of the plaintext file. The two are unrelated values: the CID locates the container, and the document hash proves the decrypted file is the one that was issued.

Pinning​

IPFS nodes keep content they fetch only as a cache and delete it when space runs low. To keep a file available, someone must pin it: tell a node to keep it permanently. A pinning service is a company that runs IPFS nodes and pins files for its customers.

Certichain uses Pinata as its pinning service. The server route /api/upload pins with Pinata's pinFileToIPFS endpoint (src/lib/server/pinata.ts), and lists and unpins through Pinata's file API (src/lib/server/pinata-list.ts, src/lib/server/pinata-unpin.ts). The credentials are server-only environment variables (PINATA_JWT, or PINATA_API_KEY and PINATA_API_SECRET_KEY for pinning).

Each container is pinned with a name and two labels, so the issuer can find its own copies later:

Pin metadataValue
Namecertichain- and the first 8 hex characters of the document hash
documentHash labelThe document hash
issuer labelThe issuer address from the signed-in session

Both labels are values that are already public on chain. Pinned files are public on IPFS: anyone with the CID can fetch the bytes. That is acceptable only because the bytes are encrypted (see Encryption).

In Certichain​

  • The browser checks that the server stored exactly the bytes it sent: the response's blobHash must equal the SHA-256 of the container it uploaded (DocumentService.store in src/services/domain/document-service.ts).
  • Under v0.3 the route accepts only encrypted containers; it refuses one whose header does not name the signed-in issuer or whose document hash differs from the hash the browser declared. Under the legacy v0.2 configuration it still accepts plaintext PDF, PNG and JPEG files (readEncryptionMode in src/lib/server/upload-container.ts).
  • The upload limit is 4 MB (MAX_UPLOAD_BYTES in src/lib/upload-limits.ts), under the 4.5 MB request body limit of the hosting platform.
  • The issuer can list its own copies of a document (GET /api/documents?documentHash=...) and remove all but one (DELETE /api/documents), both limited per wallet and only for the signed-in issuer's own pins. Removal is used after replacing a document key. See Upload and documents.

Where the CID lives​

Contract generationWhere the CID is kept
v0.3 (current)Not on chain. In the holder's credential package (cid), in grant files, in key update files and in capability links
v0.2 (legacy)On chain, in the ipfsHash field of the record, pointing at a plaintext file

Keeping the CID off chain means the chain does not point at the document at all; only someone given the package, a grant or a link can find it.

Gateways​

A gateway is an HTTP server that fetches IPFS content by CID, so a browser can read IPFS with a plain GET https://<gateway>/ipfs/<cid>. Gateways are a convenience, not a source of truth: they can be slow, can refuse a file, or could return the wrong bytes.

In Certichain​

  • In the browser, src/services/documents/gateway-fetch.ts tries the configured gateway first (NEXT_PUBLIC_PINATA_GATEWAY_URL), then https://ipfs.io and https://dweb.link, each with a 15 second timeout (GATEWAY_TIMEOUT_MS) and a bounded read.
  • On the server, the pin check reads back through the configured gateway (or https://ipfs.io when none is set) plus any listed in PIN_READ_GATEWAYS (readGateways in src/lib/server/pinning/providers.ts).
  • Whatever a gateway returns is untrusted until checked: the container must parse, name the credential's issuer, decrypt under the AEAD tag and produce a file whose SHA-256 equals the anchored document hash. A wrong or tampered file is refused; it can cause a failure to open but never a false "genuine" result.

The handbook records that a dedicated Pinata gateway refused files belonging to another Pinata account during the live test, so the deployed site uses Pinata's shared gateway.

A second pinning provider​

If the one pinning service lost a file or closed an account, holders could not open their documents (their package still proves the credential, but the document would be gone). Certichain therefore has machinery for a second, independent copy:

PartWhat it doesCode
PinProvider interfaceOne method, pin(bytes, metadata), returning the CID the service assignedsrc/lib/server/pinning/types.ts
Mirror after uploadPins the same bytes with the second provider after a successful upload, without delaying the responsesrc/lib/server/pinning/mirror.ts, after-pin.ts
LedgerRecords each CID, the SHA-256 of its bytes and which providers still lack a copy, in the state storesrc/lib/server/pinning/ledger.ts
RepairRetries missing copiessrc/lib/server/pinning/repair.ts
Monitor checkReads back the 10 newest pins through the gateways and compares their SHA-256 with the ledgersrc/lib/server/pinning/check.ts, wired in src/lib/server/ops/wiring.ts

The provider is chosen by name with SECOND_PIN_PROVIDER. No adapter is registered yet: the adapter table in src/lib/server/pinning/providers.ts is empty, and setting the variable to any name fails with "SECOND_PIN_PROVIDER names a service with no adapter." The handbook leaves the choice of service to the owner. Until one is added, the pin check does not run.

What storage cannot guarantee​

  • Erasure. Unpinning removes Pinata's copy, but other IPFS nodes or gateways that fetched the container may keep it for a while. This is one reason the container is encrypted: a lingering copy is ciphertext.
  • Availability without a pin. If every pin is removed and no node has a cached copy, the document cannot be fetched. The credential itself still verifies from the chain and the package.
  • Confidentiality of metadata. The container header names the issuer and document hash, and its size reveals the file size rounded up to 64 KiB.

Source files​

  • src/lib/cid.ts
  • src/app/api/upload/route.ts, src/app/api/documents/route.ts
  • src/lib/server/pinata.ts, pinata-list.ts, pinata-unpin.ts, upload-container.ts, config.ts
  • src/lib/server/pinning/types.ts, providers.ts, mirror.ts, after-pin.ts, ledger.ts, repair.ts, check.ts, gateway-reader.ts
  • src/services/documents/gateway-fetch.ts, src/services/domain/document-service.ts, src/services/upload.ts
  • src/lib/upload-limits.ts

Tests​

  • src/lib/cid.test.ts, src/lib/pinata.test.ts
  • src/lib/server/pinata.test.ts, pinata-list.test.ts, pinata-unpin.test.ts, upload-container.test.ts
  • src/lib/server/pinning/providers.test.ts, mirror.test.ts, after-pin.test.ts, ledger.test.ts, repair-and-check.test.ts, gateway-reader.test.ts
  • src/services/documents/gateway-fetch.test.ts
  • src/app/api/upload/route.test.ts, route.container.test.ts, src/app/api/documents/route.test.ts
  • E2E journey e2e/journeys/encrypted-documents.journey.ts