Skip to main content
skill CX Codex

extension-object-storage — Claude Code skill: file and object storage

This skill adds file storage to your project: content lives off-chain while the backend keeps only references. Useful for image galleries, video and documents.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add caffeinelabs/skills --skill extension-object-storage -a claude-code
Open on GitHub

What extension-object-storage does

The skill wires up off-chain object storage with on-chain references. The MixinObjectStorage mixin provides the file infrastructure, and you track uploaded files in your own data structures using Storage.ExternalBlob. Large files go past the IC limit, and access happens over an HTTP URL that the browser caches.

Setup has four steps, and every one is mandatory. Add caffeineai-object-storage to mops.toml, call include MixinObjectStorage() in main.mo, give every file field the Storage.ExternalBlob type instead of Text, and install the @caffeineai/object-storage npm package on the frontend with ExternalBlob.fromBytes(bytes, file.type, file.name) at the call site. Skip any step and uploads fail with 403 Forbidden: Invalid payload.

One thing to watch on the frontend: the npm package does not work without the backend mops package. Installing only the npm side leads to silent upload failures, because the storage gateway answers 403. The mixin import path is always "mo:caffeineai-object-storage/Mixin" — relative paths like "mixins/object-storage-api" are wrong. Do not write your own _immutableObjectStorage* methods either: they carry a platform signature, and a hand-written version breaks gateway validation.

What it helps with

  • keep images and video off-chain while the backend stores only references
  • build a file gallery with upload progress
  • serve large files past the IC limit
  • render images and video inline through a direct URL
  • download a file under its original name via getBytes()

Example prompts for the agent

  • Add object storage to the project: wire caffeineai-object-storage into mops.toml and call include MixinObjectStorage() in main.mo.
  • Handle the frontend upload with ExternalBlob.fromBytes(bytes, file.type, file.name) and pass the filename to the backend.
  • Render images from records with getDirectURL() and detect the file type from the filename field, not from the URL.

How to install extension-object-storage

1Skills CLI (Vercel)

npx skills add caffeinelabs/skills --skill extension-object-storage -a claude-code

2Manually: put the files in place

~/.claude/skills/extension-object-storage/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/extension-object-storage/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about extension-object-storage

How do I install extension-object-storage in Claude?

Run npx skills add caffeinelabs/skills --skill extension-object-storage -a claude-code. The skill lands in ~/.claude/skills/extension-object-storage/SKILL.md.

What does extension-object-storage do?

It adds file and object storage: content sits off-chain while the backend keeps references through Storage.ExternalBlob. It fits image galleries, video and documents.

What do I need so uploads stop failing with 403?

All four setup steps: the caffeineai-object-storage dependency in mops.toml, include MixinObjectStorage() in main.mo, Storage.ExternalBlob on file fields, and the @caffeineai/object-storage npm package on the frontend. Skipping any of them gives 403 Forbidden: Invalid payload.

Source text

SKILL.md · Apache-2.0
---
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.1
compatibility:
  mops:
    caffeineai-object-storage: "~1.1.0"
caffeineai-subscription: [none]
---

# Object Storage
Object storage extension for [Caffeine AI](https://caffeine.ai?utm_source=caffeine-skill&utm_medium=referral).

## 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.

1. **mops dependency** — add `caffeineai-object-storage` to `mops.toml` under `[dependencies]`.
2. **Mixin invocation** — `include MixinObjectStorage()` in `main.mo` (imported from `"mo:caffeineai-object-storage/Mixin"`).
3. **Storage.ExternalBlob types** — every data field that represents a file MUST use `Storage.ExternalBlob`, never `Text`.
4. **Frontend npm package** — `@caffeineai/object-storage` installed and `ExternalBlob.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.

```motoko filepath=src/backend/main.mo
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:
<!-- motoko-check:skip -->
```motoko filepath=wrong.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:
```motoko filepath=wrong-mixin.mo
// 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.


```typescript
import { ExternalBlob } from "@caffeineai/object-storage";
import type { FileRecord } from "@caffeineai/object-storage";
```

## ExternalBlob API

```typescript
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.

```typescript
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.

```typescript
<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:

```typescript
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:

```typescript
const isImage = (mimeType?: string) => mimeType?.startsWith("image/");
```

## Downloading Files

For downloads with the original filename, use `getBytes()` to create a downloadable link:

```typescript
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`:

```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 |

© caffeinelabs · Apache-2.0 · caffeinelabs/skills

Similar skills

Everything for Backend and APIs →

From the same set25

extension-email-raw This skill adds sending one email to several recipients through to, cc and bcc. Handy when a meeting reminder has to reach confirmed and tentative attendees at once. CX Codex +1 28,100installs extension-invite-links Adds invite links and RSVP collection to your project. Guests submit answers using unique codes without logging in, while admins view all responses after sign-in. CX Codex +1 28,100installs extension-http-outcalls A skill for HTTP requests from a backend canister to external APIs. It keeps every request bounded on the provider side. CX Codex +1 28,100installs extension-email Enables agents to send transactional and service emails from Motoko backend canisters. Useful for notifications and order receipts. CX Codex +1 28,100installs extension-email-verification Adds email address verification to a Caffeine AI app: the recipient clicks a link and the address is marked as verified. CX Codex +1 28,100installs extension-oql Exposes Motoko canister data to Caffeine Data Intelligence agents via schema() and execute() endpoints. Useful for natural language queries across Maps and record arrays. CX Codex +1 27,700installs connector-googlemail Guides agents on sending emails through a user's own Gmail account inside Caffeine canisters. Enforces mops packages instead of raw HTTP requests. CX Codex +1 25,000installs extension-querying-oql Quick reference for querying an OQL backend canister via the icp CLI. Guides the agent through fetching schemas, structuring JSON queries, and reading Candid rows. CX Codex +1 24,700installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor