Single-admin multi-project gateway: expose Cloudflare KV (Upstash-style Redis REST) and D1 over stable HTTP. Data plane lives under
/v1/{ref}/…; management under/admin/…. Dashboard SPA shares the same Worker origin.
Replace {origin} with your Worker URL (e.g. https://cfbridge.example.com) and {ref} with the project ref.
- Header:
Authorization: Bearer <project_api_key> - Roles:
anon(publishable) |service_role(secret) - New projects auto-mint one of each; plaintext is returned only on create
- If the project has
anon_readonlyenabled (default for new projects),anonkeys cannot write
- Header:
Authorization: Bearer <session_token>fromPOST /admin/auth/login(for curl / scripts) - Or HttpOnly session cookie set by login (Dashboard uses cookie only;
credentials: include)
GET /health— no auth; returns{ ok: true }only. Setup/migration details live under/admin/setup/status.
Admin (and most non-Redis JSON errors):
{ "error": { "code": "unauthorized", "message": "…" } }Redis (Upstash-compatible):
{ "result": … }{ "error": "ERR …" }D1 libSQL / Hrana: statement errors appear as results[].type === "error" with HTTP 200. Auth failures remain HTTP 4xx.
Upstash Redis REST–compatible subset. Storage backend is the project’s Cloudflare KV namespace.
| Method | Path | Description |
|---|---|---|
| GET/POST | /v1/{ref}/redis/{cmd}/…args |
Path-style command, e.g. /set/k/v, /get/k |
| POST | /v1/{ref}/redis |
Body: ["SET","k","v"] |
| POST | /v1/{ref}/redis/pipeline |
Body: [["SET","k","v"],["GET","k"]] |
PING, GET, SET (option EX only), SETEX, DEL, EXISTS, MGET, MSET, EXPIRE, PERSIST, TTL, KEYS (* or prefix* only), SCAN.
Hash / List / Set / ZSet, INCR*, SET NX|XX|PX, /multi-exec, Pub/Sub.
EX, SETEX, and EXPIRE require TTL ≥ 60 seconds (Cloudflare KV limit).
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: "{origin}/v1/{ref}/redis",
token: "cfb_sk_…",
});
await redis.set("user:100", "Lee");
console.log(await redis.get("user:100"));curl -s "{origin}/v1/{ref}/redis/set/hello/world" \
-H "Authorization: Bearer $API_KEY"
curl -s -X POST "{origin}/v1/{ref}/redis" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '["SET","hello","world","EX",3600]'Request/response shape matches the Cloudflare D1 Account API /query and /raw. The database is selected by {ref} in the URL (no account_id / database_id in the body). Auth: project API key.
| Method | Path | Description |
|---|---|---|
| POST | /v1/{ref}/d1/cf/query |
Rows as objects (CF /query) |
| POST | /v1/{ref}/d1/cf/raw |
Rows as arrays (CF /raw) |
Body (either form):
{ "sql": "SELECT * FROM notes WHERE id = ?", "params": [1] }{
"batch": [
{ "sql": "INSERT INTO notes (body) VALUES (?)", "params": ["a"] },
{ "sql": "SELECT * FROM notes" }
]
}Success shape:
{
"success": true,
"errors": [],
"messages": [],
"result": [
{ "results": [], "success": true, "meta": {} }
]
}curl -s -X POST "{origin}/v1/{ref}/d1/cf/query" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"sql":"SELECT * FROM notes"}'Compatible with @libsql/client (and Drizzle / Kysely on top). Point the client URL at the data plane.
| Method | Path | Description |
|---|---|---|
| GET | /v1/{ref}/d1/v2 · /v3 |
Protocol probe (no auth) |
| POST | /v1/{ref}/d1/v2/pipeline · /v3/pipeline |
Hrana pipeline (Bearer API key) |
Supported request types: execute, batch (with ok / error / not / and / or / is_autocommit), sequence, store_sql / close_sql, describe (stub), close, get_autocommit.
Not supported: interactive multi-request sessions (baton is always null). Put atomic work in one batch / pipeline, or use explicit BEGIN / COMMIT in SQL.
Underlying storage uses Cloudflare D1 /raw (array rows; duplicate column names are preserved).
import { createClient } from "@libsql/client/web";
const client = createClient({
url: "{origin}/v1/{ref}/d1",
authToken: "cfb_sk_…",
});
await client.execute(
"CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)",
);
await client.execute({
sql: "INSERT INTO notes (body) VALUES (?)",
args: ["hello from libsql"],
});
const rs = await client.execute("SELECT * FROM notes");
console.log(rs.rows);
await client.batch([
"INSERT INTO notes (body) VALUES ('a')",
"INSERT INTO notes (body) VALUES ('b')",
]);Bearer session token or cookie. Full operational setup is documented in the repository README, not here.
| Method | Path | Description |
|---|---|---|
| POST | /admin/auth/login |
{ password } → { token } |
| GET | /admin/auth/me |
Session check |
| GET/POST | /admin/projects |
List / create |
| GET/PATCH/DELETE | /admin/projects/:id |
Get / update / delete (?delete_cf=true deletes CF resources) |
| POST | /admin/projects/:id/resources/create |
Create KV or D1 via CF API |
| POST | /admin/projects/:id/resources/attach |
Attach existing CF id |
| GET/POST | /admin/projects/:id/keys |
List / mint API key (plaintext shown once) |
| POST | /admin/projects/:id/keys/:keyId/revoke |
Revoke key |
| POST | /admin/projects/:id/browser/redis |
Dashboard Redis argv proxy { argv } → { result } (service_role) |
| POST | /admin/projects/:id/browser/redis/keys |
Paginated key list { prefix?, cursor?, limit? } → KvListResponse |
| POST | /admin/projects/:id/browser/redis/inspect |
Key inspect { key } → value, metadata, TTL |
| POST | /admin/projects/:id/browser/d1/query |
Dashboard D1 SQL { sql, params? } → CF query envelope |
| POST | /admin/projects/:id/browser/d1/schema |
Tables/views + PRAGMA table_info → D1SchemaResponse |
Dashboard Redis/D1 browsers use the admin session only (not project API keys). TTL/EX/SETEX/EXPIRE still require ≥ 60 seconds (Cloudflare KV).
| URL | Content |
|---|---|
/docs/api.md |
This document (text/markdown) |
/llms.txt |
Curated index (llms.txt format) |
/llms-full.txt |
Full API reference (same body as /docs/api.md) |
/docs |
Human-readable page (copy Markdown button) |