ZBSearch

Setup

Deploy ZBSearch Edge to Cloudflare Workers and R2 - step by step from zero to a live search API. Cloudflare deploy is currently in beta.

Beta

Cloudflare deploy is in beta. APIs, defaults, and CLI behavior may change. Pin package versions in production and review release notes before upgrading.

This guide deploys ZBSearch Edge to your Cloudflare account. You do not need to clone the repository - install the published npm packages and run the setup CLI.

Quick start (npm)

Install the Worker runtime and Wrangler, then run the setup wizard:

mkdir my-search-api && cd my-search-api
npm init -y
npm install @zbsearch/runtime-cloudflare wrangler
npx wrangler login
npx zbsearch-edge-setup --init

The wizard asks for Worker name, R2 bucket names, account ID, optional R2 API credentials, optional read/write API keys, and rebuild settings. It saves zbsearch.edge.config.json in your project directory (gitignore this file), generates wrangler.toml and .env, then provisions and deploys.

Config file instead of wizard

Download the example config (JSON) and edit it:

curl -o zbsearch.edge.config.json https://zbsearch.dev/downloads/zbsearch.edge.config.example.json
# edit zbsearch.edge.config.json - set r2.accountId and R2 API credentials
npx zbsearch-edge-setup

YAML is also supported (example):

curl -o zbsearch.edge.config.yaml https://zbsearch.dev/downloads/zbsearch.edge.config.example.yaml
npx zbsearch-edge-setup --config zbsearch.edge.config.yaml
FlagPurpose
--dry-runPreview actions without changes
--write-onlyOnly write wrangler.toml and .env
--skip-deployProvision buckets/secrets, skip deploy
--yesSkip confirmation prompt

zbsearch.edge.config.json may contain secrets. Add it to .gitignore and never commit real credentials.

npm packages

PackageRole
@zbsearch/runtime-cloudflareWorker runtime + zbsearch-edge-setup / zbsearch-edge-teardown CLIs
@zbsearch/edge-index-builderRebuild CLI (zbsearch-edge-builder) for production indexes
@zbsearch/edge-coreInstalled automatically as a dependency

Monorepo development

If you are developing ZBSearch itself, clone the repository and run the setup CLI directly (no root package.json scripts required):

git clone https://github.com/micheleriva/zbsearch.git
cd zbsearch
pnpm install
wrangler login
node packages/runtime-cloudflare/deploy/setup.mjs --deploy-dir deploy/cloudflare --init

Config and generated files live under deploy/cloudflare/. Templates are at packages/runtime-cloudflare/deploy/templates/.


Manual setup

Follow the steps below if you prefer to configure each piece yourself.

Prerequisites

Log in to Cloudflare from Wrangler:

npx wrangler login

1. Install the runtime

mkdir my-search-api && cd my-search-api
npm init -y
npm install @zbsearch/runtime-cloudflare @zbsearch/edge-index-builder wrangler

Create a minimal wrangler.toml in your project directory:

name = "zbsearch-edge"
main = "node_modules/@zbsearch/runtime-cloudflare/src/worker.ts"
compatibility_date = "2025-03-01"

No build step is needed - Wrangler bundles the Worker straight from the npm package.

2. Create an R2 bucket

The default configuration expects a bucket named zbsearch-edge. Create it once:

npx wrangler r2 bucket create zbsearch-edge

For local/preview dev, Wrangler also uses a preview bucket. You can create it optionally:

npx wrangler r2 bucket create zbsearch-edge-preview

Add the bucket binding to wrangler.toml:

[[r2_buckets]]
binding = "BUCKET"
bucket_name = "zbsearch-edge"
preview_bucket_name = "zbsearch-edge-preview"

The default configuration (and the setup-CLI template) also includes the Durable Object bindings (write coordinator, plus the per-shard search node used for distributed group searches) and an extended CPU limit:

[[durable_objects.bindings]]
name = "INDEX_COORDINATOR"
class_name = "IndexCoordinator"

[[durable_objects.bindings]]
name = "SEARCH_SHARD"
class_name = "ShardSearch"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["IndexCoordinator"]

[[migrations]]
tag = "v2"
new_sqlite_classes = ["ShardSearch"]

[limits]
cpu_ms = 300000

ZBSearch Edge requires the Workers Paid plan. The Free plan caps CPU at 10ms per invocation, which search workloads exceed - see Production. The SQLite-backed Durable Object class above works on both plans.

The Worker handles search and WAL writes. Rebuilds that merge the WAL into a new snapshot can run inside the Worker (small indexes) or via an external CLI (production). The same CLI also supports bulk import - building a snapshot locally and uploading it to R2 without per-document API calls.

Create a .env file in your project directory with your R2 credentials from the Cloudflare dashboard:

  1. Go to R2 -> Overview -> Manage R2 API Tokens
  2. Create a token with Object Read & Write on your bucket
  3. Copy the Access Key ID, Secret Access Key, and your Account ID
# .env (gitignore this)
R2_BUCKET=zbsearch-edge
R2_ACCESS_KEY_ID=your_access_key_id
R2_SECRET_ACCESS_KEY=your_secret_access_key
R2_ENDPOINT=https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com

The Worker binds to R2 natively via Wrangler - it does not need these S3 credentials. They are only for the builder CLI and CI runners.

Rebuild or import against R2 with the builder CLI (installed from npm in step 1):

source .env
npx zbsearch-edge-builder rebuild --all
npx zbsearch-edge-builder import products ./catalog.json \
  --create --name products --schema-file ./schema.json

See Indexing & rebuilds for import file formats ({ "data": [...] }, JSON array, or JSONL).

4. Optional: protect the API with keys

By default, the API is open. For anything beyond local testing, set two keys — a write key with full access, and a read-only key for search traffic (safe to embed in public clients):

npx wrangler secret put WRITE_API_KEY
npx wrangler secret put READ_API_KEY

When prompted, enter a strong random string for each. Authenticated requests must send:

Authorization: Bearer YOUR_API_KEY

The write key can do everything (index management, document writes, rebuilds, searches). The read key only covers searches and index/metadata reads — it gets 401 on any write route. If you only set READ_API_KEY, writes are closed entirely. /health and /v1/info remain public so load balancers and uptime checks can probe the Worker without credentials.

The legacy single API_KEY secret still works as a full-access key, but prefer the split above.

5. Deploy the Worker

From your project directory:

npx wrangler deploy

Wrangler prints your Worker URL, for example:

https://zbsearch-edge.your-subdomain.workers.dev

Save this as WORKER_URL for the examples below.

6. Verify the deployment

Check health:

curl "$WORKER_URL/health"
# {"ok":true}

Create an index:

curl -X POST "$WORKER_URL/v1/indexes" \
  -H 'content-type: application/json' \
  -H "authorization: Bearer YOUR_API_KEY" \
  -d '{
    "name": "products",
    "schema": {
      "title": "string",
      "description": "string",
      "price": "number"
    }
  }'

The index ID is derived from the name (products from "products", my-products from "My Products").

Insert a document (appended to WAL - searchable after rebuild, or sooner via WAL overlay):

curl -X PUT "$WORKER_URL/v1/indexes/products/documents/sku-1" \
  -H 'content-type: application/json' \
  -H "authorization: Bearer YOUR_API_KEY" \
  -d '{"title":"Wireless Headphones","description":"Noise cancelling","price":99}'

Trigger a rebuild:

curl -X POST "$WORKER_URL/v1/indexes/products/rebuild" \
  -H "authorization: Bearer YOUR_API_KEY"

Search:

curl -X POST "$WORKER_URL/v1/indexes/products/search" \
  -H 'content-type: application/json' \
  -H "authorization: Bearer YOUR_API_KEY" \
  -d '{"term":"wireless headphones"}'

7. Configure rebuild behavior

Open wrangler.toml. Key settings:

[triggers]
crons = ["*/5 * * * *"]

[vars]
REBUILD_THRESHOLD_OPS = "500"

[limits]
cpu_ms = 300000
SettingDefaultMeaning
cronsEvery 5 minutesWorker checks indexes for pending writes
REBUILD_THRESHOLD_OPS500Auto-rebuild in-Worker when pending ops reach this count
cpu_ms300000 (5 min)Per-request CPU limit (Workers Paid plan); headroom for in-Worker rebuilds
SNAPSHOT_CACHE_MAX_ENTRIES4Deserialized snapshots cached per isolate
SNAPSHOT_CACHE_MAX_BYTES67108864Byte budget for the per-isolate snapshot cache

For development and small indexes, the cron + threshold is enough. For production, use an external rebuild runner and set BUILDER_WEBHOOK_URL so the Worker triggers GitHub Actions instead of rebuilding inline.

npx wrangler secret put BUILDER_WEBHOOK_URL

Custom domains

To serve the API on your own domain, add a route in the Cloudflare dashboard or extend wrangler.toml:

routes = [
  { pattern = "search.example.com/*", zone_name = "example.com" }
]

Then redeploy with npx wrangler deploy.

Teardown (unpublish / reset)

npx zbsearch-edge-teardown --dry-run
npx zbsearch-edge-teardown --yes
npx zbsearch-edge-teardown --keep-buckets --yes

Monorepo equivalent: node packages/runtime-cloudflare/deploy/teardown.mjs --deploy-dir deploy/cloudflare --yes

The script reads zbsearch.edge.config.json when present, otherwise wrangler.toml and .env. To deploy again: npx zbsearch-edge-setup.

Troubleshooting

ProblemLikely causeFix
404 on search after insertNo rebuild yetCall POST /v1/indexes/:id/rebuild or wait for cron
401 UnauthorizedMissing or wrong Authorization headerSet WRITE_API_KEY/READ_API_KEY secrets and send a Bearer token
R2 bucket not found on deployBucket name mismatchMatch bucket_name in wrangler.toml to an existing R2 bucket
Rebuild CLI failsMissing S3 credentialsFill .env with R2 API token values
Searches fail with error 1102Free plan 10ms CPU capUpgrade to Workers Paid
Worker CPU timeout on rebuildIndex too large for in-Worker rebuildUse external builder via BUILDER_WEBHOOK_URL, or bulk import

What's next

On this page