extension-object-storage
caffeinelabs/skills
Off-chain file and object storage with on-chain references for images, videos, documents, and bulk data.
What is extension-object-storage?
This skill adds file/object storage infrastructure to Caffeine AI applications, storing content off-chain while maintaining on-chain references via the Storage.ExternalBlob type. Use it for image galleries, video galleries, document management, and any application requiring large file uploads beyond IC limits with browser-cached HTTP access.
- Store files and objects off-chain with on-chain references using Storage.ExternalBlob types
- Support large files beyond IC size limits with HTTP URL access and browser caching
- Upload files from the browser with progress tracking and proper MIME type/filename headers
- Download files with original filenames or stream inline for images and videos
- Detect file types using filename or MIME type fields rather than URL inspection
How to install extension-object-storage
npx skills add https://github.com/caffeinelabs/skills --skill extension-object-storage- Add caffeineai-object-storage ~1.1.0 to mops.toml [dependencies]
- Import and include MixinObjectStorage() in main.mo from mo:caffeineai-object-storage/Mixin
- Install @caffeineai/object-storage npm package on the frontend
- Use Storage.ExternalBlob type for all file/image/document fields in backend data structures
How to use extension-object-storage
- 1.Add caffeineai-object-storage to mops.toml dependencies and run mops install
- 2.Import MixinObjectStorage and Storage in main.mo and include the mixin in your actor
- 3.Define data types using Storage.ExternalBlob for all file fields (never use Text for files)
- 4.On the frontend, import ExternalBlob and convert browser File objects using ExternalBlob.fromBytes(bytes, file.type, file.name)
- 5.Call your backend upload method with the ExternalBlob to store the file reference
- 6.Use blob.getDirectURL() to display files inline or blob.getBytes() for downloads with original filenames
Use cases
- Building image galleries with metadata stored on-chain and images served via cached HTTP URLs
- Creating video management systems where video files are stored off-chain but indexed on-chain
- Implementing document upload and download features for file management applications
- Managing user-generated content in social or collaborative applications with large media files
- Storing bulk data like PDFs, spreadsheets, or archives without hitting canister storage limits
- Motoko backend developers building Caffeine AI applications
- Full-stack developers implementing file upload/download features
- Teams building media-heavy applications (galleries, video platforms, document management)
- Developers needing to handle files larger than Internet Computer canister limits
extension-object-storage FAQ
Silent upload failures occur (403 Forbidden from the storage gateway). Both the backend mops package (caffeineai-object-storage) and frontend npm package (@caffeineai/object-storage) must be installed together.
No. Using Text breaks the upload/download proxy. Every data field representing a file, image, document, or media MUST use Storage.ExternalBlob as its type.
No. Never create your own implementation of platform-reserved storage methods. Always import MixinObjectStorage from the mops package (mo:caffeineai-object-storage/Mixin), never from relative paths.
Use the filename field from the backend record or a mimeType field if available. Never inspect the URL from getDirectURL() as these are opaque proxy URLs with no file extension.
Use getDirectURL() for streaming inline display (images, videos) with browser caching. Use getBytes() when you need the raw file data, such as for save-as downloads with the original filename.
Full instructions (SKILL.md)
Source of truth, from caffeinelabs/skills.
name: extension-object-storage description: General file/object storage, such as for images, videos, files, documents and other bulk data. Perfect fit for image galleries, video galleries, and other file or object management. Supports large files beyond IC limit, with browser-cached HTTP URL access. version: 1.1.0 compatibility: mops: caffeineai-object-storage: "~1.1.0" caffeineai-subscription: [none]
Object Storage
Object storage extension for Caffeine AI.
Overview
This skill adds off-chain file/object storage with on-chain references. The MixinObjectStorage mixin provides infrastructure for file operations; you track uploaded files in your own data structures using Storage.ExternalBlob.
Required Setup Checklist
All four steps are mandatory. Skipping any one causes 403 Forbidden: Invalid payload at upload time.
- mops dependency — add
caffeineai-object-storagetomops.tomlunder[dependencies]. - Mixin invocation —
include MixinObjectStorage()inmain.mo(imported from"mo:caffeineai-object-storage/Mixin"). - Storage.ExternalBlob types — every data field that represents a file MUST use
Storage.ExternalBlob, neverText. - Frontend npm package —
@caffeineai/object-storageinstalled andExternalBlob.fromBytes(bytes, file.type, file.name)used at the call site.
CRITICAL: The frontend package (@caffeineai/object-storage) does NOT work without the backend mops package (caffeineai-object-storage). Installing only the npm package and not the mops package causes silent upload failures (403 from the storage gateway). You MUST install both together.
Backend
File content is stored off-chain. The backend manages references to external files using the Storage.ExternalBlob type from mo:caffeineai-object-storage/Storage. The frontend handles the actual upload/download; the backend only stores the reference.
CRITICAL: ANY data field that represents a file, image, photo, document, or media MUST use Storage.ExternalBlob as its type -- NEVER Text. Using Text breaks the upload/download proxy. Method parameters that accept file uploads MUST also use Storage.ExternalBlob, not Text.
Correct:
blob : Storage.ExternalBlob
Wrong:
blobId : Text
imageUrl : Text
fileRef : Text
Module API
The only type you use from mo:caffeineai-object-storage/Storage is ExternalBlob (which is Blob). All other functions in Storage.mo are internal infrastructure used by MixinObjectStorage -- do not call them directly.
Setup in main.mo
include MixinObjectStorage() MUST be placed in main.mo, not in a custom mixin file. Your own file-tracking logic goes in a separate mixin.
import MixinObjectStorage "mo:caffeineai-object-storage/Mixin";
import Storage "mo:caffeineai-object-storage/Storage";
actor {
include MixinObjectStorage();
// Track file references
type Data = {
id: Text;
blob: Storage.ExternalBlob;
name: Text;
// other metadata
};
};
Wrong: Do NOT Implement Storage Methods Yourself
NEVER create your own implementation of _immutableObjectStorageCreateCertificate or any other _immutableObjectStorage* method. These are platform-reserved method names provided exclusively by the MixinObjectStorage mixin from the mops package. Hand-written implementations produce wrong return types and cause 403 Forbidden: Invalid payload at upload time.
Wrong — inline stub in main.mo:
// WRONG: Do not write this yourself
public shared func _immutableObjectStorageCreateCertificate(fileHash : Text) : async Blob {
CertifiedData.set(Blob.fromArray(hashBytes));
Blob.fromArray([])
};
Wrong — custom mixin file mimicking the platform shape:
// WRONG: Do not create src/backend/mixins/object-storage-api.mo
import ObjectStorageMixin "mixins/object-storage-api";
include ObjectStorageMixin();
The correct import path is ALWAYS "mo:caffeineai-object-storage/Mixin" — a mops package, never a relative path. Any relative import like "mixins/object-storage-api" or "./ObjectStorage" is wrong.
The correct signature produced by the platform mixin is:
_immutableObjectStorageCreateCertificate : (blobHash : Text) -> async record { method : Text; blob_hash : Text }
Any other return type (Blob, (), Text, etc.) will fail gateway validation.
Frontend
Backend Blob fields are represented as ExternalBlob on the frontend.
import { ExternalBlob } from "@caffeineai/object-storage";
import type { FileRecord } from "@caffeineai/object-storage";
ExternalBlob API
class ExternalBlob {
getBytes(): Promise<Uint8Array<ArrayBuffer>>;
getDirectURL(): string;
static fromURL(url: string): ExternalBlob;
static fromBytes(
blob: Uint8Array<ArrayBuffer>,
contentType?: string,
filename?: string,
): ExternalBlob;
withUploadProgress(onProgress: (percentage: number) => void): ExternalBlob;
}
Uploading Files
Pass the browser File type and name into fromBytes so the gateway blob tree stores Content-Type and Content-Disposition (original filename). Also pass file.name to the backend so app records keep the filename for lists and UI.
const handleUpload = async (file: File) => {
const bytes = new Uint8Array(await file.arrayBuffer());
const blob = ExternalBlob.fromBytes(bytes, file.type, file.name).withUploadProgress((pct) => {
setProgress(pct);
});
await actor.uploadFile(file.name, blob);
};
Gateway GET/HEAD responses echo the stored filename via Content-Disposition. Keep the backend filename field for queries and display without hitting the gateway.
Displaying Files
Use getDirectURL() for inline display (images, videos). This returns an opaque proxy URL -- it has no file extension, so never inspect the URL to determine file type.
<img src={record.blob.getDirectURL()} alt={record.filename} />
File Type Detection
CRITICAL: Never detect file types by inspecting the URL from getDirectURL(). These are opaque proxy URLs with no extension. Instead use the filename field from the backend record:
const isImage = (filename: string) =>
/\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)$/i.test(filename);
// Conditional rendering
{isImage(record.filename) ? (
<img src={record.blob.getDirectURL()} alt={record.filename} />
) : (
<div>{record.filename}</div>
)}
If the backend also returns a mimeType field, prefer that:
const isImage = (mimeType?: string) => mimeType?.startsWith("image/");
Downloading Files
For downloads with the original filename, use getBytes() to create a downloadable link:
const handleDownload = async (record: FileRecord) => {
const bytes = await record.blob.getBytes();
const blob = new Blob([bytes]);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = record.filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
Use getDirectURL() for inline display, getBytes() for save-as downloads.
Summary
| Use case | Method | Notes |
|---|---|---|
| Display image/video | blob.getDirectURL() | Streaming, cached |
| Download with filename | blob.getBytes() | Wrap in Blob + anchor |
| Upload from browser | ExternalBlob.fromBytes(bytes, file.type, file.name) | MIME + filename in gateway headers |
| Detect file type | filename or mimeType field | NEVER inspect the URL |
Verifying the Setup
Confirm the backend has the mops dependency installed. Check src/backend/mops.toml:
[dependencies]
caffeineai-object-storage = "0.1.2"
If caffeineai-object-storage is missing from [dependencies], object storage will not work regardless of what the frontend does. Add it, run mops install, and rebuild.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
403 Forbidden: Invalid payload on PUT /v1/blob-tree/ | Backend canister missing _immutableObjectStorageCreateCertificate or returning wrong type | Install caffeineai-object-storage in mops.toml, add include MixinObjectStorage() in main.mo, redeploy |
403 Forbidden: Invalid payload (all files) | @caffeineai/object-storage npm installed but caffeineai-object-storage mops NOT installed | Add the mops dependency and rebuild backend |
| Method exists but still 403 | Hand-written stub returns wrong type (e.g. Blob or () instead of record { method; blob_hash }) | Remove the custom implementation, use the platform mixin instead |
Forbidden: Owner does not have an account with the cashier | Cashier registration issue (unrelated to this skill) | Redeploy the backend canister to trigger self-healing registration |
Related skills
More from caffeinelabs/skills and the wider catalog.

extension-openai
Mandatory OpenAI integration for Caffeine builds using ChatGPT, GPT-4o, or LLM APIs.

extension-oql
Expose canister data as queryable tables for natural-language analytics via Caffeine Data Intelligence agent.

extension-posting-to-x
Mandatory OAuth 2.0 PKCE integration for posting tweets to X (Twitter) from Caffeine AI canisters.

extension-qr-code
QR code scanner using device camera with jsQR decoding.

extension-querying-oql
Query OQL canisters on ICP via icp CLI: schema discovery, JSON filtering, sorting, pagination, and aggregation.

extension-stripe
Stripe payment integration for credit and debit card transactions via HTTP outcalls.