Store data from your app
Save per-user data from your app with the ambient window.panoplyStorage API — no backend required
Every deployed app gets an ambient window.panoplyStorage client, so your app can remember what each visitor does without standing up a backend. Write against window.panoplyStorage and it works the same whether the app is backed by a real per-app database or by the visitor's browser.
The API
window.panoplyStorage is available as a global before your own scripts run. Every method returns a Promise, so await them.
get(key)→ the stored value, ornullif the key doesn't exist.list(prefix?)→ an array of{ key, value, updatedAt }. Pass aprefixto return only keys that start with it; omit it to list everything.set(key, value)→{ ok: true }.valueis any JSON-serializable data.delete(key)→{ ok: true }.
await window.panoplyStorage.set('score', 42)
const score = await window.panoplyStorage.get('score') // 42, or null if unset
const games = await window.panoplyStorage.list('game:') // [{ key, value, updatedAt }, …]
await window.panoplyStorage.delete('score')
Keys are strings. Values are anything JSON can represent — objects, arrays, numbers, strings, booleans, null.
Two backings, one API
The client comes in two variants, injected at deploy time. Your code doesn't change between them:
- Server-backed — the app has a storage slot, so it has its own database. Data is saved server-side, scoped to the signed-in user, and encrypted at rest. It follows the user across devices and browsers.
- Browser-backed — the app has no slot (a free-plan app that isn't using its one storage slot, or storage that wasn't requested). The same API is backed by the visitor's
localStorage: data stays on that one device and browser,updatedAtis alwaysnull, and there are no network calls.
Which one an app gets is decided by your plan and whether the app holds a storage slot — see the plan matrix on the pricing page. Design for the browser-backed case if the app might not have a slot: it still works, the data is just per-device.
Scope, persistence, and privacy
- Server-backed rows are per user — each signed-in visitor sees only their own data — and encrypted at rest. It is not a shared or public database, and not a secrets vault: don't store API keys or credentials in it.
- Browser-backed data lives only in that visitor's browser. Clearing site data removes it, and it never leaves the device.
Limits
Server-backed storage has fixed per-user quotas (they are not tier-derived):
| Limit | Value |
|---|---|
| Keys per user | 10,000 |
| Bytes per value | ~1 MB |
| Total bytes per user | ~50 MB |
They suit preferences, saved state, and cached results — not bulk data or file hosting. Browser-backed storage is bounded by the browser's own localStorage limit instead.
Under the hood — the wire contract
You normally only need the window.panoplyStorage methods above. For tools generating an app, or for debugging, this is what the server-backed client does: it POSTs JSON same-origin to /__panoply/storage with an action and the operation's fields.
| action | request | response |
|---|---|---|
get | { key } | { value } (value is null if absent) |
list | { prefix? } | { items: [{ key, value, updatedAt }] } |
set | { key, value } | { ok: true } |
delete | { key } | { ok: true } |
Behavior at the edges:
- POST only — any other method returns
405. - Same-origin only — a request from another site returns
403with codecross_origin. That includes another app onpanop.ly. 501when the app has no database bound — browser-backed apps never call the route, so this only appears if server-backed code runs somewhere it shouldn't.- When the session has expired, the client renews it in the background, without leaving the page, and retries the call once. If it can't (the visitor signed out of Panoply, or no longer owns the app), the call rejects with
code: "session_expired"and nothing navigates, so your app decides what to show.panoplyFilesandpanoplyVaultbehave the same way.