A playground tab with TypeScript IntelliSense showing delete / get / getWithMetadata / list / put on env.feature_flags, and the CONSOLE panel docked below
Full IntelliSense: autocomplete against the account's real namespaces and the KV binding API, with the CONSOLE panel docked below.

What it is — and what it isn't

A playground is a data-access scratchpad. You get the Cloudflare Workers env binding model, standard JavaScript built-ins, and full TypeScript autocomplete for every namespace on the current account. That's it.

It is not a general JS sandbox. There is no network access beyond KV, no DOM, no file system, no fetch, no setTimeout beyond 30 seconds, and no module imports. Playgrounds are for reading, writing, listing, and transforming KV data — if you need to do anything else, it belongs in a real Worker, not here. See Limitations below.

Creating a playground

Right-click on an account in the sidebar tree and choose New Playground…, or expand the Playgrounds section under the account and click the + button in its hover-revealed toolbar. A new tab opens with an empty TypeScript editor on top and a console panel (powered by xterm.js) on the bottom. Your code is autosaved ~500 ms after every keystroke — you can close the tab at any time without losing work.

Playgrounds are scoped to the account they were created under. They live in KVault's local SQLite database, not on Cloudflare — nothing leaves your machine except the KV calls themselves.

Right-clicking a playground in the sidebar opens a menu with Rename…, Export to file…, and Delete. The upload icon next to the + in the Playgrounds section header imports a .ts file as a new playground — round-tripping scripts through git is a supported workflow.

IntelliSense

The playground is a full TypeScript editor — same experience you get in VS Code, not a cut-down textbox:

  • Autocomplete on your namespaces — type env. and every namespace on the current account shows up, each typed as a KVNamespace. The list regenerates whenever namespaces are added, renamed, or removed, so autocomplete is always in sync with what Cloudflare actually has.
  • Autocomplete on the KV API.get, .put, .delete, .list, .getWithMetadata pop up with their real signatures. Hit Tab to fill parameters, hover any method for docs, jump to the KVNamespace type definition to see the full surface.
  • Inline type checking — passing a number where a string is expected, or forgetting await, gets a red squiggle as you type. Errors live in the same editor as the code, so you catch them before Run.
  • Hover docs and parameter hints — hover an identifier to see the real namespace title and id; hover a KV method to see its parameter types and description; type ( and the parameter hints stay visible as you fill them in.
  • Top-level await and return are first-class — the editor doesn't flag either, and the runtime wraps your script so both work at file scope.

The env binding

Every namespace on the account is exposed as a property on a global env object, using the Cloudflare Workers convention. Namespace titles are normalized to valid JS identifiers:

  • Non-alphanumeric characters replaced with _ (case is preserved — the title you see in the tree is the identifier you type).
  • A leading underscore is prepended if the title would otherwise start with a digit.
  • Collisions (two titles normalizing to the same identifier) get a numeric suffix like _2, _3.

So a namespace titled user sessions becomes env.user_sessions, and a namespace titled 42-flags becomes env._42_flags. Hover an identifier in the editor to see the real namespace title and id.

Because the Worker binding exposes the namespaces via env as well as via the global scope, self.USER_SESSIONS also works — pick whichever feels natural.

Read-only vs Live mode

New playgrounds start in Read-only mode — every put() or delete() call throws, no request reaches Cloudflare. This is the default because "I just wanted to peek at some keys" should never be a foot-gun.

To enable writes, click the mode toggle in the tab header; the dot turns from green to red. The mode is saved per playground, so a migration script you intend as live stays live across app restarts. The toggle is disabled while a run is in progress — mid-run switches don't affect the active run either way, which avoids confusing half-applied scripts.

Running a script

Click Run in the tab header, or press ⌘/Ctrl + Enter. The script is transpiled in-browser by esbuild-wasm (bundled locally with the app — no network fetch), then run in a sandboxed iframe (sandbox="allow-scripts", no same-origin) with the env bindings injected via a message-passing RPC. The iframe cannot touch KV directly — every get / put / delete / list is forwarded to the Rust backend over postMessage, which is also where the Read-only gate is enforced.

Console output streams into the xterm panel as it happens; console.log, .info, .warn, .error, and .debug are all wired and colored by level. The final return value of the script is pretty-printed at the end under a —— return value —— divider.

To stop a run, click Stop or press ⌘/Ctrl + .. Stop cancels all future KV operations immediately. Requests whose HTTP call is already past the point-of-no-return may still complete at Cloudflare, so treat Stop as a best-effort kill switch, not an undo.

The console panel

The bottom panel is an xterm.js terminal wrapped in its own header bar. The header stays docked even when the console is collapsed so the affordance for reopening it is always visible.

  • Clear (trash icon) — wipes the xterm buffer without touching the editor.
  • Copy output (copy icon) — copies the entire terminal buffer to the clipboard and raises a toast; if the buffer is empty, the toast says so.
  • Collapse / Show (chevron) — toggles the terminal area. Dragging the divider between editor and console also collapses; the chevron stays in sync either way.

The console auto-expands on Run even if it was collapsed, so you never lose output to a hidden panel. Clear and Copy remain clickable while collapsed — both act on the buffered terminal, not its visibility.

API reference

The playground implements the full Cloudflare Workers KV binding surface. Method signatures and semantics match the official Workers KV API exactly — code copied from a real Worker runs here unchanged.

env.NS.get(key, type?)

Reads a single key. type is one of 'text' (default), 'json', 'arrayBuffer', or 'stream'. Returns the value, or null if the key doesn't exist.

env.NS.get(keys, type?)

Bulk read up to 100 keys in one call (Cloudflare's limit, enforced server-side). Returns a Map<string, Value | null>. Only 'text' and 'json' are allowed for bulk reads.

env.NS.getWithMetadata(key | keys, type?)

Same as get, but returns { value, metadata } (or a Map of them for bulk calls). Metadata is whatever was stored with the key on put.

env.NS.put(key, value, options?)

Writes a value. value can be a string, ArrayBuffer, ArrayBufferView, or ReadableStream. Options:

  • expiration — absolute Unix timestamp (seconds since epoch).
  • expirationTtl — relative seconds from now.
  • metadata — any JSON-serializable value (Cloudflare caps the serialized form at 1 KiB).

Read-only mode rejects this call before any request is made.

env.NS.delete(key)

Removes a key. A successful call for a non-existent key is treated as success (matches Cloudflare's behavior — no error is raised). Read-only mode rejects this call before any request is made.

env.NS.list(options?)

Lists keys with prefix, limit, and cursor. Returns { keys, list_complete, cursor } matching the Workers binding verbatim. Unlike the rest of KVault's UI, this call is always live — it hits Cloudflare directly and returns real cursors, so pagination behaves the same as in production.

Examples

Read a single value

const session = await env.USER_SESSIONS.get('session:abc-123', 'json');
console.log(session);
return session;

Scan a prefix and open one

const res = await env.USER_SESSIONS.list({ prefix: 'user:42:', limit: 50 });
console.log('matches:', res.keys.length);
return res.keys.map(k => k.name);

Bulk TTL bump (requires Live mode)

const { keys } = await env.SESSIONS.list({ prefix: 'short-lived:' });
for (const k of keys) {
  const value = await env.SESSIONS.get(k.name);
  if (value !== null) {
    await env.SESSIONS.put(k.name, value, { expirationTtl: 86400 });
  }
}
return `bumped ${keys.length} keys`;

Migrate with a transform (requires Live mode)

const { keys } = await env.PROFILES.list({ prefix: 'profile:' });
let moved = 0;
for (const k of keys) {
  const val = await env.PROFILES.get(k.name, 'json');
  if (val && typeof val === 'object' && 'email' in val) {
    await env.PROFILES_V2.put(k.name, JSON.stringify({ ...val, emailLower: val.email.toLowerCase() }));
    moved++;
  }
}
return moved;

Limitations

Playgrounds are deliberately narrow. Reaching for any of the following throws a clear error pointing back at this page:

  • No networkfetch, XMLHttpRequest, WebSocket, and EventSource are shadowed and throw on access. The only way out of the sandbox is the env bindings.
  • No DOM or storagedocument, navigator, localStorage, sessionStorage, indexedDB, and caches are all shadowed.
  • No modulesimport() and require() are not available. Playgrounds are single-file.
  • Timers are cappedsetTimeout / setInterval are allowed for short delays but any value above 30 s is clamped. Any residual timers are cleared when the run ends.
  • Mock account is unsupported — the dev-only mock account can't run playgrounds. Switch to a real Cloudflare account.
  • Cancellation is best-effort for writes — clicking Stop cancels future operations, but requests already in flight at the Cloudflare edge may still commit. Treat Live mode as "these changes are probably going to happen the instant you ask for them".
  • Cloudflare's limits apply — key size (512 bytes), value size (25 MiB), metadata size (1 KiB), bulk-read max (100 keys), TTL min (60 s), writes per key (1 /s). These are enforced at Cloudflare, not mirrored client-side, so their numbers are always current.

Tips

  • Start in Read-only. Drop the script in, see it works, then flip to Live and run again. The extra second is worth it.
  • Top-level await works. You don't need to wrap the script in an async function.
  • Top-level return also works — the script is implicitly wrapped in an async function, so return at the top of the file ends the run and surfaces its value under the return-value divider in the console.
  • Use the return value as a lightweight summary: return `updated ${n} keys`.
  • Switching tabs doesn't reset the playground — editor state and the console's scrollback survive tab switches so you can hop away and come back to the same output.
  • Export to .ts to share or version-control important scripts; re-import via the upload icon in the Playgrounds section header whenever.