Data storage
Every app comes with storage your agent can read and write: lists, settings, totals, even files.
What your app can store
Values. Lists, settings, totals, notes: the everyday things an app needs to remember. Whatever your app saves survives between visits and across code updates.
Files. Your app can keep images, PDFs, audio, video, and other files, and show them right in its interface. Charming apps render inside AI chats like Claude and ChatGPT, so your files show up there too. Each app holds up to 50 files, 10MB each, 100MB combined. Files stick around the same way your app’s other data does. Contact us if you need more.
Technical
The app you build is its own UI for its own storage: it reads and writes key-value data (each JSON-serializable value keyed by a string you choose) by calling its own backend operations (window.buildy.api(op) from the frontend, env.storage from the handler behind it), and files the same way through window.buildy.assets or env.assets. An agent can also add a file directly with the upload_asset MCP tool, no app code required.
Copy this prompt for your agent
Add persistent storage to my Charming app. Read
https://charm.ing/docs/capabilities/data-storage.md first. Work out
from the app what it should save, values or files; if it isn't
obvious, ask me.
If you haven't built a Charming app before, read
https://charm.ing/docs/build-mcp.md (or build-http.md for the HTTP API)
for the app skeleton first.
How an agent performs this job
Your agent declares buildy:storage/kv@1.0 in the app’s manifest.capabilities.imports when it creates or updates the app (create_app / update_app over MCP, or POST /app / PUT /app/<id> over HTTP). Inside a route handler, it reads env.storage from the handler’s context and calls .get, .put, .delete, or .list. To read or change stored data without touching the app’s code, an agent (including a different one from whichever built it) calls query_app for a read-only operation or mutate_app for one that writes.
Files work the same way, plus a shortcut that skips the app’s code entirely. Two paths add or use a file:
- Without touching the app’s code. An agent calls the
upload_assetMCP tool with the app id, a key, and exactly one oftext(UTF-8),sourceUrl(the server fetches it), ordataBase64(small binaries only). This works whether or not the app declared the blob import, because it writes directly rather than going throughenv.assets. - From inside the app. The app declares
buildy:storage/blob@1.0inmanifest.capabilities.importswhen it’s created or updated. A route handler then readsenv.assetsfrom its context and calls.get,.put,.delete,.list, or.url. The frontend can call the matchingwindow.buildy.assetsmethods (.getUrl,.load,.upload,.list,.delete) directly, without a handler in between.
The contract
env.storage is only defined if the app declared the buildy:storage/kv@1.0 import. Its shape:
env.storage.get(key); // Promise<unknown | undefined>
env.storage.put(key, value); // Promise<void> - value must be JSON-serializable
env.storage.delete(key); // Promise<boolean>
env.storage.list(); // Promise<string[]>
Storage is per-app and per-key, not a shared database across apps.
env.assets is only defined if the app declared the buildy:storage/blob@1.0 import. Its shape:
env.assets.get(key); // Promise<{ bytes: Uint8Array; contentType: string } | undefined>
env.assets.put(key, bytes, { contentType }); // Promise<void>
env.assets.delete(key); // Promise<boolean>
env.assets.list(); // Promise<Array<{ key: string; contentType: string; sizeBytes: number }>>
env.assets.url(key); // string - same-origin serve URL
env.assets.url(key) returns a same-origin URL your app’s UI can drop into an <img src> or <a href>, or your app’s backend can read the raw bytes itself with .get(key). window.buildy.assets mirrors it on the frontend: getUrl(key) returns the same-origin URL synchronously; load(key) fetches the file and resolves a data: URL instead, for hosts like Claude and ChatGPT whose own CSP blocks the cross-origin getUrl result; upload(file, opts?) posts a File or Blob and resolves { key, url }; list() and delete(key) round out the set.
Limits, access, and deletion
- Per-value limit. Each value you
putis capped at 10MB, measured as its JSON-serialized UTF-8 byte length. That cap applies to any one value; your keys as a group aren’t capped separately. - Serialization.
putstores JSON-compatible data: objects, arrays, strings, numbers, booleans, andnull. A value that can’t be JSON-serialized (a circular reference, aBigInt) fails the write. - Key order.
list()returns your keys but doesn’t guarantee an order; sort them yourself if order matters. - Storage failures. A
putthat exceeds the per-value cap fails witherror.kind: "storage_quota_exceeded". The recovery is to shrink the value, for example by trimming old entries, and retry. - Per-file limit. Each file is capped at 10MB, measured as raw bytes, not JSON-encoded like key-value storage.
- Per-app file totals. Up to 50 files per app, 100MB combined. A
putthat would cross either cap fails and leaves what’s already stored untouched. - File keys. URL-safe: letters, digits,
.,_,-, up to 128 characters, no leading dot, no..sequences. - File content type. Defaults to
text/plainfor atextpayload, the sniffed response header forsourceUrl(falling back toapplication/octet-stream), or the browserFile’s own type forwindow.buildy.assets.upload.text/html,application/xhtml+xml, andimage/svg+xmlare always rejected: files serve back same-origin, so any of those would run as active content if opened directly. upload_assetinput caps.sourceUrlis fetched server-side and cut off the moment it crosses the 10MB file cap, even if the remote server sends a missing or lyingContent-Length.dataBase64decodes to at most 64KB and exists only for small binaries. Keep the encoded string under ~16KB; a larger base64 tool-call argument can stall in some MCP clients before it ever reaches the server.- File failure behavior. A rejected write carries an
error.kind:asset_too_large,asset_count_exceeded, orasset_quota_exceededfor a cap,asset_invalid_keyorasset_invalid_content_typefor bad input. The recovery is to shrink, remove, or rename and retry. - Access. An app’s storage and files belong to the app, not to any one visitor. Apps are single-tenant: one app is one shared dataset, owned by whoever created it (or claimed it). The owner, and anyone they invite as a collaborator or end-user, can read and write storage and files through the app’s operations or
upload_asset. A read-only viewer can read but not write. - Deletion. Deleting an app doesn’t purge key-value storage or secrets by default;
delete_apptakes apurge_storage: trueflag to wipe those along with the app entry. Files are different: deleting an app always removes its stored files too, whether or notpurge_storageis set.