From a06d0c60b1d31d32d5e988153b90a4e07f92bec7 Mon Sep 17 00:00:00 2001 From: Brent Jackson Date: Mon, 1 Jun 2026 16:51:49 -0400 Subject: [PATCH 1/6] Add blob storage skill --- package-lock.json | 4 +- plugin/skills/blob-storage/SKILL.md | 81 +++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 plugin/skills/blob-storage/SKILL.md diff --git a/package-lock.json b/package-lock.json index 30c1354..e1eb654 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@valtown/skills", - "version": "0.1.0", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@valtown/skills", - "version": "0.1.0", + "version": "0.1.2", "license": "MIT", "devDependencies": { "@changesets/cli": "^2.31.0", diff --git a/plugin/skills/blob-storage/SKILL.md b/plugin/skills/blob-storage/SKILL.md new file mode 100644 index 0000000..2c028c5 --- /dev/null +++ b/plugin/skills/blob-storage/SKILL.md @@ -0,0 +1,81 @@ +--- +name: blob-storage +description: Use when a val needs simple key/value persistence — JSON documents, cached responses, uploaded files, or binary assets. Covers the std/blob API, listing and deleting keys, account-global scoping, and storage limits. +triggers: [blob, storage, kv, key-value, persistence, cache, store, upload, file, json, asset, binary] +--- + +# Blob Storage + +Val Town provides built-in key/value blob storage via the `std/blob` module. Reach for it whenever a val needs to persist simple values — JSON documents, cached API responses, uploaded files, or binary assets — keyed by a string. For relational or structured data you query with SQL, prefer `std/sqlite` instead. + +## Basic usage (JSON) + +```ts +import { blob } from "https://esm.town/v/std/blob"; + +await blob.setJSON("config", { theme: "dark", count: 0 }); + +const config = await blob.getJSON("config"); +// config = { theme: "dark", count: 0 }, or undefined if the key doesn't exist +``` + +`getJSON` returns `undefined` when the key is missing, so guard before using the result: + +```ts +const config = (await blob.getJSON("config")) ?? { theme: "light", count: 0 }; +``` + +## Raw and binary data + +Use `set`/`get` for strings, binary, or any `BodyInit`. `get` returns a standard `Response`, so use its body helpers (`.text()`, `.json()`, `.arrayBuffer()`, `.blob()`): + +```ts +await blob.set("logo.png", imageBytes); // string | BodyInit (Blob, ArrayBuffer, ReadableStream, …) + +const res = await blob.get("logo.png"); +const bytes = await res.arrayBuffer(); +``` + +Unlike `getJSON`, `get` **throws** `ValTownBlobNotFoundError` if the key doesn't exist — wrap it in `try/catch` when the key may be absent. + +## Listing, deleting, copying + +```ts +const entries = await blob.list("user_"); // optional key prefix filter +// entries = [{ key, size, lastModified }, …] + +for (const { key } of entries) { + await blob.delete(key); +} + +await blob.copy("config", "config.bak"); // duplicate under a new key +await blob.move("draft", "published"); // rename / relocate +``` + +`list(prefix?)` returns an array of `{ key: string; size: number; lastModified: string }` — objects, not bare key strings. + +## Scoping: account-global, not per-val + +**Blob storage is scoped to the owning account, shared across every val it owns** — not isolated per val like a val-scoped SQLite database. If you `setJSON("config", …)` in one val, another val owned by the same account reads the same key. (Your personal account counts as one such scope; an organization is another.) + +Because the namespace is shared, **prefix your keys to avoid collisions** between vals — e.g. `myapp:config`, `myapp:sessions:`. The `prefix` argument to `list` then lets you enumerate just one val's keys. + +## Limits + +- **Key length:** up to 512 characters. +- **Total storage:** 10 MB on the free plan, 1 GB on Pro — shared across all blobs in the account. +- Store large or structured datasets in `std/sqlite` rather than as one giant blob. + +## Reading/writing blobs via tools + +When using the `storeBlob`, `readBlob`, `listBlobs`, or `deleteBlob` tools against a val owned by an organization (not your personal account), pass the org handle as the `org` parameter so the call hits that organization's blob storage. Example: `{ key: "myapp:config", org: "some-org" }`. This only matters for the tool calls — code inside the val reads and writes its owning account's storage automatically. Note `storeBlob` accepts UTF-8 text up to 100 KB; write larger or binary blobs from code with `blob.set`. + +## Rules + +- Treat keys as a flat namespace. Use prefixes (`feature:subkey`) for organization and to scope `list`. +- `getJSON` returns `undefined` for missing keys; `get` throws `ValTownBlobNotFoundError`. Handle the absent case accordingly. +- Don't store secrets in blobs — use environment variables for credentials. + +## Reference + +Full API docs: https://docs.val.town/std/blob/ From 83b9394c4bb48b5bc36e2ebf84a36db71b5d537e Mon Sep 17 00:00:00 2001 From: Brent Jackson Date: Mon, 1 Jun 2026 17:01:52 -0400 Subject: [PATCH 2/6] Undo lockfile change --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index e1eb654..30c1354 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@valtown/skills", - "version": "0.1.2", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@valtown/skills", - "version": "0.1.2", + "version": "0.1.0", "license": "MIT", "devDependencies": { "@changesets/cli": "^2.31.0", From 26374fe51875d6ee48f7b48ff5554ed07b951486 Mon Sep 17 00:00:00 2001 From: Brent Jackson Date: Mon, 1 Jun 2026 17:05:39 -0400 Subject: [PATCH 3/6] Add link to std/utils docs --- plugin/skills/http-endpoints/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/skills/http-endpoints/SKILL.md b/plugin/skills/http-endpoints/SKILL.md index da68434..0839437 100644 --- a/plugin/skills/http-endpoints/SKILL.md +++ b/plugin/skills/http-endpoints/SKILL.md @@ -36,7 +36,7 @@ app.onError((err) => Promise.reject(err)); export default app.fetch; ``` -`serveStatic` and `cors` middleware from Hono do **not** work on Val Town. Use `serveFile` / `staticHTTPServer` from `std/utils` for static files, and rely on Val Town's default CORS (see below). +`serveStatic` and `cors` middleware from Hono do **not** work on Val Town. Use `serveFile` / `staticHTTPServer` from `std/utils` for static files, and rely on Val Town's default CORS (see below). For the full `std/utils` API (`readFile`, `serveFile`, `staticHTTPServer`, `listFiles`, `listFilesByPath`, `httpEndpoint`, `parseVal`, …), fetch `https://utilities.val.run/docs.md`. ## CORS From a94b650783047feb2c5b31f74c1ef3d1b9559e1d Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Mon, 8 Jun 2026 13:25:58 -0400 Subject: [PATCH 4/6] Edit for blob scoping --- plugin/skills/blob-storage/SKILL.md | 37 ++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/plugin/skills/blob-storage/SKILL.md b/plugin/skills/blob-storage/SKILL.md index 2c028c5..c19fa90 100644 --- a/plugin/skills/blob-storage/SKILL.md +++ b/plugin/skills/blob-storage/SKILL.md @@ -8,10 +8,40 @@ triggers: [blob, storage, kv, key-value, persistence, cache, store, upload, file Val Town provides built-in key/value blob storage via the `std/blob` module. Reach for it whenever a val needs to persist simple values — JSON documents, cached API responses, uploaded files, or binary assets — keyed by a string. For relational or structured data you query with SQL, prefer `std/sqlite` instead. -## Basic usage (JSON) +## Scoping: account-global or per-val depending on import + +There are two exports of the blob utility: `global.ts`, which is scoped to the user account, and `main.ts`, which is scoped to the val itself. Prefer the `main.ts` interface and val scoping for new vals. + +Here is the scoped import: + +```ts +/** + * Importing from `main.ts` provides an interface to val-scoped blobs. + */ +import { blob } from "https://esm.town/v/std/blob/main.ts"; +``` + +Here are the global imports: ```ts +/** + * Importing from `global.ts` provides a blob interface that is scoped + * to your account. + */ +import { blob } from "https://esm.town/v/std/blob/global.ts"; +/** + * This entrypoint is also available as `v/std/blob`. This is common + * in older vals. + */ import { blob } from "https://esm.town/v/std/blob"; +``` + +Scoped & global blobs are stored separately: you cannot access global blobs with the scoped interface or vice versa. + +## Basic usage (JSON) + +```ts +import { blob } from "https://esm.town/v/std/blob/main.ts"; await blob.setJSON("config", { theme: "dark", count: 0 }); @@ -54,11 +84,6 @@ await blob.move("draft", "published"); // rename / relocate `list(prefix?)` returns an array of `{ key: string; size: number; lastModified: string }` — objects, not bare key strings. -## Scoping: account-global, not per-val - -**Blob storage is scoped to the owning account, shared across every val it owns** — not isolated per val like a val-scoped SQLite database. If you `setJSON("config", …)` in one val, another val owned by the same account reads the same key. (Your personal account counts as one such scope; an organization is another.) - -Because the namespace is shared, **prefix your keys to avoid collisions** between vals — e.g. `myapp:config`, `myapp:sessions:`. The `prefix` argument to `list` then lets you enumerate just one val's keys. ## Limits From 55274db27e43f8d3f9785f59529ff27e9910bd36 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Mon, 8 Jun 2026 13:28:04 -0400 Subject: [PATCH 5/6] More notes --- plugin/skills/blob-storage/SKILL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin/skills/blob-storage/SKILL.md b/plugin/skills/blob-storage/SKILL.md index c19fa90..4a9e131 100644 --- a/plugin/skills/blob-storage/SKILL.md +++ b/plugin/skills/blob-storage/SKILL.md @@ -36,6 +36,8 @@ import { blob } from "https://esm.town/v/std/blob/global.ts"; import { blob } from "https://esm.town/v/std/blob"; ``` +The scoped & global `blob` interfaces have the same methods. + Scoped & global blobs are stored separately: you cannot access global blobs with the scoped interface or vice versa. ## Basic usage (JSON) From 2154b59f118876b56337512d39f31c4dad95eded Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Mon, 8 Jun 2026 13:30:23 -0400 Subject: [PATCH 6/6] Add changeset --- .changeset/pretty-dingos-wear.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pretty-dingos-wear.md diff --git a/.changeset/pretty-dingos-wear.md b/.changeset/pretty-dingos-wear.md new file mode 100644 index 0000000..e471346 --- /dev/null +++ b/.changeset/pretty-dingos-wear.md @@ -0,0 +1,5 @@ +--- +"@valtown/skills": minor +--- + +Add instructions for using scoped blob storage