Managed buckets

Managed buckets are Clank's first class application file and image layer. Declare what an app may store once; the server, browser, deployment platform, and every app's MCP server use that same contract. Local development needs no service ac

6 min read1,285 wordsClank 0.19.5

Managed buckets are Clank's first-class application file and image layer. Declare what an app may store once; the server, browser, deployment platform, and every app's MCP server use that same contract. Local development needs no service account. A deployment receives an isolated catalog, object namespace, signing key, and administrator-controlled project quota automatically.

Declare a bucket

For an AI-generated app, put buckets in clank.app.ts beside entities and actions:

ts
import type { AppBlueprintInput } from "@clank.run/framework/blueprint";

export default {
  name: "Field Notes",
  description: "Shared field observations.",
  entities: {},
  routes: [{ path: "/", view: "notes" }],
  buckets: {
    attachments: {
      description: "Files owned by one signed-in user.",
      ownership: "user",
      visibility: "private",
      browserAccess: "authenticated",
      allowedContentTypes: ["image/*", "application/pdf", "text/plain"],
      maxObjectBytes: 25 * 1024 * 1024,
      maxObjects: 10_000,
      maxBytes: 1024 * 1024 * 1024,
      perOwnerMaxObjects: 500,
      perOwnerMaxBytes: 100 * 1024 * 1024,
      resumable: true,
      maxChunkBytes: 4 * 1024 * 1024,
    },
  },
} satisfies AppBlueprintInput;

Run clank generate .. The generated src/buckets.ts opens local object storage under .clank/buckets during development, passes the manager into openBackend, and accepts the project-scoped managed environment in production. There is no bucket SDK to install.

Use defineBucket directly when an app is not generated from a blueprint:

ts
import { defineBucket, openBucketManager } from "@clank.run/framework/buckets";
import { openLocalObjectStore } from "@clank.run/framework/object-storage";

const photos = defineBucket({
  name: "photos",
  ownership: "user",
  visibility: "private",
  browserAccess: "authenticated",
  maxObjectBytes: 10 * 1024 * 1024,
  maxObjects: 50_000,
  maxBytes: 5 * 1024 * 1024 * 1024,
  image: {
    maxWidth: 8000,
    maxHeight: 8000,
    maxPixels: 40_000_000,
    formats: ["png", "jpeg", "webp", "avif"],
    variants: {
      thumbnail: { width: 320, height: 320, fit: "cover", format: "webp", quality: 82 },
    },
  },
});

const objects = await openLocalObjectStore({ directory: ".data/objects" });
const buckets = await openBucketManager({
  definitions: [photos],
  store: objects,
  databasePath: ".data/buckets.sqlite",
  stagingDirectory: ".data/uploads",
  signingKey: process.env.CLANK_BUCKET_SIGNING_KEY!,
});

const backend = await openBackend(definition, { path: "app.sqlite", buckets });

openBackend.close() closes the bucket catalog it owns.

Browser uploads

The browser asks the authenticated backend for a short-lived, resource-bound upload capability. It never receives object-store credentials. The capability contains the bucket, owner, operation, reservation, and expiry under HMAC; changing any byte invalidates it. The initiating management request uses the application's normal origin, session, and CSRF checks.

ts
import { createBucketClient } from "@clank.run/framework/buckets";

const attachments = createBucketClient("attachments", {
  csrfToken: () => document.querySelector('meta[name="clank-csrf"]')?.content,
});

const object = await attachments.upload({
  key: `receipts/${crypto.randomUUID()}.pdf`,
  value: file,
  contentType: file.type,
  resumable: true,
  onProgress(uploaded, total) {
    console.log(`${uploaded} / ${total}`);
  },
});

Large uploads use sequential offset-checked PATCH chunks. HEAD reports the durable offset, so a client can continue after a lost response. A wrong offset cannot overwrite an earlier chunk. DELETE cancels the reservation. Completion verifies declared length, optional SHA-256, allowed media type, image signature and dimensions, and the metadata returned by the object provider before publishing the new generation. The prior generation remains current until that commit succeeds.

list, stat, delete, and createReadIntent use the same client. Private reads use an expiring read capability. Public objects receive an opaque ID plus digest URL that changes with each generation and the bucket's cacheControl policy. Responses set an exact type and length, nosniff, a digest ETag, safe content disposition, and a sandbox content security policy.

Ownership and access

These settings are independent:

SettingMeaning
ownership: "user"A key is resolved inside the authenticated user's partition. Two users may safely use the same key.
ownership: "app"One application-wide keyspace, useful for public assets and generated reports.
visibility: "private"Bytes require a server call or signed read capability.
visibility: "public"Opaque public URLs may be cached according to cacheControl.
browserAccess: "authenticated"Browser management requires the application session.
browserAccess: "public"Anonymous reads/listing are allowed only when ownership and visibility are both app-wide/public; writes still require authentication and CSRF.
browserAccess: "server"HTTP management is closed; server actions and MCP tools remain available.

Never treat a public URL as authorization. Use a private bucket for access-controlled material.

Images and variants

Image buckets inspect file signatures rather than trusting an extension or Content-Type. PNG, JPEG, GIF, WebP, and AVIF dimensions are parsed before commit and checked against format, width, height, and pixel limits. This blocks simple content-type spoofing and decompression-bomb dimensions before an image decoder receives the file.

Variant names and geometry are part of the immutable bucket contract. Supply an imageTransformer to openBucketManager for the codec available in your runtime. The callback receives only verified source bytes and the declared variant; its output passes the full upload policy again. Clank intentionally does not hide a native image binary or billable transformation service inside its zero-dependency package.

Every bucket is available to agents

Passing the manager to openBackend adds current tools to that app's MCP contract:

text
bucket_attachments_list
bucket_attachments_read
bucket_attachments_put
bucket_attachments_delete

An image bucket with variants also gets bucket_<name>_transform. Read tools require agent:read; writes and deletes require agent:write. OAuth resolves the same application user as the UI, so a tool cannot list or mutate another user's partition. Small objects travel as bounded base64. Larger reads return a short-lived resource-bound URL instead of overflowing the MCP response. Bucket definitions are included in clank://actions, GET /__clank/manifest, and the public Clank discovery document, so an agent sees policy changes with the same contract revision as server actions.

S3-compatible production storage

Generated apps select S3-compatible storage when CLANK_BUCKET_S3_ENDPOINT is present:

sh
CLANK_BUCKET_S3_ENDPOINT=https://objects.example.com
CLANK_BUCKET_S3_REGION=auto
CLANK_BUCKET_S3_BUCKET=application-objects
CLANK_BUCKET_S3_ACCESS_KEY_ID=...
CLANK_BUCKET_S3_SECRET_ACCESS_KEY=...
CLANK_BUCKET_PREFIX=project_01

Optional variables are CLANK_BUCKET_S3_SESSION_TOKEN and CLANK_BUCKET_S3_PATH_STYLE=1. The application protocol is unchanged: browser capabilities are served by the app while verified generations are retained in S3. This works with AWS S3, Railway Buckets, Cloudflare R2, and compatible self-hosted services through the low-level ObjectStore contract.

On Clank's deployment platform, each project receives:

  • an isolated local volume directory and catalog;
  • a stable project-derived signing key that is never returned through an API;
  • a unique logical object prefix for shared S3-compatible storage;
  • account/workspace administrator limits for total bucket bytes and object count; and
  • cleanup with the project's managed data boundary.

Local managed bytes are removed with that project boundary. When operators attach an external S3-compatible bucket, they must also configure provider lifecycle/deletion for the project's exact CLANK_BUCKET_PREFIX; Clank never scans or bulk-deletes an unbounded shared provider namespace by guessing keys after its catalog is gone.

The environment also supplies CLANK_BUCKET_MAX_BYTES and CLANK_BUCKET_MAX_OBJECTS. These are deployment-wide ceilings across every declared bucket and cannot be raised by application code. Definition limits and per-owner limits still apply, so the strictest relevant limit wins. Server observability can call buckets.usage() for aggregate active and reserved project totals; individual runtimes return the corresponding bucket/owner usage from bucket.usage(identity) and every list response includes its scoped usage.

Inspect storage in a deployed app

The project's Storage page in the Clank control plane shows the enforced byte/object ceilings. For locally placed apps it also samples aggregate active and reserved usage from the bucket catalog through a read-only SQLite connection. Provider volumes remain outside the control-plane trust boundary, so the page does not mint a privileged storage credential or impersonate an app user.

Use Open file browser or visit https://your-app.example/__clank/buckets. That inventory is served by the application itself and requires its normal signed-in session. It lists at most 100 objects per page, supports bucket and key-prefix navigation, partitions user-owned buckets by the current user, omits server-only buckets, and mints five-minute download capabilities for private objects. The response is non-cacheable, cannot be framed, sends no referrer, contains no script, and uses a restrictive content security policy. It is intentionally read-only; application UI, server actions, the browser client, or MCP tools perform uploads and deletion with their normal CSRF/scope checks.

Failure and security model

  • The SQLite catalog is authoritative for visibility, ownership, quota, and the active generation.
  • An object-store write is not visible until its size, SHA-256, type, and key match the reservation.
  • Reservations count against quota, preventing concurrent uploads from overcommitting capacity.
  • Replacements reserve only their byte delta and use compare-and-set SHA-256 when requested.
  • Expired reservations and staging files are swept on startup and before new reservations.
  • Provider deletions enter a durable garbage ledger before catalog visibility is removed; failures retry across sweeps/restarts without resurrecting the object or losing its cleanup key.
  • Object bytes missing from or changed behind the catalog fail closed as integrity errors.
  • Signed capabilities expire within 24 hours, are operation-specific, and become unusable after a write reservation commits or is cancelled.
  • User IDs are supplied by Clank auth or OAuth context, never from a browser query or MCP argument.
  • Public delivery addresses objects by opaque ID rather than exposing storage keys or provider URLs.
  • The local catalog is required to be a regular non-symlink file and is permissioned to its owner.

Back up both the bucket catalog and object provider. The catalog alone cannot recreate bytes, and orphaned provider bytes are deliberately not made visible by discovery.