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:
| Form | Looks like | Notes |
|---|---|---|
| CIDv0 | Qm followed by 44 base58 characters | The original form, always SHA-256 |
| CIDv1 | b followed by base32 characters | Self-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)
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 metadata | Value |
|---|---|
| Name | certichain- and the first 8 hex characters of the document hash |
documentHash label | The document hash |
issuer label | The 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
blobHashmust equal the SHA-256 of the container it uploaded (DocumentService.storeinsrc/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 (
readEncryptionModeinsrc/lib/server/upload-container.ts). - The upload limit is 4 MB (
MAX_UPLOAD_BYTESinsrc/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 generation | Where 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.tstries the configured gateway first (NEXT_PUBLIC_PINATA_GATEWAY_URL), thenhttps://ipfs.ioandhttps://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.iowhen none is set) plus any listed inPIN_READ_GATEWAYS(readGatewaysinsrc/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:
| Part | What it does | Code |
|---|---|---|
PinProvider interface | One method, pin(bytes, metadata), returning the CID the service assigned | src/lib/server/pinning/types.ts |
| Mirror after upload | Pins the same bytes with the second provider after a successful upload, without delaying the response | src/lib/server/pinning/mirror.ts, after-pin.ts |
| Ledger | Records each CID, the SHA-256 of its bytes and which providers still lack a copy, in the state store | src/lib/server/pinning/ledger.ts |
| Repair | Retries missing copies | src/lib/server/pinning/repair.ts |
| Monitor check | Reads back the 10 newest pins through the gateways and compares their SHA-256 with the ledger | src/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.tssrc/app/api/upload/route.ts,src/app/api/documents/route.tssrc/lib/server/pinata.ts,pinata-list.ts,pinata-unpin.ts,upload-container.ts,config.tssrc/lib/server/pinning/types.ts,providers.ts,mirror.ts,after-pin.ts,ledger.ts,repair.ts,check.ts,gateway-reader.tssrc/services/documents/gateway-fetch.ts,src/services/domain/document-service.ts,src/services/upload.tssrc/lib/upload-limits.ts
Tests
src/lib/cid.test.ts,src/lib/pinata.test.tssrc/lib/server/pinata.test.ts,pinata-list.test.ts,pinata-unpin.test.ts,upload-container.test.tssrc/lib/server/pinning/providers.test.ts,mirror.test.ts,after-pin.test.ts,ledger.test.ts,repair-and-check.test.ts,gateway-reader.test.tssrc/services/documents/gateway-fetch.test.tssrc/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