Storage & settings
Storage
A card’s page has no localStorage, indexedDB or cookies — its sandbox has no origin to keep them
in. card.storage is the replacement: JSON values the app keeps on disk, atomically, across reloads,
suspension and restarts. No permission needed.
// This card on the canvas (instance scope).
const draft = await card.storage.get('draft', '') // string, '' when missing
await card.storage.set('draft', `${draft}\nmore`)
await card.storage.set('runs', [{ at: Date.now(), failed: 3 }])
const runs = await card.storage.get<{ at: number; failed: number }[]>('runs') // undefined when missing
await card.storage.delete('draft')
const keys = await card.storage.keys('run:') // keys starting with a prefix
const { bytes, quota } = await card.storage.usage()
render(runs?.length, keys, bytes, quota)
// Shared by every copy of this card, on every canvas (package scope).
await card.storage.package.set('lastSync', Date.now())
card.storage.onChange(({ key, byInstance }) => {
if (key === 'lastSync') render(`updated by card ${byInstance}`)
})| Method | Result |
|---|---|
get(key) | The value, or undefined. |
get(key, fallback) | The value, or fallback. The result is typed like the fallback. |
set(key, value) | Stores any JSON value. |
delete(key) | |
keys(prefix?) | string[] |
clear() | Deletes every key of that scope. |
usage() | { bytes, quota, keys } |
onChange(handler) | Another copy of this card wrote a package key: { scope, key, byInstance }. |
The same methods exist on card.storage.package.
Limits: keys ≤ 256 characters, values ≤ 1 MB, ≤ 10 000 keys; 5 MB per card, 20 MB per package.
Going over fails with QUOTA_EXCEEDED. Writes reach the disk within about a quarter of a second
and are flushed when the app quits.
What happens to it: instance storage is deleted with the card. Package storage survives
uninstalling unless the user ticks Also delete its saved data and secrets. Storage survives
updates — keep your stored shapes readable by newer versions (store a version field if you
might change them).
Save before you are unloaded: a card that is hidden for a while is suspended, and its page is
thrown away. Use card.lifecycle.onSuspend to write what is
still in memory.
Settings
Declare fields in the manifest’s settings; the app draws the form
(from the card’s ⋯ → Settings…, or when you call card.settings.open()), validates it and
stores the values. Your card reads them:
const command = card.settings.value<string>('command') ?? 'npm test'
const all = card.settings.values // defaults filled in, secrets never included
const hasKey = card.settings.hasSecret('apiKey') // true when the user saved one
card.settings.onChange((snapshot) => {
render(snapshot.values['command'], snapshot.secrets['apiKey'])
})
// Change your own non-secret settings (a toggle in your UI, say). Validated like the form.
await card.settings.set({ compact: true })
// Open the form on the card (only while it is visible).
if (!hasKey) await card.settings.open()
render(command, all)| Member | What |
|---|---|
values | Current values, defaults filled in. Kept current. |
secrets | Record<key, boolean>: which secret settings are set. |
value(key) | One value, typed by you. |
hasSecret(key) | Whether a secret setting is set. |
get() | A fresh SettingsSnapshot from the app. |
set(values) | Changes non-secret settings; every copy of the card gets settings.changed. Invalid values fail with INVALID_PARAMS. At most 60 a minute. |
open() | Opens the settings form on the card. NOT_VISIBLE if the card is off screen. |
onChange(handler) | The user saved the form, or set changed something. |
Scopes. A field with "scope": "package" has one value for every copy of your card; the rest are
per card.
Secrets. A secret field is typed only into the app’s own dialog — in the settings form its row
shows just “set” or “not set” and opens that window-wide dialog — and stored encrypted by the app. The
card can never read it — not through values, not through any method. To use one, put
{{secret:<key>}} into a network request header; the app fills it
in on its way out, only for internet hosts your card was granted (never for localhost). Do not build your own “paste your API key”
field inside the card: the key would then be yours to leak, and users are told that NeuroSquad
never asks for keys inside a card.