---
title: Data storage
description: 'Every app comes with storage your agent can read and write: lists, settings, totals, even files.'
sidebar:
  label: Data storage
  order: 3
---

## 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](limits#per-app-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`](/docs/technical-reference/mcp/tools/upload_asset).

### Copy this prompt for your agent

```text
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/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 have two paths:

- **Agent upload.** Call [`upload_asset`](/docs/technical-reference/mcp/tools/upload_asset). The tool reference defines its inputs and errors.
- **App storage.** Declare `charming:storage/blob@1.0`, then use `env.assets` in a handler or `window.charming.assets` in the UI.

### The contract

`env.storage` is only defined if the app declared the `charming:storage/kv@1.0` import. Its shape:

```ts
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/blob@1.0` import. Its shape:

```ts
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](../technical-reference/external-files-and-csp) for URL lifetime and host behavior. A browser UI can also upload, list, and delete files directly:

```js
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](/docs/technical-reference/browser-runtime#uploaded-files) defines the full method and error contract.

### Limits, access, and deletion

- **Serialization.** `put` stores JSON-compatible data: objects, arrays, strings, numbers, booleans, and `null`. A value that can't be JSON-serialized (a circular reference, a `BigInt`) 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](limits#per-app-limits) and [`upload_asset`](/docs/technical-reference/mcp/tools/upload_asset).
- **Access.** Storage belongs to the app. See [Privacy and sharing](privacy-and-sharing).
- **Deletion.** Deleting an app doesn't purge key-value storage or secrets by default; `delete_app` takes a `purge_storage: true` flag to wipe those along with the app entry. Files are different: deleting an app always removes its stored files too, whether or not `purge_storage` is set.

## Related

- [Limits](limits)
- [How Charming works](../concepts/how-charming-works)
- [Browser runtime API](../technical-reference/browser-runtime)
- [External files and CSP](../technical-reference/external-files-and-csp)
- [Docs home](..)
- [llms-full.txt](https://charm.ing/docs/llms-full.txt)

Found a bug or need a feature? [Tell us](/docs/capabilities/feedback) with `submit_feedback` or `POST /app/{id}/feedback`. Your feedback shapes what we build next.
