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 as application/octet-stream with 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