Key/value store
Small per-user JSON values scoped to your app — settings, state, and caches.
A per-user JSON store scoped to your app. Use it for settings, saved state, drafts, and small caches — the things that should survive between sessions without you running a database.
Requires the kv.read and kv.write scopes.
Store a value#
curl -X PUT https://api.portmodels.com/connect/kv/preferences \
-H "Authorization: Bearer pmc_..." \
-H "Content-Type: application/json" \
-d '{"value": {"theme": "dark", "volume": 7}}'The body must be a JSON object with a value field. value itself can be any
JSON — object, array, string, number, boolean.
Read a value#
curl https://api.portmodels.com/connect/kv/preferences \
-H "Authorization: Bearer pmc_..."A missing key returns 404 with {"error": "key not found"}.
List keys#
curl https://api.portmodels.com/connect/kv \
-H "Authorization: Bearer pmc_..."{ "keys": ["preferences", "draft:2026-08-01", "cache:model-list"] }Delete a value#
curl -X DELETE https://api.portmodels.com/connect/kv/preferences \
-H "Authorization: Bearer pmc_..."{ "message": "deleted" }Limits#
| Limit | Value |
|---|---|
| Key length | 1–256 characters |
| Value size | 64KB encoded |
| Keys per user, per app | 1000 |
Exceeding the value size returns 400 with
value exceeds 64KB limit; exceeding the key count returns 400 with
key limit reached (1000 keys per user per app).
For anything larger than 64KB, use file storage.
Isolation#
Entries are namespaced per (app, user). Your app cannot see what another app stored for the same user, and cannot see another user's entries. When the user disconnects your app your token stops working, but the data stays until it is deleted.
Patterns that work well#
Namespace your keys. A flat namespace gets crowded at 1000 keys. Prefix by
purpose: pref:theme, draft:2026-08-01, cache:models.
Read once per session. Load the user's settings at the start of a session and keep them in memory rather than re-reading on every interaction.
Use it as a cache. Storing a derived result the user will need again is usually cheaper than regenerating it — see Pricing and markup.
Version your shape. Store {"v": 2, ...} so an older client can recognize a
newer format instead of misreading it.
async function loadPrefs(token) {
const res = await fetch('https://api.portmodels.com/connect/kv/preferences', {
headers: { Authorization: `Bearer ${token}` },
});
if (res.status === 404) return DEFAULT_PREFS; // first run
if (res.status === 403) return DEFAULT_PREFS; // user declined kv.read
return (await res.json()).value;
}Tip
Degrade gracefully when the user declined the storage scopes. An app that still works — just without remembering anything — beats one that refuses to start.