extension-object-storage
caffeinelabs/skills
Off-chain file/object storage with on-chain references for images, videos, and bulk data.
What is extension-object-storage?
This skill adds general-purpose file and object storage for Caffeine AI canisters, storing content off-chain while maintaining on-chain references. Use it when you need to handle images, videos, documents, or other large files that exceed Internet Computer limits, with browser-cached HTTP URL access for retrieval.
- Store files off-chain with on-chain blob references using Storage.ExternalBlob types
- Support large files beyond IC canister limits with streaming and caching
- Provide opaque proxy URLs for secure, direct file access from the browser
- Track uploaded files in your own data structures with metadata
- Enable upload progress tracking and file downloads with original filenames
- Detect file types using filename or mimeType 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.0.0 to mops.toml [dependencies]
- Install @caffeineai/object-storage npm package on the frontend
- Import MixinObjectStorage from mo:caffeineai-object-storage/Mixin in main.mo
How to use extension-object-storage
- 1.Add caffeineai-object-storage to mops.toml and run mops install
- 2.Include MixinObjectStorage() in main.mo (not in a custom mixin file)
- 3.Define all file-tracking data fields using Storage.ExternalBlob type, never Text
- 4.Install @caffeineai/object-storage npm package in your frontend
- 5.Use ExternalBlob.fromBytes(bytes) to convert browser File objects before upload
- 6.Call your backend method with the filename and ExternalBlob to upload
- 7.Use blob.getDirectURL() to display images/videos inline
- 8.Use blob.getBytes() to enable downloads with original filenames
Use cases
- Building image galleries with metadata tracking and direct URL access
- Creating video management systems with large file support and streaming
- Implementing document repositories with download-as functionality
- Managing user-uploaded media in social or collaboration apps
- Storing bulk data assets with browser-cached HTTP access
- Motoko backend developers building file-heavy applications
- Full-stack developers integrating file uploads with Caffeine AI canisters
- Teams building media galleries, asset managers, or document systems
- Developers needing to handle files larger than IC canister storage limits
extension-object-storage FAQ
Using Text breaks the upload/download proxy. Any data field representing a file, image, document, or media MUST use Storage.ExternalBlob as its type, never Text. This applies to both data structures and method parameters.
This typically means the backend is missing the caffeineai-object-storage mops package, the MixinObjectStorage() include is missing from main.mo, or you're using Text instead of Storage.ExternalBlob. Verify all four setup steps are complete: mops dependency, mixin invocation, ExternalBlob types, and frontend npm package.
No. Never create your own implementation of _immutableObjectStorage* methods. These are platform-reserved and must come exclusively from the MixinObjectStorage mixin via the mops package. Hand-written implementations produce wrong return types and cause upload failures.
Never inspect the URL from getDirectURL() — these are opaque proxy URLs with no extension. Instead, use the filename field from the backend record with regex matching, or prefer a mimeType field if the backend provides it.
Use getDirectURL() for inline display of images and videos (streaming and cached). Use getBytes() for downloads, where you wrap the bytes in a Blob and create a downloadable link 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.0.0 compatibility: mops: caffeineai-object-storage: "~1.0.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()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>): ExternalBlob;
withUploadProgress(onProgress: (percentage: number) => void): ExternalBlob;
}
Uploading Files
Convert the browser File object to ExternalBlob and pass the original filename alongside:
const handleUpload = async (file: File) => {
const bytes = new Uint8Array(await file.arrayBuffer());
const blob = ExternalBlob.fromBytes(bytes).withUploadProgress((pct) => {
setProgress(pct);
});
await actor.uploadFile(file.name, blob);
};
Always send file.name so the backend stores the original filename.
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) | Pair with .withUploadProgress() |
| 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 embeddings via secure canister-side API keys.

extension-oql
Make canister data queryable by natural language via OQL schema and execute endpoints.

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-stripe
Stripe payment integration for credit and debit card transactions via HTTP outcalls.

extension-user-approval
Approval-based user management with admin controls for Caffeine AI applications.