A lightweight “JSON KV” service deployed on Cloudflare Workers:
- Uses
namespaceas the top-level KV key, storing the entire JSON document in Cloudflare KV - Accesses/writes JSON subpaths via the URL path (
a/b/c=> JSONPatha.b.c) - Supports read-only / read-write authorization (including an admin master key)
- Node.js (LTS recommended)
pnpm- A Cloudflare account with Workers enabled
- Install and log in to
wrangler
pnpm installThis project binds a KV namespace via kv_namespaces in wrangler.toml:
kv_namespaces = [
{ binding = "JSONBIN", id = "...", preview_id = "..." }
]If you deploy under your own account, create your own KV namespaces and write the id/preview_id back into wrangler.toml.
Example commands (follow the actual output from wrangler):
wrangler kv namespace create JSONBIN
wrangler kv namespace create JSONBIN --previewFill the output IDs into wrangler.toml:
id: production KV idpreview_id: preview KV id used bywrangler dev
The service uses the following configuration:
APIKEY(required, recommended): admin master key, used to mint tokens and for admin accessDEFAULT_AUTHORIZATION(optional): default authorization (applies when the requester is not admin)
Set APIKEY as a secret (so it won’t be committed into config files):
wrangler secret put APIKEYDEFAULT_AUTHORIZATION can be placed under [vars] in wrangler.toml (an example is already provided).
The format of DEFAULT_AUTHORIZATION is a “querystring-like” key-value list:
- key: an authorization entry (see “Authorization model”)
- value:
rorrw(empty string is treated asr)
For example:
[vars]
DEFAULT_AUTHORIZATION = "public::*=r&demo=rw"pnpm devBy default this runs wrangler dev src/index.ts.
pnpm deployEquivalent to: wrangler deploy --minify src/index.ts.
Each request tries to read userToken from the following locations (priority high to low):
- URL query:
?key=... - Header:
Authorization: Bearer ... - Cookie:
key=...
See implementation in src/auth.ts#getUserToken().
The requester is treated as admin if either condition is met:
- Environment variable
APIKEYis empty (not recommended) userToken === APIKEY
Admins have read-write access to all paths, and can call admin APIs: /_/token, /_/list.
When the requester is not admin, the service reads DEFAULT_AUTHORIZATION as the “base permission”. If not configured, the default is no permission.
Examples:
demo=rw: read-write for the wholedemonamespacedemo::a.b.c=rw: only allow accessing JSON subpatha.b.cunderdemo(and its children)
Note: the code uses URLSearchParams to parse, so DEFAULT_AUTHORIZATION is essentially a string like k=v&k2=v2.
You can call /_/token with admin privilege to mint a JWT token with embedded permissions, then use it as userToken.
The service uses APIKEY as the JWT signing/verification key (see src/auth.ts#jwtSign / jwtVerify).
The a field in the JWT payload is the authorization table (Record<string,'r'|'rw'>). The service merges it with DEFAULT_AUTHORIZATION:
- For the same entry, if either side is
rw, the merged result isrw - Tokens that fail verification or don’t match the expected format are treated as invalid (401)
Authorization checks happen on namespace + '/' + jsonpath, where:
namespace: the first path segment (e.g.demofor/demo/a/b)jsonpath: the remaining path segments joined by.(e.g. jsonpath isa.bfor/demo/a/b)
Rules for entry keys:
- If the key does not contain
/: it’s treated as a namespace (e.g.demo)- It will be expanded to
demo/anddemo/*
- It will be expanded to
- If the key contains
/: it’s treated asnamespace/jsonpath(e.g.demo/a.b)- If it does not end with
*, children are also allowed (by appending.*)
- If it does not end with
Matching syntax uses micromatch (glob-style), so you can use wildcards like *.
Routes are defined in src/index.ts:
- Public:
GET / - Admin:
GET /_/signin,GET /_/token,GET /_/list,GET /_/list/:prefix - KV data:
GET|POST|PATCH|DELETE /*(note: currentlyDELETEeffectively returns 405; see below)
All responses default to Content-Type: application/json; charset=utf-8, and CORS is enabled (via hono/cors in src/index.ts).
Health check + returns the authorization info parsed for the current request:
{ "ok": true, "authorization": "admin" }When not admin, authorization may be null or an authorization table.
Writes the current request’s userToken into cookie key, then 302 redirects to /.
- cookie:
httpOnly,secure,sameSite=Strict,path=/,maxAge=180 days
Useful to “persist” ?key=... into a browser cookie.
Mints a JWT token (signed with APIKEY) to carry fine-grained permissions.
Query parameters:
ttl: validity duration in seconds- omitted: defaults to 15 minutes
ttl>=0:exp = iat + ttlttl<0: do not setexp(no expiry)
svc: optional service name (written into payload as-is)r: repeatable parameter, adds read-only entries (e.g.&r=demo&r=demo/a.b)rw: repeatable parameter, adds read-write entriesraw=1: returns the raw token string (text/plain) instead of JSON
Response (JSON by default):
{
"id": "...",
"service": null,
"createdAt": "2026-01-01T00:00:00.000Z",
"expiresAt": "2026-01-01T00:15:00.000Z",
"authorization": { "demo": "rw" },
"token": "eyJ..."
}If you provide no r/rw, the service will insert an example entry (test=r) to avoid minting an empty authorization table.
Lists keys in KV (i.e. all namespaces).
GET /_/list: no prefix filterGET /_/list/:prefix: filter by prefix- If
:prefixends with*, the trailing*will be removed before using it as the prefix
- If
Query parameters:
limit: default 50;<=0means no limitcursor: pagination cursor
Response:
{
"keys": ["demo", "public"],
"next": "..."
}next being null means there are no more results.
Core idea:
- The first path segment is the
namespace - Each subsequent path segment is one JSON key (internally joined by
.intojsonpathfor authorization checks)
For example:
/demo=> namespacedemo, jsonpath[]/demo/a/b=> namespacedemo, jsonpath["a","b"]
Reads data (requires r permission).
If the value read is a string, the service returns the raw string with text/plain; charset=utf-8; otherwise it returns JSON.
Supported query parameters (see src/kv_query.ts):
q: JSONPath query (via thejsonpathlibrary)first=1: only take the first element of theqresult (requires the result to be an array)shape: “project/reshape” the output; value is a JSON stringcount=1: count arrays/objects (array length / object key count)
Examples:
# Read the whole JSON under demo
curl -H 'Authorization: Bearer <token>' https://<your-domain>/demo
# Read demo.a.b
curl -H 'Authorization: Bearer <token>' https://<your-domain>/demo/a/b
# JSONPath query + count
curl -H 'Authorization: Bearer <token>' 'https://<your-domain>/demo?q=$.items[*]&count=1'Writes/overwrites the value at a subpath (requires rw permission).
Content-Type: application/json: request body is parsed as JSONContent-Type: text/plain: request body is read as plain text and stored as a string
Example:
curl -X POST \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{"a":1}' \
https://<your-domain>/demoApplies JSON Merge Patch for objects, or appends for arrays (requires rw permission).
Query parameters:
append=1: target value must be an array; the request body will be pushed as one elementmax=N: only effective withappend=1; when the array exceedsN, it is truncated to keep the lastNelementscreate=1: create the target path if it does not exist- with
append=1, create an empty array[] - otherwise create an empty object
{}
- with
Examples:
# Merge patch an object
curl -X PATCH \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{"enabled":true}' \
https://<your-domain>/demo/config?create=1
# Append to an array
curl -X PATCH \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{"ts": 123}' \
'https://<your-domain>/demo/events?append=1&create=1&max=100'Although the router registers DELETE (see src/index.ts), src/kv.ts currently does not implement a DELETE branch, so it returns 405 Method Not Allowed.
If you need to “delete a subpath” today, the available workaround is:
- Use
POST/PATCHto overwrite that path withnull(semantically treated as delete)
Non-2xx responses return JSON (see src/error.ts#errorToResponse):
{
"ok": false,
"error": "unauthorized",
"message": "Authorization Key is Missing"
}Common status codes:
401: missing token or invalid token403: token provided but permission denied404: path not found405: method not allowed