Agent index (llms.txt) — fetch this first, then the page it names.
Storage
Orama Storage is file storage built on IPFS. Upload, retrieve, pin and unpin files across the cluster. Every file is identified by its CID (content identifier), a hash of its content: the same bytes always have the same CID, which gives built-in integrity checking and de-duplication.
Do not confuse this with on-chain storage deals, where independent providers hold encrypted pieces and prove it every epoch. Gateway storage is part of your namespace and is not metered by the chain.
How it works
Files go into a private IPFS swarm on the nodes (it has a swarm key of its own, so it never joins the public IPFS network), and IPFS Cluster pins them across the cluster. Uploads are pinned automatically. A pin keeps the file; an unpinned file is eventually removed by garbage collection. Garbage collection runs on a timer (6 hours), and each node's IPFS storage budget is set from half its disk.
Private blobs are encrypted before they are added. A node serves content it holds straight from disk, so a download does not need the cluster to be reachable.
SDK
import { createClient } from "@debros/orama";
const client = createClient({
baseURL: "https://ns-myapp.cluster.example.com",
jwt: accessToken, // the signed-in user's token: a key on its own is refused here
});
const { cid, name, size } = await client.storage.upload(file, file.name); // File, Blob, ArrayBuffer, Uint8Array or ReadableStream
const status = await client.storage.status(cid);
const stream = await client.storage.get(cid); // ReadableStream
const response = await client.storage.getBinary(cid); // Response with headers
await client.storage.pin(existingCid, "report.pdf");
await client.storage.unpin(cid);upload works in the browser and in Node, has a 5 minute timeout for large files, and pins by default. A read that closely follows a write can legitimately 404 while the pin propagates, so reads retry a 404 for about eighteen seconds, but only while the gateway marks it retryable (a CID uploaded or pinned in the last two minutes). A 404 for content that is gone is returned at once.
Read errors
| Status | code | Meaning |
|---|---|---|
| 404 | NOT_FOUND | The cluster does not hold the content: never pinned, or unpinned and reclaimed. Answered at once. retryable: true only in the two minutes after the upload or pin; after that it is final |
| 403 | FORBIDDEN | The namespace does not own the CID, or the credential's grant does not cover it |
| 504 | TIMEOUT (retryable) | The content could not be retrieved in time (the local daemon did not answer, or fetching from other nodes took over 20 seconds) |
| 503 | SERVICE_UNAVAILABLE (retryable) | The storage cluster could not be reached |
| 400 | VALIDATION_FAILED | Not a valid CID in canonical form |
| 500 | INTERNAL | The storage node failed to read the content |
Permissions
Storage needs the storage grant, and a logged-in user's wallet session (or a deployed app's own workload token). A key on its own is refused, so an app-runtime key extracted from a bundle cannot act as anybody. A grant can be narrowed to a name prefix: storage:avatars/* limits upload, get, pin and unpin to objects uploaded under that prefix. GET /v1/storage/status/<cid> answers only for a CID the caller's namespace references; for any other it returns the same 404 as for a CID nobody pinned, so it cannot be used to test what other tenants store. See Access control.
Unpinning and deleting
curl -X DELETE https://ns-myapp.cluster.example.com/v1/storage/unpin/<cid> -H "Authorization: Bearer <token>"
The cluster keeps one pin per CID however many namespaces uploaded the same bytes. Whether an unpin removes it is decided from a cluster-wide reference count (in the registry), never from your own database: pins and deployments register a reference, an unpin releases yours, and only the release that leaves none removes the pin. The reclaim of the bytes then waits for garbage collection, or add ?immediate=true for a privacy-grade delete that evicts the blocks cluster-wide at once when yours was the last reference. The response's evicted field says what happened ("true", "partial", "shared", "skipped"); only "true" is a delete-for-everyone guarantee. See Go client for the table. Deleting a row or a file is not erasure: a deleted database row stays in the replication log until compaction.
Quotas
A namespace can have a storage quota (max_storage_bytes), but it is opt-in: with no quota row nothing is enforced, and no namespace has one by default. When set, uploads and pins that would exceed it are refused, and a backup restore that would exceed it is refused too.
Relayed fetch
You can hand someone a token that lets them download one object without an identity, through a relay, so the node holding it never learns who asked.
const { caps } = await client.storage.mintFetchCaps(cid, { count: 3, ttlSeconds: 86400 }); // device-bound session
await client.storage.revokeFetchCap(caps[0].id, caps[0].revokeKey);Minting needs a device-bound session, the storage read grant and ownership of the CID. See Relayed fetch.
REST API
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/storage/upload | Upload (multipart/form-data with file and optional pin) |
GET | /v1/storage/get/{cid} | Download a file. Always sends Content-Length |
POST | /v1/storage/pin | Pin an existing CID. Body {cid, name?} |
GET | /v1/storage/status/{cid} | Pin status |
DELETE | /v1/storage/unpin/{cid} | Unpin (?immediate=true for eviction) |
POST | /v1/storage/fetch-caps | Mint fetch capabilities. Body {cid, count, ttl_seconds} |
DELETE | /v1/storage/fetch-caps/{id} | Revoke one, with X-Orama-Revoke-Key |
GET | /v1/storage/relayed/{cid} | Download with X-Orama-Fetch-Cap and no credential |
Full route list: Gateway HTTP API.
Types
interface StorageUploadResponse { cid: string; name: string; size: number }
interface StoragePinResponse { cid: string; name: string }
interface StorageStatus {
cid: string; name: string;
status: string; // "pinned" | "pinning" | "queued" | "unpinned" | "error" | "unknown"
replication_min: number; replication_max: number; replication_factor: number;
peers: string[]; error?: string;
}CLI
There are no orama commands for gateway file storage; use the SDK or REST. orama db backup stores a database backup into IPFS (Databases), and orama namespace backup makes a sealed backup (Backups). orama storage … is for on-chain deals. orama storage repair --deal-id <id> --repair-seed-file <file> restores the replicas a deal lost: it fetches an accepted replica from another provider, checks it against its on-chain root, re-seals it for the new provider and uploads it, never recovering the plaintext. A deal that names a repair delegate is repaired by the delegate while you are away; without one it runs with fewer replicas until you run the command (Storage deals).