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, JavaScript, and other files. See Limits for current caps.
Technical
Apps use env.storage for JSON values and env.assets or window.charming.assets for files. An agent can also call upload_asset.
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 charming:storage/[email protected] 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 have two paths:
- Agent upload. Call
upload_asset. The tool reference defines its inputs and errors. - App storage. Declare
charming:storage/[email protected], then useenv.assetsin a handler orwindow.charming.assetsin the UI.
The contract
env.storage is only defined if the app declared the charming:storage/[email protected] 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 charming:storage/[email protected] 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); // Promise<string> - signed Charming URL; await it
Accepted file types are images other than SVG, audio, video, fonts, PDF, JSON, plain text, CSV, Markdown, JavaScript, and application/octet-stream. JavaScript uploads (text/javascript, application/javascript, or application/x-javascript) are stored as text/javascript, and a .js or .mjs upload that declares no type is treated as JavaScript. HTML, SVG, XHTML, WebAssembly, and other types fail with asset_invalid_content_type. Only a caller who can edit the app, such as the agent with upload_asset or the app token, can upload JavaScript; window.charming.assets.upload() from the page is refused with 401. An uploaded script runs only at the app’s own origin on the web; see uploaded scripts and workers.
Use await env.assets.url(key) or window.charming.assets.getUrl(key) for a time-limited signed Charming URL. env.assets.url returns a Promise, so return { url: await env.assets.url(key) } from a route, not the unawaited call. Store the key, not the URL. Use window.charming.assets.load(key) when an uploaded image must render in a chat embed. See External files and CSP for URL lifetime and host behavior. A browser UI can also upload, list, and delete files directly:
const uploaded = await window.charming.assets.upload(file, { key: 'avatar.png' });
const files = await window.charming.assets.list();
await window.charming.assets.delete(uploaded.key);
Reading and listing require app read access. Uploading and deleting require app run access, so a read-only viewer cannot use them. The Browser runtime API defines the full method and error contract.
Limits, access, and deletion
- 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. - Caps and errors. See Limits and
upload_asset. - Access. Storage belongs to the app. See Privacy and sharing.
- Deletion. Deleting an app from its settings page always deletes its key-value storage and secrets with it. Through an agent,
delete_apptakespurge_storage: trueandDELETE /app/{id}takes?purge=storageto do the same. Without the flag, the app’s secrets are kept, and an hourly cleanup clears its key-value storage once no app owns it. Files are different: deleting an app always removes its stored files too, whether or notpurge_storageis set.