Storage on Every Host
wisp::kv, wisp::blobs and wisp::sql are one API on every Wisp host. The code stays the same. One environment variable per store picks where the data goes.
let kv = wisp::kv("SESSIONS");
kv.put_for(&format!("sess:{id}"), &user, 30 * 86_400).await?; // TTL in seconds
let who: Option<String> = kv.get(&format!("sess:{id}")).await?;
let page = kv.list_page("sess:", "", 100).await?; // Page { keys, cursor }
let photos = wisp::blobs("PHOTOS");
photos.put("a.png", png, "image/png").await?;
let blob = photos.get("a.png").await?; // Blob { body, content_type }
let rows: Vec<Recipe> = wisp::sql("DB").all("select * from recipes where owner = ?", &[&id]).await?;
The full API (metadata, absolute expiry, get_json, put_json, list) is on Bindings.
Choosing a Driver
WISP_KV_<NAME> picks the driver of wisp::kv("NAME"), WISP_BLOB_<NAME> of wisp::blobs("NAME") and WISP_SQL_<NAME> of wisp::sql("NAME"). Wisp reads and opens nothing until the first call to the store, so an app that uses none of them pays nothing. The code is not in its binary or its wasm.
WISP_KV_* / WISP_BLOB_* / WISP_SQL_* | kv | blobs | sql | Runs on |
|---|---|---|---|---|
cf:BINDING | Workers KV | R2 | D1 | Cloudflare Workers and Pages |
deno-kv, deno-kv:<path or url> | yes | yes, 64 KiB each | Deno, Deno Deploy | |
netlify-blobs, netlify-blobs:<store> | yes | yes | Netlify Functions and Edge Functions | |
fs, fs:<dir> | yes | yes | the built-in server, wisp dev, containers | |
memory | yes | yes | the built-in server (one process, gone on restart) | |
upstash, upstash:<url> | Redis over REST | every host | ||
s3:<endpoint>/<bucket> | yes | every host | ||
vercel-blob, vercel-blob:private | yes | every host | ||
libsql://<host>, https://<host> | yes | every host | ||
d1-http:<account id>/<database id> | yes | every host |
The drivers in the last five rows speak HTTP. On the edge they use the host's fetch. On the built-in server they use wisp::fetch. There, an https store needs Wisp's tls feature (wisp = { features = ["tls"] }, off by default). Without it, the first call answers a 500 that names the feature. wisp::fetch opens a new connection for each call, so on the built-in server each operation pays a TCP and TLS handshake (one or two round trips more than a kept connection). When that matters, keep hot keys in fs or memory.
Wisp makes the driver on a store's first call and keeps it. It reads the variables once.
Defaults per Host
| Host | Unset WISP_KV_* / WISP_BLOB_* | Unset WISP_SQL_* |
|---|---|---|
| Cloudflare | the binding called NAME in wrangler.toml | the D1 binding NAME |
| Deno Deploy | Deno KV (when there is no binding NAME) | 501 |
| Netlify | Netlify Blobs, store NAME | 501 |
| Vercel | none: set upstash (Vercel KV, Marketplace Redis) and vercel-blob | 501 |
| Node, Bun, Azure (Node build) | none: set upstash or s3: | 501 |
| AWS Lambda | fs under /tmp/wisp-data/storage, per instance: set upstash or s3: | 501 |
VPS, Docker, Fly, Railway, Render, Cloud Run, wisp dev | fs: $WISP_DATA/storage when WISP_DATA is set (memory when it is off), else .wisp/data | 501 |
A store with no driver answers a 500 on the first call. The 500 names the variable to set. It never panics.
Settings of Each Driver
upstash: the URL fromUPSTASH_REDIS_REST_URL(orKV_REST_API_URL, the name Vercel sets) unless the spec has one. The token comes fromWISP_KV_<NAME>_TOKEN,UPSTASH_REDIS_REST_TOKENorKV_REST_API_TOKEN. Each store keeps the keys that<length of NAME>:NAME:starts, so several stores share one database and no two stores' keys meet. A put is oneMULTI/EXEC(value, metadata and TTL as anEXAT). Metadata sits beside the key and no list shows it. A value that is not UTF-8 is kept as\0wisp-b64:and its base64, which other Redis clients read as such.s3:<endpoint>/<bucket>: path-style, signed with AWS Signature Version 4. The key comes fromWISP_BLOB_<NAME>_KEY_ID(orAWS_ACCESS_KEY_ID) andWISP_BLOB_<NAME>_SECRET(orAWS_SECRET_ACCESS_KEY). An optionalWISP_BLOB_<NAME>_SESSION_TOKEN(orAWS_SESSION_TOKEN) adds a session token. The region isWISP_BLOB_<NAME>_REGIONorAWS_REGION, else Wisp reads it from the endpoint:autofor R2, the region ofs3.<region>.amazonaws.com, elseus-east-1(MinIO). Examples:s3:https://s3.eu-west-2.amazonaws.com/photos,s3:https://<account>.r2.cloudflarestorage.com/photos,s3:http://localhost:9000/photos.vercel-blob: the token fromWISP_BLOB_<NAME>_TOKENorBLOB_READ_WRITE_TOKEN(it names the store). Keys are pathnames as given, and a put overwrites in place. A public store's CDN may serve an overwritten blob for up to 60 s. Reads of a public store go to its CDN without the token. Add:privatefor a private store, whose reads carry the token.VERCEL_BLOB_API_URLmoves the API.libsql://<host>: Turso or any libSQL server, over its HTTP pipeline. The token comes fromWISP_SQL_<NAME>_TOKEN, and Wisp sends it overhttpsonly. Wisp refuses a plainhttp://address with a token unless it islocalhost,127.0.0.1or[::1]. A BLOB column reads as its base64 text.d1-http:<account>/<database>: D1 from outside Cloudflare, over its REST API. The token (with D1 edit) comes fromWISP_SQL_<NAME>_TOKENorCLOUDFLARE_API_TOKEN. Wisp writestrueandfalsearguments as 1 and 0, as the binding and libSQL store them.deno-kv: the keys are[kind, NAME, key]in the databaseDeno.openKvopens (the same handle asWISP_STORE=deno-kv). Local Deno needs--unstable-kv. Deno Deploy has it.netlify-blobs: the site storeNAME(or the one the spec names), reached with theNETLIFY_BLOBS_CONTEXTthat Netlify gives each function. It needs no npm package.fs:<dir>: each key is one file under<dir>/<kv or blob>/<name>/. Wisp escapes the file name so no key leaves the folder or clashes on a case-blind disk. A put writes to a temporary file, flushes to disk and renames, so a reader never sees half a value. File calls run on blocking threads, never the request's worker.
What Each Driver Keeps
- Expiry (
put_for,KvPut { ttl, expires }) holds on every driver. Cloudflare needs at least 60 s. Deno KV and Netlify Blobs drop expired keys when read. - Lists come in key order.
upstashpages follow Redis'sSCAN. Each page is sorted, and a page may hold fewer keys than asked.upstash,netlify-blobs,s3:andvercel-bloblist names only, with no expiry or metadata. Netlify Blobs takes no page size. - An
s3:orvercel-blobkey that is empty or has a.or..segment (../x,a/./b) gets a 400. Many servers and proxies (MinIO, R2) resolve such a path to another object, and an empty one names the bucket's listing. A list's prefix may be empty. - A Deno KV value is at most 64 KiB (65,536 bytes, with its metadata and type): a larger put answers 413 and keeps nothing. Keep larger blobs in
s3:orvercel-blob.
Host Bindings Stay on Their Host
wisp::platform::call(binding, method, ...) reaches a Cloudflare binding (Workers AI, Queues, Durable Objects) and runs only there. Anywhere else it answers 501, because the binding exists nowhere else. Use the binding's HTTP API through wisp::fetch when the same code must run off Cloudflare.
Is This Page Useful?