# How Instatic Handles Media Storage and Image Variants: Architecture and Implementation

> Discover how Instatic manages media storage and image variants through its pluggable adapter system and automatic image-variant pipeline, generating responsive WebP ladders for your projects.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: architecture
- Published: 2026-07-03

---

**Instatic treats media as a first-class resource using a pluggable storage adapter system with a two-phase upload contract and an automatic image-variant pipeline that generates responsive WebP ladders.**

The CoreBunch/Instatic repository implements a fully modular media layer that separates storage concerns from presentation logic. By abstracting uploads through signed plans and delegating variant generation to either local workers or external CDNs, the system keeps plugins lightweight while ensuring bytes never cross the QuickJS sandbox boundary.

## Storage Adapter Architecture

Instatic organizes media access through **storage adapters**, each implementing a strict two-phase upload contract. Adapters register via the plugin SDK at `api.cms.media.registerStorageAdapter` and declare supported **roles** (`original`, `variant`, `avatar`, `font`, `plugin-pack`). The host elects one adapter per role, persisting the choice in the `active_media_storage_adapter` database row managed by [`src/server/repositories/media.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/server/repositories/media.ts).

### Two-Phase Upload Contract

Every adapter implements `beginWrite` → `finalizeWrite`:

1. **`beginWrite`**: Returns a `MediaStorageUploadPlan` containing signed URLs, HTTP method, and expiration.
2. **Streaming**: The host executes the plan in [`src/server/handlers/cms/mediaUploadExecutor.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/server/handlers/cms/mediaUploadExecutor.ts), streaming bytes via `fetch` or local filesystem operations without entering the plugin sandbox.
3. **`finalizeWrite`**: Confirms persistence and validates content hashes.

### Registry and Resolution

The singleton registry in [`src/core/plugins/mediaStorageRegistry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/mediaStorageRegistry.ts) maintains an in-memory map of all adapters. At boot, the system wires the built-in *local-disk* adapter via `mediaStorageRegistry.configureLocalDisk`, storing files under the `uploads/` directory. When resolving reads or writes, the registry queries the elected adapter ID from the database and returns the matching instance.

### Serving Modes

Adapters declare a `servingMode` that determines how browsers retrieve assets:

- **`public-url`**: Direct browser access (used by the local-disk adapter).
- **`signed-redirect`**: The host issues a short-lived signed URL and redirects via `/_instatic/media/<adapterId>/<storagePath>`.
- **`proxy`**: The host streams bytes through a FastCGI-style proxy.

These contracts are defined in [`src/core/plugin-sdk/types/media.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/media.ts).

## The Upload Pipeline

When a client initiates an upload, the request flows through a strictly typed dispatch chain:

```

POST /admin/api/cms/media/upload
   └─► server/handlers/cms/mediaUpload.ts (validation)
        └─► mediaUploadDispatch.ts (role-based routing)
             └─► mediaUploadExecutor.ts (byte streaming)
                  └─► executeUploadPlan()

```

The executor walks the `MediaStorageUploadPlan` steps, handling `file://` sentinels for local writes or performing `PUT/POST` requests for remote buckets. This design ensures that **bytes never cross the QuickJS sandbox**, keeping plugins secure and lightweight.

## Image Variant Generation and Ladder

After the original file persists, the **image-variant worker** in [`src/server/handlers/cms/imageVariantWorker.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/server/handlers/cms/imageVariantWorker.ts) processes the asset using `sharp`. It generates a ladder of WebP variants at preset widths ranging from 64 px up to the image’s intrinsic width.

### Variant Storage

Generated variants are stored in the `media_assets` table under the `variants_json` column (following the repository’s `*_json` naming convention for JSON columns). Each entry contains the variant’s `width`, `path`, and metadata.

### Rendering Helpers

The admin UI and publisher rely on utilities in [`src/modules/base/utils/mediaAttrs.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/utils/mediaAttrs.ts) to construct responsive markup:

```ts
// Build a srcset attribute from the variant ladder
export function buildMediaSrcset(media: RenderResolvedMedia): string | null;

// Select the smallest variant ≥ target width
export function pickMediaVariantUrl(
  media: RenderResolvedMedia | null,
  targetWidth: number | null,
): string | null;

```

These functions sort the `variants` array and return HTML-safe URLs via `safeUrl`, enabling browsers to select optimal resolutions.

## Variant Delegates for External CDNs

For sites using external image CDNs, Instatic supports **variant delegates** (Tier 3 plugins). A delegate replaces the local ladder generation with URL templates pointing to services like Cloudflare Images or Imgix.

Registration occurs via `api.cms.media.registerVariantDelegate`:

```ts
api.cms.media.registerVariantDelegate({
  id: 'myplugin.cdn',
  variantUrlTemplate:
    'https://cdn.example.com/img/{width}/{format}{path}',
  widths: [640, 1280, 1920],
  formats: ['webp', 'jpeg'],
});

```

When elected, the host skips local variant generation and stores only the delegate template. At render time, [`src/core/publisher/mediaPresentation.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/mediaPresentation.ts) builds URLs on-the-fly by interpolating the template. The delegate registry lives in [`src/core/plugins/mediaVariantDelegateRegistry.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/mediaVariantDelegateRegistry.ts).

## Database Schema and Persistence

Media assets are organized across dedicated tables:

- **`media_assets`**: Stores original file metadata, storage adapter IDs, and `variants_json`.
- **`media_folders`**: Hierarchical organization of media.
- **`media_asset_folders`**: Junction table linking assets to folders.

The schema supports strict separation between storage backend configuration and asset metadata, allowing adapter swaps without migrating file data.

## Security and Runtime Flow

The complete lifecycle enforces a strict security model:

1. **Validation**: [`server/handlers/cms/mediaUpload.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/mediaUpload.ts) validates MIME types and content hashes before dispatch.
2. **Signed Plans**: Adapters return time-limited signed URLs (default 5-minute expiration) via `beginWrite`.
3. **Sandbox Isolation**: Plugin code only manipulates upload plans; byte streaming executes in the host process via [`mediaUploadExecutor.ts`](https://github.com/CoreBunch/Instatic/blob/main/mediaUploadExecutor.ts).
4. **Proxy Safety**: When using `servingMode: 'proxy'`, the host validates all paths before streaming to prevent directory traversal.

## Code Examples

### Register a Custom Storage Adapter

```ts
// Plugin-side registration for S3-compatible storage
api.cms.media.registerStorageAdapter({
  id: 'myplugin.s3',
  label: 'Amazon S3',
  roles: ['original', 'variant'],
  servingMode: 'signed-redirect',
  beginWrite: async (input) => {
    const signedUrl = await getSignedPutUrl(input.suggestedStoragePath);
    return {
      storagePath: input.suggestedStoragePath,
      steps: [{ method: 'PUT', url: signedUrl, headers: {} }],
      expiresAt: Date.now() + 5 * 60 * 1000,
    };
  },
  finalizeWrite: async ({ storagePath, uploadReceipts }) => {
    // Verify etag and update database
  },
});

```

### Register a Variant Delegate

```ts
// Redirect variant generation to Cloudflare Images
api.cms.media.registerVariantDelegate({
  id: 'myplugin.cloudflare',
  variantUrlTemplate:
    'https://example.com/cdn-cgi/image/width={width},format={format},quality=80{path}',
  widths: [640, 1280, 1920, 3840],
  formats: ['webp', 'jpeg'],
});

```

### Consume Variants in a React Component

```tsx
import { buildMediaSrcset, pickMediaVariantUrl } from '@modules/base/utils/mediaAttrs';

function ResponsiveImage({ media }: { media: RenderResolvedMedia }) {
  const srcSet = buildMediaSrcset(media);
  const src = pickMediaVariantUrl(media, 800); // Target 800px width
  
  return (
    <img 
      src={src} 
      srcSet={srcSet} 
      alt={media.altText}
      loading="lazy"
    />
  );
}

```

### Execute Upload Plan on the Host

```ts
import { executeUploadPlan } from '@core/plugins/mediaUploadExecutor';

async function streamUpload(
  plan: MediaStorageUploadPlan, 
  fileBuffer: Uint8Array,
  adapter: MediaStorageAdapter
) {
  // Execute the signed plan steps
  const receipts = await executeUploadPlan(plan, fileBuffer);
  
  // Confirm completion with the adapter
  await adapter.finalizeWrite({
    storagePath: plan.storagePath,
    uploadReceipts: receipts,
  });
}

```

## Summary

- **Pluggable Architecture**: Storage adapters implement a `beginWrite`/`finalizeWrite` contract, allowing seamless integration with local disks or cloud buckets without modifying core logic.
- **Secure Isolation**: Byte streaming occurs in the host process ([`mediaUploadExecutor.ts`](https://github.com/CoreBunch/Instatic/blob/main/mediaUploadExecutor.ts)), while plugins only handle signed URLs, maintaining strict QuickJS sandbox boundaries.
- **Automatic Variants**: The image-variant worker generates WebP ladders stored in `variants_json`, with helpers in [`mediaAttrs.ts`](https://github.com/CoreBunch/Instatic/blob/main/mediaAttrs.ts) enabling responsive `srcset` generation.
- **CDN Delegation**: Tier 3 plugins can replace local processing via variant delegates, offloading transformation to external CDNs using URL templates.
- **Type Safety**: All contracts are defined in [`src/core/plugin-sdk/types/media.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugin-sdk/types/media.ts), ensuring consistent behavior across storage backends and rendering contexts.

## Frequently Asked Questions

### How does Instatic secure media uploads against unauthorized access?

Instatic validates MIME types and content hashes in [`server/handlers/cms/mediaUpload.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/mediaUpload.ts) before dispatching to adapters. Storage adapters return time-limited signed URLs (typically expiring after 5 minutes) through the `beginWrite` method, ensuring that upload endpoints cannot be replayed. Additionally, the `servingMode` configuration restricts direct file access, with `signed-redirect` and `proxy` modes enforcing host-side authorization before bytes reach the browser.

### Can I use Amazon S3 or Google Cloud Storage instead of local disk?

Yes. You can register a custom storage adapter via `api.cms.media.registerStorageAdapter` that implements the two-phase upload contract. The adapter specifies `roles: ['original', 'variant']` and a `servingMode` of either `signed-redirect` (for presigned S3 URLs) or `proxy` (for streaming through your server). The host stores the elected adapter ID in `active_media_storage_adapter` and routes all read/write operations through your implementation without code changes to the core.

### What image formats and sizes does Instatic generate by default?

By default, the image-variant worker in [`server/handlers/cms/imageVariantWorker.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/imageVariantWorker.ts) generates WebP variants at preset widths starting from 64 px up to the image’s intrinsic width. The exact width steps are configurable, but the system prioritizes WebP for compression efficiency. If you register a variant delegate, Instatic skips local generation entirely and relies on the delegate’s specified formats (commonly `webp` and `jpeg`) and widths array.

### How do I customize the responsive image sizes for my theme?

In the publisher or admin UI, use the `pickMediaVariantUrl` helper from [`src/modules/base/utils/mediaAttrs.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/utils/mediaAttrs.ts) to select variants by target width. For global customization, register a variant delegate with `api.cms.media.registerVariantDelegate` defining your own `widths` array (e.g., `[640, 1280, 1920]`). The delegate’s `variantUrlTemplate` can point to any image processing service, allowing you to define custom size ladders without modifying the database schema.