App Storage
A built-in key-value store for paid-tier creator apps
App Storage is a simple, server-side key-value store for your published apps. It lets a deployed app save and read data — user settings, cached results, generated content — with no database to set up, no API keys to manage, and no auth code to write.
It's automatic on paid tiers. When a Hobby or Pro creator's app is deployed, Panoply injects everything it needs at build time. Free-tier apps don't get storage.
How it's enabled
When your app deploys to {your-app}.panop.ly, Panoply adds a small runtime to your page automatically. That gives your app a ready-to-use panoplyStorage object — no installation, no npm package, no build step.
// Available globally in your deployed app — nothing to import.
await panoplyStorage.set('greeting', { text: 'Hello' })
const greeting = await panoplyStorage.get('greeting') // { text: 'Hello' }
Storage is scoped to your app. Each app reads and writes only its own data.
The four methods
| Method | Returns | Description |
|---|---|---|
get(key) | the stored value, or null | Read one key |
list(prefix?) | array of { key, value, updatedAt } | List all keys, optionally filtered by prefix |
set(key, value) | { ok: true } | Create or overwrite a key (upsert) |
delete(key) | { ok: true } | Remove a key |
Values can be any JSON-serializable data — objects, arrays, strings, numbers.
await panoplyStorage.set('digest:2026-05-31', { headlines: [/* … */] })
const today = await panoplyStorage.get('digest:2026-05-31')
const allDigests = await panoplyStorage.list('digest:')
// [{ key: 'digest:2026-05-31', value: {…}, updatedAt: '2026-05-31T…' }]
await panoplyStorage.delete('digest:2026-05-31')
Writing your own client
The injected panoplyStorage global is optional convenience. Under the hood it's a single POST to your storage endpoint — you can call it directly if you prefer. The two values you need are injected as globals: PANOPLY_APP_ID and PANOPLY_STORAGE_URL.
fetch(PANOPLY_STORAGE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'set', appId: PANOPLY_APP_ID, key: 'k', value: { a: 1 } }),
}).then((r) => r.json())
The action field is one of get, list, set, or delete. Pass key for get/set/delete, value for set, and an optional prefix for list.
Quotas
Quotas apply per app and depend on the creator's plan:
| Tier | Keys per app | Total size | Max value size |
|---|---|---|---|
| Free | — | — | No storage |
| Hobby | 1,000 | 5 MB | 100 KB |
| Pro | 10,000 | 50 MB | 1 MB |
A set that would exceed a limit is rejected — your app receives an error response and the existing data is left untouched.
Worked example: a news digest
A common pattern: an app that fetches data once a day and caches the result, so every visitor reads from storage instead of re-fetching.
async function getTodaysDigest() {
const today = new Date().toISOString().slice(0, 10) // "2026-05-31"
const key = `digest:${today}`
// Serve the cached digest if we already built it today.
const cached = await panoplyStorage.get(key)
if (cached) return cached
// Otherwise build it once, store it, and return it.
const headlines = await fetchHeadlines()
const digest = { date: today, headlines }
await panoplyStorage.set(key, digest)
return digest
}
The first visitor of the day triggers the fetch and write; everyone after reads the cached value. Use list('digest:') to show an archive of past days, and delete to prune old entries.
Related
- Pricing & the Tiered Commission — Tiers and what each plan includes
- Publishing — How apps get deployed