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. 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); // string - same-origin serve URL
Use env.assets.url(key) or window.charming.assets.getUrl(key) for a time-limited signed Charming URL. 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 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.