# OpenMAIC StorageProvider Interface: A Backend-Agnostic Contract for Asset Persistence

> Explore OpenMAIC's StorageProvider interface, a backend-agnostic contract for asset persistence. Learn how put, resolve, and remove methods enable stable references across diverse storage solutions like IndexedDB and cloud CDNs.

- Repository: [MAIC/OpenMAIC](https://github.com/THU-MAIC/OpenMAIC)
- Tags: api-reference
- Published: 2026-09-04

---

**The `StorageProvider` interface in OpenMAIC defines a minimal, three-method contract—`put`, `resolve`, and `remove`—that abstracts binary asset storage across any implementation, from IndexedDB to cloud CDNs, while maintaining stable references in DSL documents.**

The THU-MAIC/OpenMAIC repository decouples document structure from storage mechanics through the `StorageProvider` interface defined in `packages/@openmaic/dsl/src/storage.ts`. This abstraction enables the platform to persist images, audio, and video files through diverse backends without embedding storage-specific URLs directly into DSL documents. By pairing this interface with the enumeration capabilities in `packages/@openmaic/dsl/src/asset-manifest.ts`, OpenMAIC creates a persistence layer where asset references remain stable even as underlying storage implementations change.

## StorageProvider Interface Structure

The `StorageProvider` interface declares three asynchronous methods that form the complete lifecycle of binary asset management. Located at lines 86-93 of `packages/@openmaic/dsl/src/storage.ts`, this contract requires implementations to handle storage, retrieval, and deletion operations while returning opaque references rather than direct URLs.

### The put Method

**The `put` method** stores binary data and returns a unique asset reference.

```typescript
(data: BinaryBlob, meta?: AssetMeta) => Promise<AssetRef>

```

When invoked, `put` accepts a `BinaryBlob` containing the payload bytes and optional `AssetMeta` metadata, then persists the data to the underlying storage. Every successful call must return a freshly allocated `AssetRef` string, even if the identical binary content already exists in storage. This guarantees that each stored asset receives a unique identifier that can be embedded safely in DSL documents without collision risks.

### The resolve Method

**The `resolve` method** translates an opaque reference into a publicly accessible URL.

```typescript
(ref: AssetRef) => Promise<string | null>

```

This method takes an `AssetRef` and returns a promise resolving to a URL string suitable for use as a source in `<img>`, `<audio>`, or `<video>` elements. If the reference does not correspond to stored bytes, the method returns `null`. The implementation handles backend-specific URL generation, whether generating temporary presigned S3 URLs or resolving IndexedDB object URLs.

### The remove Method

**The `remove` method** deletes assets from storage.

```typescript
(ref: AssetRef) => Promise<void>

```

This operation deletes the binary data associated with the provided reference. When invoked with a non-existent reference, `remove` executes as a no-op rather than throwing errors. This behavior ensures safe cleanup operations in document editing workflows where references may have already been purged.

## Supporting Types

The interface relies on three core type definitions that ensure type safety across the persistence layer.

### AssetRef

**An `AssetRef`** is a simple opaque string that uniquely identifies stored bytes. Defined as a type alias in `packages/@openmaic/dsl/src/storage.ts`, this reference is intentionally opaque to callers and embeds directly into DSL documents as a stable identifier, surviving storage backend migrations that would invalidate direct URLs.

### BinaryBlob

**The `BinaryBlob` type** provides a minimal, DOM-independent representation of binary payloads. It specifies `size`, MIME type, and an `arrayBuffer()` method, making it compatible with browser-native `Blob` objects and Node.js buffers while keeping the DSL package free of DOM dependencies.

### AssetMeta

**Optional `AssetMeta`** defines structured-cloneable metadata stored alongside the asset, such as `contentType`. This metadata travels with the binary data through `put` operations, allowing storage implementations to preserve content-type headers or encryption parameters without polluting the asset reference itself.

## Integration with Asset Manifest

The `StorageProvider` interface operates in concert with `packages/@openmaic/dsl/src/asset-manifest.ts` to enable enumerable, stable asset management. While [`asset-manifest.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/asset-manifest.ts) defines the manifest structure and enumeration logic, it relies on `StorageProvider` implementations to materialize assets into accessible URLs.

This integration enables three critical capabilities:

1. **Stable References**: DSL documents store `AssetRef` strings rather than fragile URLs, ensuring documents remain valid when assets migrate between storage backends.
2. **Lazy Resolution**: Exporters for formats like ZIP, PPTX, or video files enumerate references via the manifest but only call `resolve()` when generating the final output, minimizing unnecessary URL generation.
3. **Atomic Replacement**: Documents update atomically by swapping `AssetRef` values—calling `put()` for new data, updating the document reference, then `remove()` for the old data—without breaking existing links during the transition.

## Implementation Examples

OpenMAIC provides concrete implementations in the storage package, including browser-based IndexedDB stores and HTTP backends. You can also implement custom providers by satisfying the three-method contract.

### In-Memory Storage Provider

The following implementation demonstrates a simple memory-backed provider that satisfies the `StorageProvider` interface:

```typescript
import type { StorageProvider, BinaryBlob, AssetMeta, AssetRef } from '@openmaic/dsl';

class MemoryStorage implements StorageProvider {
  private store = new Map<AssetRef, { blob: BinaryBlob; meta?: AssetMeta }>();
  private counter = 0;

  async put(data: BinaryBlob, meta?: AssetMeta): Promise<AssetRef> {
    const ref: AssetRef = `mem-${this.counter++}`;
    this.store.set(ref, { blob: data, meta });
    return ref;
  }

  async resolve(ref: AssetRef): Promise<string | null> {
    return this.store.has(ref) ? `memory://${ref}` : null;
  }

  async remove(ref: AssetRef): Promise<void> {
    this.store.delete(ref);
  }
}

```

### Resolving Assets via the Manifest

When processing documents, use the enumeration function from [`asset-manifest.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/asset-manifest.ts) to collect all references, then resolve them through your provider:

```typescript
import { enumerateAssetManifest } from '@openmaic/dsl/asset-manifest';
import type { AssetManifestDocument } from '@openmaic/dsl/asset-manifest';
import { StorageProvider } from '@openmaic/dsl';

// Assume `doc` conforms to AssetManifestDocument
const manifest = enumerateAssetManifest(doc);

// Resolve each asset with a concrete provider (e.g., BrowserAssetStore)
async function resolveAll(provider: StorageProvider) {
  for (const entry of manifest.entries) {
    const url = await provider.resolve(entry.ref);
    console.log(`Asset ${entry.ref} (${entry.kind}) → ${url ?? 'missing'}`);
  }
}

```

### Swapping Existing Assets

For document editing workflows that replace assets while maintaining document integrity:

```typescript
async function replaceAsset(
  provider: StorageProvider,
  oldRef: AssetRef,
  newBlob: BinaryBlob,
  meta?: AssetMeta
) {
  const newRef = await provider.put(newBlob, meta);
  // Document update logic would replace `oldRef` with `newRef`
  await provider.remove(oldRef);
}

```

## Summary

The `StorageProvider` interface in OpenMAIC establishes a clean separation between DSL document structure and binary persistence:

- **Three-method contract**: `put()`, `resolve()`, and `remove()` handle complete asset lifecycles without exposing backend details.
- **Opaque references**: `AssetRef` strings provide stable identifiers that survive storage migrations, unlike direct URLs.
- **Backend flexibility**: Implementations in `packages/@openmaic/storage/src/asset/browser-store.ts` and [`http.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/http.ts) demonstrate support for IndexedDB and HTTP backends using identical interfaces.
- **Manifest integration**: `packages/@openmaic/dsl/src/asset-manifest.ts` leverages the interface to enumerate and resolve assets lazily during export operations.

## Frequently Asked Questions

### What is the relationship between StorageProvider and asset-manifest.ts?

While the `StorageProvider` interface is defined in `packages/@openmaic/dsl/src/storage.ts`, it serves as the persistence contract for the asset enumeration system in `packages/@openmaic/dsl/src/asset-manifest.ts`. The manifest tracks which `AssetRef` values exist within a document, while the provider implementation resolves those references to retrievable URLs and handles actual binary storage.

### How does AssetRef differ from a storage URL?

An `AssetRef` is an opaque string identifier embedded in DSL documents, while the URL returned by `resolve()` represents a concrete, often temporary, access mechanism. This distinction allows documents to remain valid when storage backends change—only the `resolve()` implementation updates to generate new URLs, while the `AssetRef` stored in the document stays constant.

### Can StorageProvider implementations cache resolved URLs?

Yes, implementations may cache URL generation results, though they must respect the contract that `resolve()` returns `null` for non-existent references. Concrete implementations like `BrowserAssetStore` in `packages/@openmaic/storage/src/asset/browser-store.ts` typically generate object URLs or presigned links that may expire, requiring fresh resolution calls for long-lived documents.

### What happens if remove() is called on an active reference?

The `remove()` method executes as a no-op when given a non-existent reference, preventing runtime errors during cleanup operations. However, once an asset is removed, subsequent calls to `resolve()` with that `AssetRef` will return `null`, and any document containing that reference will display missing asset behavior until the reference is updated to point to new data via `put()`.