An Instance of keepjson is one Cloudflare Worker plus one D1 database in your own Cloudflare account. This guide sets one up from a clone, then covers configuration, local development, updates and the demo environment.
- A Cloudflare account. The Worker, D1, Cron Triggers and the Workers Rate Limiting bindings all run on it.
- Node.js 22 and pnpm.
- Wrangler, Cloudflare's CLI, is a dev dependency, so you run it as
pnpm exec wranglerand don't need a global install.
git clone https://github.com/avinoth/keepjson.git
cd keepjson
pnpm installpnpm exec wrangler login
pnpm exec wrangler d1 create keepjsonThe command prints the new database's database_id. Put it into wrangler.jsonc, replacing the all-zero placeholder in the d1_databases entry:
Keep binding as DB and migrations_dir as migrations: the code and the commands below rely on them.
The migrations in migrations/ create the bins table. Apply them to the remote database before the first deploy:
pnpm exec wrangler d1 migrations apply keepjson --remoteWrangler lists the migrations it is about to apply and asks you to confirm. It applies only the ones the database hasn't had yet, so the same command is safe to rerun. The argument is the database name or its binding, so pnpm exec wrangler d1 migrations apply DB --remote works too.
For local development, apply them to the local database as well (see Local development):
pnpm db:migrate:localAn Instance is configured through the vars in wrangler.jsonc. Every value is a string. An empty or missing var takes its default.
| Var | Default | Meaning |
|---|---|---|
MAX_BIN_BYTES |
102400 (100 KiB) |
The largest body, in bytes, that POST and PUT accept. A larger one gets 413 payload_too_large. A positive integer, at most 1000000. |
DEFAULT_TTL_SECONDS |
0 (none) |
The TTL of a Bin created without X-Bin-TTL. 0 means no default. |
MAX_TTL_SECONDS |
0 (none) |
The largest TTL a Bin can have. 0 means no maximum. |
CORS_ORIGINS |
* |
The browser origins allowed to call the Instance: *, or a comma-separated allowlist. |
RATE_LIMIT_ENABLED |
true |
false turns off rate limiting for POST, PUT and DELETE. Only true or false. |
Why MAX_BIN_BYTES stops at 1,000,000. Each Bin's content is stored inline in its D1 row, and D1 caps a row at about 2 MB. The ceiling leaves headroom for the rest of the row. Bins larger than that need a different storage layer.
Expiry. DEFAULT_TTL_SECONDS and MAX_TTL_SECONDS work together:
- With
X-Bin-TTLon create, the Bin expires that many seconds later. A TTL aboveMAX_TTL_SECONDS(when it isn't0) is rejected with400 invalid_ttl. - Without the header, the Bin gets
DEFAULT_TTL_SECONDS; if that is0, it getsMAX_TTL_SECONDS; if that is0too, it never expires. - So setting only
MAX_TTL_SECONDSputs an upper bound on every Bin, which is the way to keep a public Instance's database from growing without end. KeepDEFAULT_TTL_SECONDSat or belowMAX_TTL_SECONDS. - The Expiry is fixed when a Bin is created: changing these vars later doesn't change existing Bins.
CORS_ORIGINS. * lets any web page call the Instance from a browser. An allowlist is a comma-separated list of exact origins (scheme, host and port if not the default, no path and no trailing slash), for example https://app.example.com,https://admin.example.com; whitespace around each entry is ignored, and wildcards such as https://*.example.com aren't supported. A request whose Origin doesn't match an entry gets no Access-Control-Allow-Origin, so the browser blocks it. CORS only restricts browsers: it doesn't stop curl or a server from calling the Instance, and an empty value means *.
RATE_LIMIT_ENABLED. Leave it true on any Instance reachable from the internet. false is meant for private Instances, such as one behind Cloudflare Access.
A var that doesn't parse (for example MAX_BIN_BYTES above 1000000, or RATE_LIMIT_ENABLED=yes) makes the Instance answer every request with 500 internal_error. Check the values after editing them.
If you change vars or bindings, run pnpm typegen to regenerate worker-configuration.d.ts.
POST /bins is limited by the CREATE_LIMITER binding and PUT/DELETE by WRITE_LIMITER, per client IP. Reads aren't limited. They are set in wrangler.jsonc:
"ratelimits": [
{ "name": "CREATE_LIMITER", "namespace_id": "1001", "simple": { "limit": 10, "period": 60 } },
{ "name": "WRITE_LIMITER", "namespace_id": "1002", "simple": { "limit": 60, "period": 60 } }
],limitis the number of requests a single IP may make perperiod. Change it freely.namespace_idmust be unique per rate limiter within your Cloudflare account. If the account already has rate limiters with1001or1002, pick other numbers, or the Workers would share their counters.periodcan only be10or60seconds. A binding can't report when its window resets, so a429sends the whole period asRetry-After. That number comes fromRATE_LIMIT_PERIOD_SECONDSinsrc/bins/rate-limit.ts, which is shared by both bindings. If you changeperiod, change it on both bindings and setRATE_LIMIT_PERIOD_SECONDSto the same value.
Expired Bins answer 404 as soon as their Expiry passes. Their rows are deleted by a Cron Trigger that runs every hour:
"triggers": { "crons": ["0 * * * *"] },wrangler deploy registers it; there is nothing else to set up. Each run deletes expired Bins in batches of 500 for up to about 20 seconds, and whatever is left goes to the next run. You can see the runs, and how many Bins each deleted, in the Worker's logs in the Cloudflare dashboard.
pnpm run deployUse pnpm run deploy, not pnpm deploy: pnpm deploy is a built-in pnpm command and doesn't run the script. The script runs wrangler deploy, which uploads the Worker as keepjson and prints its URL (https://keepjson.<your-subdomain>.workers.dev). To use your own domain, add a route or custom domain in the Cloudflare dashboard or in wrangler.jsonc.
Check the Instance:
curl https://keepjson.<your-subdomain>.workers.dev/health
# → {"status":"ok"}The API reference is served at /docs and the OpenAPI spec at /openapi.json. The README has curl examples for every route.
The button in the README copies the repo to your GitHub account, creates the Worker and its resources, and deploys it. It doesn't run the D1 migrations, so the bins table doesn't exist yet and every Bin request fails with 500 internal_error. Clone the new repo, then run step 3 against it:
pnpm install
pnpm exec wrangler login
pnpm exec wrangler d1 migrations apply DB --remoteUsing the binding DB here works whatever name the database was given during the deploy.
pnpm install
pnpm db:migrate:local # creates the bins table in the local D1 database, under .wrangler/
pnpm dev # serves the Instance on http://localhost:8787pnpm dev uses wrangler.jsonc with a local D1 database and simulated rate limiters; it doesn't touch your Cloudflare account. To override vars locally without editing wrangler.jsonc, put them in a .dev.vars file (it is gitignored):
RATE_LIMIT_ENABLED=falsepnpm check runs the typecheck, the linter and the tests. The tests run in workerd against their own local D1 database.
git pull
pnpm install
pnpm exec wrangler d1 migrations apply keepjson --remote
pnpm run deployApply the migrations before deploying, so the new code never runs against an old schema. When there are no new migrations, Wrangler says so and changes nothing. Your edits to wrangler.jsonc (the database_id and your vars) may conflict with upstream changes on git pull; keep your values when you resolve them.
If you deployed with the button, your copy of the repo deploys itself on every push. Pull the upstream changes into it, apply any new migrations with the command above, then push.
wrangler.demo.jsonc is a second, standalone Wrangler config for a demo Instance, with the demo values from the table in step 4: every Bin expires within 30 days (DEFAULT_TTL_SECONDS and MAX_TTL_SECONDS are 2592000), so a free-tier D1 database stays small. It deploys the same code as a separate Worker, keepjson-demo, with its own D1 database (keepjson-demo) and its own rate limiter namespace_ids (2001, 2002).
Wrangler reads wrangler.jsonc by default, so the demo only happens when you pass -c wrangler.demo.jsonc (--config). To deploy it:
pnpm exec wrangler d1 create keepjson-demo
# put the printed database_id into wrangler.demo.jsonc
pnpm exec wrangler d1 migrations apply keepjson-demo --remote -c wrangler.demo.jsonc
pnpm exec wrangler deploy -c wrangler.demo.jsoncEvery Wrangler command for the demo needs -c wrangler.demo.jsonc; without it, the command acts on the main Instance.