{"protocol":"clank-doc/1","frameworkVersion":"0.19.5","slug":"buckets","title":"Managed buckets","description":"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","group":{"id":"full-stack","title":"Full stack"},"url":"https://docs.clank.run/docs/buckets","source":"docs/buckets.md","headings":["Declare a bucket","Browser uploads","Ownership and access","Images and variants","Every bucket is available to agents","S3 compatible production storage","Inspect storage in a deployed app","Failure and security model"],"tableOfContents":[{"id":"declare-a-bucket","title":"Declare a bucket","level":2},{"id":"browser-uploads","title":"Browser uploads","level":2},{"id":"ownership-and-access","title":"Ownership and access","level":2},{"id":"images-and-variants","title":"Images and variants","level":2},{"id":"every-bucket-is-available-to-agents","title":"Every bucket is available to agents","level":2},{"id":"s3-compatible-production-storage","title":"S3-compatible production storage","level":2},{"id":"inspect-storage-in-a-deployed-app","title":"Inspect storage in a deployed app","level":2},{"id":"failure-and-security-model","title":"Failure and security model","level":2}],"markdown":"# Managed buckets\n\nManaged buckets are Clank's first-class application file and image layer. Declare what an app may\nstore once; the server, browser, deployment platform, and every app's MCP server use that same\ncontract. Local development needs no service account. A deployment receives an isolated catalog,\nobject namespace, signing key, and administrator-controlled project quota automatically.\n\n## Declare a bucket\n\nFor an AI-generated app, put buckets in `clank.app.ts` beside entities and actions:\n\n```ts\nimport type { AppBlueprintInput } from \"@clank.run/framework/blueprint\";\n\nexport default {\n  name: \"Field Notes\",\n  description: \"Shared field observations.\",\n  entities: {},\n  routes: [{ path: \"/\", view: \"notes\" }],\n  buckets: {\n    attachments: {\n      description: \"Files owned by one signed-in user.\",\n      ownership: \"user\",\n      visibility: \"private\",\n      browserAccess: \"authenticated\",\n      allowedContentTypes: [\"image/*\", \"application/pdf\", \"text/plain\"],\n      maxObjectBytes: 25 * 1024 * 1024,\n      maxObjects: 10_000,\n      maxBytes: 1024 * 1024 * 1024,\n      perOwnerMaxObjects: 500,\n      perOwnerMaxBytes: 100 * 1024 * 1024,\n      resumable: true,\n      maxChunkBytes: 4 * 1024 * 1024,\n    },\n  },\n} satisfies AppBlueprintInput;\n```\n\nRun `clank generate .`. The generated `src/buckets.ts` opens local object storage under\n`.clank/buckets` during development, passes the manager into `openBackend`, and accepts the\nproject-scoped managed environment in production. There is no bucket SDK to install.\n\nUse `defineBucket` directly when an app is not generated from a blueprint:\n\n```ts\nimport { defineBucket, openBucketManager } from \"@clank.run/framework/buckets\";\nimport { openLocalObjectStore } from \"@clank.run/framework/object-storage\";\n\nconst photos = defineBucket({\n  name: \"photos\",\n  ownership: \"user\",\n  visibility: \"private\",\n  browserAccess: \"authenticated\",\n  maxObjectBytes: 10 * 1024 * 1024,\n  maxObjects: 50_000,\n  maxBytes: 5 * 1024 * 1024 * 1024,\n  image: {\n    maxWidth: 8000,\n    maxHeight: 8000,\n    maxPixels: 40_000_000,\n    formats: [\"png\", \"jpeg\", \"webp\", \"avif\"],\n    variants: {\n      thumbnail: { width: 320, height: 320, fit: \"cover\", format: \"webp\", quality: 82 },\n    },\n  },\n});\n\nconst objects = await openLocalObjectStore({ directory: \".data/objects\" });\nconst buckets = await openBucketManager({\n  definitions: [photos],\n  store: objects,\n  databasePath: \".data/buckets.sqlite\",\n  stagingDirectory: \".data/uploads\",\n  signingKey: process.env.CLANK_BUCKET_SIGNING_KEY!,\n});\n\nconst backend = await openBackend(definition, { path: \"app.sqlite\", buckets });\n```\n\n`openBackend.close()` closes the bucket catalog it owns.\n\n## Browser uploads\n\nThe browser asks the authenticated backend for a short-lived, resource-bound upload capability.\nIt never receives object-store credentials. The capability contains the bucket, owner, operation,\nreservation, and expiry under HMAC; changing any byte invalidates it. The initiating management\nrequest uses the application's normal origin, session, and CSRF checks.\n\n```ts\nimport { createBucketClient } from \"@clank.run/framework/buckets\";\n\nconst attachments = createBucketClient(\"attachments\", {\n  csrfToken: () => document.querySelector('meta[name=\"clank-csrf\"]')?.content,\n});\n\nconst object = await attachments.upload({\n  key: `receipts/${crypto.randomUUID()}.pdf`,\n  value: file,\n  contentType: file.type,\n  resumable: true,\n  onProgress(uploaded, total) {\n    console.log(`${uploaded} / ${total}`);\n  },\n});\n```\n\nLarge uploads use sequential offset-checked `PATCH` chunks. `HEAD` reports the durable offset, so a\nclient can continue after a lost response. A wrong offset cannot overwrite an earlier chunk.\n`DELETE` cancels the reservation. Completion verifies declared length, optional SHA-256, allowed\nmedia type, image signature and dimensions, and the metadata returned by the object provider before\npublishing the new generation. The prior generation remains current until that commit succeeds.\n\n`list`, `stat`, `delete`, and `createReadIntent` use the same client. Private reads use an expiring\nread capability. Public objects receive an opaque ID plus digest URL that changes with each\ngeneration and the bucket's `cacheControl` policy. Responses set an exact type and length,\n`nosniff`, a digest ETag, safe content disposition,\nand a sandbox content security policy.\n\n## Ownership and access\n\nThese settings are independent:\n\n| Setting | Meaning |\n| --- | --- |\n| `ownership: \"user\"` | A key is resolved inside the authenticated user's partition. Two users may safely use the same key. |\n| `ownership: \"app\"` | One application-wide keyspace, useful for public assets and generated reports. |\n| `visibility: \"private\"` | Bytes require a server call or signed read capability. |\n| `visibility: \"public\"` | Opaque public URLs may be cached according to `cacheControl`. |\n| `browserAccess: \"authenticated\"` | Browser management requires the application session. |\n| `browserAccess: \"public\"` | Anonymous reads/listing are allowed only when ownership and visibility are both app-wide/public; writes still require authentication and CSRF. |\n| `browserAccess: \"server\"` | HTTP management is closed; server actions and MCP tools remain available. |\n\nNever treat a public URL as authorization. Use a private bucket for access-controlled material.\n\n## Images and variants\n\nImage buckets inspect file signatures rather than trusting an extension or `Content-Type`. PNG,\nJPEG, GIF, WebP, and AVIF dimensions are parsed before commit and checked against format, width,\nheight, and pixel limits. This blocks simple content-type spoofing and decompression-bomb dimensions\nbefore an image decoder receives the file.\n\nVariant names and geometry are part of the immutable bucket contract. Supply an\n`imageTransformer` to `openBucketManager` for the codec available in your runtime. The callback\nreceives only verified source bytes and the declared variant; its output passes the full upload\npolicy again. Clank intentionally does not hide a native image binary or billable transformation\nservice inside its zero-dependency package.\n\n## Every bucket is available to agents\n\nPassing the manager to `openBackend` adds current tools to that app's MCP contract:\n\n```text\nbucket_attachments_list\nbucket_attachments_read\nbucket_attachments_put\nbucket_attachments_delete\n```\n\nAn image bucket with variants also gets `bucket_<name>_transform`. Read tools require\n`agent:read`; writes and deletes require `agent:write`. OAuth resolves the same application user as\nthe UI, so a tool cannot list or mutate another user's partition. Small objects travel as bounded\nbase64. Larger reads return a short-lived resource-bound URL instead of overflowing the MCP\nresponse. Bucket definitions are included in `clank://actions`, `GET /__clank/manifest`, and the\npublic Clank discovery document, so an agent sees policy changes with the same contract revision as\nserver actions.\n\n## S3-compatible production storage\n\nGenerated apps select S3-compatible storage when `CLANK_BUCKET_S3_ENDPOINT` is present:\n\n```sh\nCLANK_BUCKET_S3_ENDPOINT=https://objects.example.com\nCLANK_BUCKET_S3_REGION=auto\nCLANK_BUCKET_S3_BUCKET=application-objects\nCLANK_BUCKET_S3_ACCESS_KEY_ID=...\nCLANK_BUCKET_S3_SECRET_ACCESS_KEY=...\nCLANK_BUCKET_PREFIX=project_01\n```\n\nOptional variables are `CLANK_BUCKET_S3_SESSION_TOKEN` and\n`CLANK_BUCKET_S3_PATH_STYLE=1`. The application protocol is unchanged: browser capabilities are\nserved by the app while verified generations are retained in S3. This works with AWS S3, Railway\nBuckets, Cloudflare R2, and compatible self-hosted services through the low-level `ObjectStore`\ncontract.\n\nOn Clank's deployment platform, each project receives:\n\n- an isolated local volume directory and catalog;\n- a stable project-derived signing key that is never returned through an API;\n- a unique logical object prefix for shared S3-compatible storage;\n- account/workspace administrator limits for total bucket bytes and object count; and\n- cleanup with the project's managed data boundary.\n\nLocal managed bytes are removed with that project boundary. When operators attach an external\nS3-compatible bucket, they must also configure provider lifecycle/deletion for the project's exact\n`CLANK_BUCKET_PREFIX`; Clank never scans or bulk-deletes an unbounded shared provider namespace by\nguessing keys after its catalog is gone.\n\nThe environment also supplies `CLANK_BUCKET_MAX_BYTES` and `CLANK_BUCKET_MAX_OBJECTS`. These are\ndeployment-wide ceilings across every declared bucket and cannot be raised by application code.\nDefinition limits and per-owner limits still apply, so the strictest relevant limit wins.\nServer observability can call `buckets.usage()` for aggregate active and reserved project totals;\nindividual runtimes return the corresponding bucket/owner usage from `bucket.usage(identity)` and\nevery list response includes its scoped usage.\n\n## Inspect storage in a deployed app\n\nThe project's **Storage** page in the Clank control plane shows the enforced byte/object ceilings.\nFor locally placed apps it also samples aggregate active and reserved usage from the bucket catalog\nthrough a read-only SQLite connection. Provider volumes remain outside the control-plane trust\nboundary, so the page does not mint a privileged storage credential or impersonate an app user.\n\nUse **Open file browser** or visit `https://your-app.example/__clank/buckets`. That inventory is\nserved by the application itself and requires its normal signed-in session. It lists at most 100\nobjects per page, supports bucket and key-prefix navigation, partitions user-owned buckets by the\ncurrent user, omits server-only buckets, and mints five-minute download capabilities for private\nobjects. The response is non-cacheable, cannot be framed, sends no referrer, contains no script,\nand uses a restrictive content security policy. It is intentionally read-only; application UI,\nserver actions, the browser client, or MCP tools perform uploads and deletion with their normal\nCSRF/scope checks.\n\n## Failure and security model\n\n- The SQLite catalog is authoritative for visibility, ownership, quota, and the active generation.\n- An object-store write is not visible until its size, SHA-256, type, and key match the reservation.\n- Reservations count against quota, preventing concurrent uploads from overcommitting capacity.\n- Replacements reserve only their byte delta and use compare-and-set SHA-256 when requested.\n- Expired reservations and staging files are swept on startup and before new reservations.\n- Provider deletions enter a durable garbage ledger before catalog visibility is removed; failures\n  retry across sweeps/restarts without resurrecting the object or losing its cleanup key.\n- Object bytes missing from or changed behind the catalog fail closed as integrity errors.\n- Signed capabilities expire within 24 hours, are operation-specific, and become unusable after a\n  write reservation commits or is cancelled.\n- User IDs are supplied by Clank auth or OAuth context, never from a browser query or MCP argument.\n- Public delivery addresses objects by opaque ID rather than exposing storage keys or provider URLs.\n- The local catalog is required to be a regular non-symlink file and is permissioned to its owner.\n\nBack up both the bucket catalog and object provider. The catalog alone cannot recreate bytes, and\norphaned provider bytes are deliberately not made visible by discovery.\n"}