Concepts
Blob storage
Photos, audio, documents, and other binary files go in Ellivate's blob store, not the KV. Bytes live on a CDN; KV holds only the URL. 25MB per file.
Two storage primitives
Ellivate ships two storage layers — pick the one that matches your data:
- Key-value store (
ellivate.set/get/list/del). JSON-serialisable values, capped at 256KB per key. Right for app state, items, settings, lists of references. - Blob store (
ellivate.blob.put/get/list/del). Raw bytes up to 25MB, served from a CDN. Right for photos, audio, documents, attachments, anything binary.
The blob API
JavaScript / TypeScript
import { ellivate } from "../ellivate-client";
// Upload a File from <input type="file">
const result = await ellivate.blob.put(file, { mimeType: file.type });
// result.url is a proxy URL — embed in <img>, store in KV, etc.
// Save the metadata to KV (URL only, NOT the bytes)
await ellivate.set("photos/" + slug, {
url: result.url,
caption,
uploadedAt: new Date().toISOString(),
});
// Reading later — the URL works directly:
const meta = await ellivate.get("photos/" + slug);
// <img src={meta.url} />
// Listing all blobs in the current scope:
const blobs = await ellivate.blob.list();
// Deleting:
await ellivate.blob.del(meta.url);Python
import ellivate
# Upload
result = ellivate.blob.put(file_bytes, mime_type="image/jpeg")
# result["url"] is a proxy URL — store this in KV
ellivate.set("photos/" + slug, {"url": result["url"], "caption": caption})
# Read bytes back
data = ellivate.blob.get(result["url"])
# List + delete
blobs = ellivate.blob.list()
ellivate.blob.delete(result["url"])What put() returns
{
"url": "https://api.ellivate.ai/data/<dsId>/blob/<blobId>",
"blobId": "k3xj2p9...",
"mimeType": "image/jpeg",
"sizeBytes": 248302,
"scope": "PERSONAL",
"createdAt": "2026-04-27T13:24:11.123Z"
}The url is the canonical reference. Embed it in <img src>, <video src>, <audio src> — Ellivate's API serves the bytes with the right Content-Type and the same access-control rules as KV.
Scope
Blobs honor the same per-app scope rules as KV — see persistence & scopes for the full picture. The shorthand:
- PERSONAL — visible only to the viewer who uploaded. Pass
{ scope: "PERSONAL" }or rely on the app's default scope. - SHARED — visible to everyone in the share-group. Use for collaborative photo albums, household receipts, etc.
- PUBLIC — open to anyone who can open the app. Use for header images, app icons, public galleries.
If you don't pass a scope, the app's defaultScope is used.
Limits
- 25MB per file. Single PUT request. Larger files need to be split client-side or compressed before upload.
- Bytes only. Send the raw
File/Blob/Uint8Array/bytes— do not base64-encode first. The SDK sends asapplication/octet-streamwith the original mime type passed via header. - No streaming yet. v1 is a single-shot upload. Streaming and multipart are on the roadmap for video and large datasets.
How the URL works
The URL returned by put() is a proxy URL hosted by Ellivate's API, not the underlying CDN URL. Every fetch goes through Ellivate's access-control layer first — same auth + scope checks as a KV read. PERSONAL blobs require the viewer's token; SHARED blobs require group membership; PUBLIC blobs are anonymous-OK.
The proxy adds about ~30ms of latency over a direct CDN hit, and we trade that for a real privacy guarantee — the underlying CDN URL never crosses to the client, so apps can't accidentally leak it through logs or debug output.
What auto-rewriting catches
Ellivate's publish pipeline has a blob translator that looks for the base64-stuffed-into-KV pattern in your code and rewrites it to call ellivate.blob.put first. It covers patterns like:
FileReader.readAsDataURL(file)→ellivate.set(key, dataUrl)canvas.toDataURL()stored in KV"data:image/..."strings in KV writes
When the rewriter can't safely transform a file (ambiguous control flow, unusual patterns), it adds a warning to the publish manifest and leaves the code as-is. You'll see the warning in the publish status; the fix is usually a one-line swap to ellivate.blob.put.
What's next
- Persistence & scopes — how the KV side works and how scopes are picked.
- JS SDK reference — full method signatures.
- Python SDK reference — full method signatures.