Skip to content

feat(db): window.storage, the Claude artifacts storage API - #20

Merged
notsuhas merged 7 commits into
mainfrom
feat/window-storage
Oct 9, 2026
Merged

notsuhas merged 7 commits into
mainfrom
feat/window-storage

Conversation

@notsuhas

@notsuhas notsuhas commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #18. Adds window.storage, the storage API Claude artifacts expose, so sandboxed pages can keep small values across visits.

await window.storage.set(key, value)   // -> { key, value, shared: false }
await window.storage.get(key)          // -> { key, value, shared: false }, throws if missing
await window.storage.delete(key)       // -> { key, deleted: true, shared: false }
await window.storage.list(prefix?)     // -> { keys }

Keys are private to each viewer. Claude's shared = true is refused with an error pointing at postplan.db; nothing is shared across viewers. Nothing is stored unless a page calls set, and rows go with the site and with the user.

  • site_kv table (siteId, userId, key, value, bytes), both FKs cascade. Keys up to 200 chars with no spaces, slashes or control chars; values up to 1 MB; 5 MB and 1,000 keys per viewer per site, checked and written in one statement.
  • /api/_data/_kv[/key] is its own terminal sub-app with its own capability table: reads need read, writes need create.
  • list is a case-sensitive prefix match over the primary key, at most 1,000 keys.
  • postplan.db collection names starting with _ are now reserved (prod has no documents, so nothing breaks).

Migration: 0036_site_kv — apply before deploying.

Testing

  • bun run test: per-viewer isolation, quota by bytes and by key count, case-sensitive prefix list, user delete cascade, read-only token can't write, /_kv is terminal; broker mapping and validation tests. Typecheck, biome, dead-code clean.
  • Local dev stack: a page counted visits across reloads (visits=2) and set(..., true) rejected.

🤖 Generated with Claude Code

@notsuhas
notsuhas force-pushed the feat/window-storage branch 4 times, most recently from 406b12d to 2a8ec5f Compare October 9, 2026 13:07
@notsuhas
notsuhas changed the base branch from fix/iframe-hardening to main October 9, 2026 13:07
@notsuhas
notsuhas marked this pull request as ready for review October 9, 2026 13:07
@notsuhas notsuhas closed this Oct 9, 2026
@notsuhas notsuhas reopened this Oct 9, 2026
notsuhas and others added 7 commits October 9, 2026 18:40
Pages can now keep small values across visits with the same API Claude
artifacts expose: window.storage.get/set/delete/list(key, ..., shared).
Personal keys belong to the viewer; shared keys to everyone who can open the
site. Nothing is stored unless a page calls set.

- site_kv table (siteId, ownerId '' for shared, key, value), 200-char keys,
  1 MB values, 20 MB per site
- /api/_data/_kv/{personal|shared}[/key] behind the data plane; reads need
  read, writes need create (every viewer), checked in the capability gate
- the SDK defines window.storage and sends it through the viewer's broker as
  a single 'kv' op, so the page still never holds a credential

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- /_kv is its own sub-app with its own capability table, mounted before
  the method gate; no path regex. Anything under /_kv it doesn't route
  (PUT /_kv/shared, DELETE /_kv/personal) falls through to the document
  routes and keeps the owner-only write rule
- quota is checked and written in one INSERT … SELECT … ON CONFLICT …
  RETURNING statement, so concurrent sets can't jointly overshoot
- sizes are UTF-8 bytes (bytes column); 1 MB per value, 5 MB per viewer for
  personal keys, 20 MB per site
- a trigger removes a deleted user's personal keys
- key/value limits and the key validator live in packages/shared/kv.ts and
  are used by both the broker and the server; the broker parses kv calls
  into a typed union
- responses carry `shared`; docs warn that shared keys are writable by any
  viewer and unattributed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, quota pools

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One viewer could fill the site-wide key/byte cap and lock everyone out. Each
pool (a viewer's personal keys, the shared keys) now caps itself, so the site
counters and their triggers go. list uses a key range instead of LIKE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d keys

Shared keys were writable by anyone who could open a site, with no
attribution. Every key now belongs to one viewer (userId FK, cascades with
the user); `shared = true` rejects and points at postplan.db.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@notsuhas
notsuhas force-pushed the feat/window-storage branch from 2a8ec5f to 789cae6 Compare October 9, 2026 13:10
@notsuhas
notsuhas merged commit 9ec9d5e into main Oct 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant