npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bql.sh

v0.4.0

Published

SQLite as a multi-tenant database server for Bun — WAL shipping, hook-driven realtime, an FFI driver — and the durable bus that runs work against it.

Readme

bql.sh

One package: SQLite as a multi-tenant database server for Bun, and the durable bus that runs work against it.

bun add bql.sh

| Import | What it is | | --- | --- | | bql.sh | The embedded database — Bql.open(), a whole server in your own process | | bql.sh/client | The client: sql, live, transactions, client.admin | | bql.sh/context | Bun/Node resolver for saved organizations, projects, and endpoints | | bql.sh/sqlite | The bun:ffi driver over a pinned libsqlite3: hooks, the authorizer, killable statements, changesets | | bql.sh/server | Serve it: WAL shipping, replicas, S3 backup, a Raft control plane | | bql.sh/kysely, bql.sh/drizzle | A Kysely dialect and a Drizzle driver over any of the above | | bql.sh/search | Vector (sqlite-vec), hybrid, full-text and geo search — SQL builders over any Db | | bql.sh/tenant, /wal, /realtime, /core, /http, /openapi, /dataapi, /graphql, /protocol | The layers underneath, each reachable on its own | | bql.sh/bus | A durable message bus — subjects, consumer groups, leases, retries, a dead-letter path, three tiers of exactly-once | | bql.sh/bus/client | Its client and consumer |

Two binaries: bql for the database, bql bus for the bus.

Run the CLI from this checkout, or link it globally so bql works from any directory:

bun run cli --help
bun run cli db list
bun run cli:link           # once; global bql follows this checkout's source
bql --help

Bun's global bin directory (normally ~/.bun/bin) must be on your PATH. Run bun unlink from this checkout to remove the global link. Both entry points use BQL_URL for the server URL and BQL_ADMIN_KEY for its admin key (or BQL_TOKEN for a scoped token; it takes precedence over BQL_ADMIN_KEY).

Saved endpoints

Organize independently deployed servers into local organizations, projects, and named endpoints. These are connection settings on your machine; database and bus tokens still control access on each server.

bql org add personal
bql switch personal
bql project add myapp
bql project use myapp

# Enter the deployment's BQL_AUTH_ADMIN_KEY at the hidden prompt.
bql endpoint add preview --url https://your-preview.vercel.app --prompt-token
bql endpoint add production --url https://your-production.example --prompt-token
bql link --org personal --project myapp --endpoint preview

bql context                       # show target without revealing credentials
bql db list
bql db create myapp
bql exec myapp --sql 'SELECT 1'
bql db list --endpoint production # override for this command
bql endpoint use production       # change this checkout's endpoint
bql endpoint use preview --default # default for this project outside linked checkouts

bql link offers terminal selection when required context is missing. In scripts, supply the organization, project, and endpoint explicitly. Links live in .bql/project.json, work from subdirectories, and contain IDs rather than secrets. Linking adds .bql/ to .gitignore. bql unlink removes the nearest project link. bql switch and bql project use change global defaults without changing existing links.

All three record types support list, inspect, update, and remove, with --json output. Rename with bql project update myapp --name new-name; existing links keep working. Removing a nonempty organization or project requires --recursive and only removes local connection settings, never remote databases. Deleted selections produce an error until you select or link another record.

For protected Vercel deployments, save that endpoint's Protection Bypass for Automation secret separately from its BQL token:

bql endpoint update preview --prompt-vercel-bypass
bql db list --endpoint preview

The CLI sends x-vercel-protection-bypass on HTTP requests and socket handshakes. Cloud runtimes still support bql exec rather than interactive bql shell sessions. CLI requests reject HTTP redirects, and named database connections reject cross-origin primary-movement retries; select the destination endpoint explicitly instead.

For CI, reference environment variables instead of saving secret values:

bql endpoint add ci --org personal --project myapp \
  --url https://your-preview.vercel.app \
  --token-env CI_BQL_TOKEN --vercel-bypass-env CI_VERCEL_BYPASS

# Set these secrets through your CI environment, then select the context:
BQL_ORG=personal BQL_PROJECT=myapp BQL_ENDPOINT=ci bql db list --json

The registry defaults to $XDG_CONFIG_HOME/bql or ~/.config/bql. Override it with --config-dir or BQL_CONFIG_DIR to provision isolated CI configuration. Saved secrets live in an owner-only credentials.json, separately from the registry. Use --clear-token or --clear-vercel-bypass to remove credentials. Changing a service URL clears its old credential references unless you supply new ones in the same command.

Selection order is explicit --url; explicit --org/--project/--endpoint; matching BQL_ORG/BQL_PROJECT/BQL_ENDPOINT variables; legacy BQL_URL (bus: BUS_URL); checkout link; global selection; then localhost. --url cannot be combined with explicit selection flags. Within named selection, flags override environment values, then linked IDs, then global defaults; changing a parent discards incompatible child selections. --cwd changes link lookup without changing the shell directory.

Named endpoints use their own credentials and ignore ambient BQL_TOKEN, BQL_ADMIN_KEY, and BUS_TOKEN. Explicit --token and --vercel-bypass override credentials for one command. Direct URL mode keeps the old environment-variable behavior and never inherits saved credentials; its bypass variables are BQL_VERCEL_BYPASS and BUS_VERCEL_BYPASS.

Add an optional bus connection to the same endpoint with separate credentials:

bql endpoint update preview --bus-url https://your-bus.example --prompt-bus-token
# Add --prompt-bus-vercel-bypass if that service is also protected.
bql bus stats --endpoint preview
bql bus publish work.hello '{"hello":"world"}' --endpoint preview
bql bus follow --endpoint preview --data ./replica

Missing bus settings fail explicitly; database credentials are never reused for the bus. Use --clear-bus to remove that connection. Local serve, key management, and storage initialization do not require a saved remote context.

Bun/Node scripts can resolve the same connection; browser SDKs remain explicitly configured:

import { resolveContext } from "bql.sh/context"
import { createClient } from "bql.sh/client"

const connection = await resolveContext({ org: "personal", project: "myapp", endpoint: "preview" })
const client = createClient(connection)
try {
  console.log(await client.db("myapp").sql`SELECT 1`)
} finally {
  client.close()
}

resolveContext returns credentials for client construction: don't log its result. Use the exported redactContext for diagnostics. SDK transport behavior remains the SDK's own; the resolver does not install CLI redirect or WebSocket policies.

The database and the bus ship together because they were solving the same problems apart: storage, replication, tenancy and a change feed. docs/monorepo.md is why, what moved, and what comes next.

Bun 1.4 or newer. Zero runtime dependencies.

bun install

bun run typecheck          # the repository's scripts, then both halves
bun run test               # both halves
bun run bytes              # no raw control bytes in any tracked file

bun run db sqlite:build    # build the pinned libsqlite3 bql.sh needs
bun run db test
bun run bus test
bun run bus dev

The repository keeps two source trees — packages/db and packages/bus — behind that one package, so bun run db <script> and bun run bus <script> forward to the tree, and every script either half already had still runs by its own name.

The database

Thousands of small SQLite databases in one process, their WAL frames streamed to replicas over a socket, backed up continuously to any S3-compatible bucket, with realtime subscriptions driven by SQLite's own preupdate/update hooks rather than triggers or SQL parsing. It speaks its own API and libsql's Hrana, so @libsql/client, Drizzle and Kysely reach it unmodified. The engine is a bun:ffi driver over a pinned libsqlite3 that exposes what bun:sqlite does not: hooks, the authorizer, query cancellation, per-connection limits, session changesets.

bun run db sqlite:build                      # once per machine; needs a C compiler
bun run db start                             # prints an admin key the first time, once
import { createClient } from "bql.sh/client"

const db = createClient({ url: "http://localhost:4321", token }).db("acme")

await db.sql`insert into todos(title) values (${"write it"})`.run()
const live = db.live`select * from todos where done = 0`.key("id")
live.on("diff", (e) => patch(e.added, e.removed, e.updated))

Phases 0, 1 and 2 are built, and so is the surfaces track: replica streaming and promotion, a Raft control plane holding per-database leases off the write path, S3 shipping and point-in-time restore, and one operation model rendered as REST, OpenAPI 3.1 and GraphQL. The control plane is on the client too, under client.admin. packages/db/README.md is the real documentation; packages/db/docs/api.md is the API as built.

Search

Vector, hybrid, full-text and geo search are SQL builders over any Db — client, embedded or a transaction — so they work the same against a server, a replica and your own process. sqlite-vec and the geo functions are compiled into the library bun run db sqlite:build makes; on any other library the helpers throw FEATURE_UNAVAILABLE rather than fall back.

import { ftsIndex, ftsQuote, geoIndex, hybridSearch, vectorIndex } from "bql.sh/search"

const fts = ftsIndex(db, { table: "posts_fts", source: "posts", columns: ["title", "body"] })
const vec = vectorIndex(db, { table: "posts_vec", dimensions: 384 })
await fts.create() // triggers keep it in step with `posts`
await vec.create()
await vec.upsert(postId, embedding)

await hybridSearch(db, { fts, vector: vec, query: ftsQuote(userInput), embedding: queryEmbedding, k: 10 })
await geoIndex(db, { table: "shops_geo", source: "shops" }).near(51.5, -0.12, 2_000)

A database per pull request

Forks are O(1) and record their parent, so a branch per pull request costs a directory. The bql-branch action creates one when a pull request opens and deletes it when it closes — curl against the HTTP API, no install. Branch names default to pr-<number>. delete only removes a database that has a parent, and never the source itself.

# .github/workflows/db-branch.yml
on:
  pull_request:
    types: [opened, reopened, synchronize, closed]
jobs:
  branch:
    runs-on: ubuntu-latest
    steps:
      - id: db
        uses: TimMikeladze/bql/.github/actions/bql-branch@main
        with:
          url: ${{ secrets.BQL_URL }}
          token: ${{ secrets.BQL_ADMIN_KEY }}
          source: main
          action: ${{ github.event.action == 'closed' && 'delete' || 'create' }}
          on-exists: keep            # or `reset`: back to main's head on every push
      - run: echo "database ${{ steps.db.outputs.name }}"

Vercel previews. A preview build knows its git branch as VERCEL_GIT_COMMIT_REF. Turn it into a database name — [a-z0-9][a-z0-9-_]{0,63}, so lower-case it, dash everything else, trim, cut to 64 — fork it from main before the build, and read the same name at runtime:

// db-name.ts — imported by the build step and by the app
export function dbName(): string {
  const ref = process.env.VERCEL_ENV === "preview" ? process.env.VERCEL_GIT_COMMIT_REF : undefined
  if (!ref) return "main"
  const name = ref.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/-+/g, "-")
    .replace(/^[-_]+/, "").slice(0, 64).replace(/-+$/, "")
  return name || "main"
}
// scripts/branch.ts — the build command is `bun scripts/branch.ts && next build`
import { createClient } from "bql.sh/client"
import { dbName } from "../db-name"

const name = dbName()
if (name !== "main") {
  const admin = createClient({ url: process.env.BQL_URL!, token: process.env.BQL_ADMIN_KEY! }).admin
  await admin.fork(name, "main").catch((err) => { if (err.code !== "CONFLICT") throw err })
}

bql db diff main <branch> shows what the branch's migrations did, and bql db reset <branch> throws them away. packages/db/docs/x2-branching.md has the rest.

The bus

Publish to a subject; durable subscriptions deliver to consumers with leases, retries, ordering and a dead-letter path. One set of primitives covers a work queue, pub/sub and request/reply. Nothing in the core knows what an agent is — agent patterns are conventions over subjects, which is what keeps a plain background job queue a first-class use. It ships the library, the bql bus command and a built dashboard.

bun run bus dev                              # bus, three consumers and the dashboard, on free ports

bql bus serve &
bql bus subscribe work 'work.>'
bql bus consume work --exec ./handle.sh &
bql bus publish work.resize '{"src":"a.png"}'

At-least-once by default, with three named tiers of exactly-once on top — a transactional ack in embedded mode, atomic read-process-write inside the bus, and fenced ledgered effects against the outside world. Fan-out happens on pull rather than on publish, so publishing is O(1) in the number of subscriptions and a subscription created today can read last week. A JSON Schema registry with computed compatibility checks, scoped tokens with affordable revocation, W3C traceparent end to end, and epoch-fenced promotion over asynchronous log replication. Cron schedules (bql bus schedule add nightly '30 2 * * *' reports.nightly --tz America/New_York) fire on the leader as ordinary deduplicated publishes, DST-correct in any IANA zone. packages/bus/README.md is the real documentation.

Together

Neither half imports the other's source. The coupling is the work after the move, in the order the value lands: the bus swapping bun:sqlite for bql.sh/sqlite, then the database as a transactional outbox, then the bus's own replication and backup giving way to WAL shipping. docs/monorepo.md has the sequence and why each step is independently revertable.

The outbox has landed. A database server with [outbox] rules tails each matching database's log and publishes every committed row change to a bus subject over HTTP, with a dedupe key per change and a cursor that moves only after the bus acknowledged — so a crash never loses a change and never puts one on the bus twice. It reads the durable log rather than a commit hook, which is what closes the dual-write window for real. On the other side, bql bus sink webhook|s3|clickhouse drains a subscription into a webhook, a bucket of gzipped NDJSON, or ClickHouse:

# bql.toml
[replication]
logicalChanges = "row"

[[outbox.rules]]
db = "app-*"
busUrl = "http://127.0.0.1:4317"
tokenEnv = "BUS_TOKEN"
bql bus sink webhook --subscription cdc --subject 'db.>' --to https://example.com/hook

packages/db/docs/x6-outbox.md is the design and what changed while building it.

CI and releases

Every push and pull request to main runs both halves on Linux. macOS and Windows legs ran here until 2026-09-26 and both were green — they were dropped for cost and wall-clock, not because they stopped passing, and what they proved is written down in packages/db/docs/e1-windows.md, e2-windows-gate.md and c6-packaging.md §1.

One package, one tag: v0.1.0 publishes bql.sh with both halves in it, and the tag must match the root package.json version or the release refuses. bun run pack:check is the gate the release runs first — it packs, installs the tarball somewhere that knows nothing about this repository, builds libsqlite3 from it, and then uses both halves and both binaries out of that install. Not on npm yet.

Deploying to Vercel, Cloudflare, or Fly

BQL uses your existing vercel, wrangler, or fly login. The CLI creates a separate application, stages only BQL's shipped source and templates, and saves a connection in your local organization/project registry. It never uploads your current project or changes its Vercel link or Fly configuration.

Cloudflare and Vercel are experimental. Their containers restore committed state from private object storage; Fly uses a persistent volume. A successful cloud write means its data, catalog changes and retry result have been published through one conditional root update. A stopped or replaced container restores that root on its next request. Local container files are disposable.

Choose a provider based on where you want compute and durable data to live:

| Provider | Compute | Durable data | Default region | Status | | --- | --- | --- | --- | --- | | Vercel | Bun container | Private Vercel Blob | iad1 | Experimental object mode | | Cloudflare | Worker + named Bun container | Private R2 bucket | wnam bucket location hint | Experimental object mode | | Fly | Bun machine | Encrypted persistent volume at /data | sjc | Persistent-disk server |

This deployment workflow provisions the database, not the message bus. Cloudflare uses R2; Vercel uses Blob. Neither relies on the container's temporary filesystem for acknowledged database writes. The database core has no provider SDK dependency: provider packages contain the SDKs, and native CLIs supply account authentication.

First deployment

Install Bun and the provider CLI and sign in (vercel login, wrangler login, or fly auth login). For Vercel and Cloudflare, start Docker with Linux amd64 build support. Cloudflare Containers requires Workers Paid; BQL does not purchase or upgrade subscriptions automatically.

Use a separate directory for each deployment:

mkdir bql-preview
cd bql-preview
bql deploy init --provider vercel --scope my-team
bql deploy plan
bql deploy apply
bql deploy status

The equivalent provider-specific starting points are:

# Vercel: use your logged-in team; start Docker first.
vercel login
bql deploy init --provider vercel --name my-bql-vercel --scope MY_TEAM --dir ./vercel-preview

# Cloudflare: use your account ID; enable Workers Paid and start Docker first.
wrangler login
bql deploy init --provider cloudflare --name my-bql-cloudflare --scope ACCOUNT_ID --dir ./cloudflare-preview

# Fly: use personal or an organization slug; builds run remotely.
fly auth login
bql deploy init --provider fly --name my-bql-fly --scope personal --dir ./fly-preview

# Plan and apply the chosen deployment directory.
bql deploy plan --dir ./fly-preview
bql deploy apply --dir ./fly-preview
bql deploy status --dir ./fly-preview

These are independent examples: provision only the provider you want. Names must be available in the provider's namespace. Without --name, BQL generates a name.

| Option | Applies to | Meaning | | --- | --- | --- | | --provider vercel\|cloudflare\|fly | init | Required deployment target | | --name NAME | init | Provider application/resource name | | --environment preview\|production | init | Defaults to preview | | --region REGION | init | Defaults shown in the provider table | | --scope ACCOUNT | init | Vercel team, Cloudflare account ID, or Fly organization | | --dir DIR | All deploy commands | Directory containing .bql/deployment.json; defaults to cwd | | --config-dir DIR | All deploy commands | Private workstation journal and connection registry | | --r2-credentials FILE | Cloudflare apply | Private file containing bucket-scoped S3 credentials | | --json | All deploy commands | Structured output; deploy commands already print JSON |

init and plan are offline. apply creates billable resources, builds and publishes the app, checks readiness, verifies a SQL write/read, and saves a connection profile. status reports recorded resources, current/pending release identities, and a live readiness probe; it does not provision resources. A probe can wake a sleeping container and incur usage.

For production, initialize a different directory and resource name with --environment production. Vercel explicitly targets the selected environment; Cloudflare uses a separate Worker, bucket, and named container. Do not edit an existing deployment's identity or reuse preview storage for production.

Cloudflare's first apply creates a private bucket and, if necessary, explains how to supply its credentials. At the linked R2 token page, create Object Read & Write credentials limited to that bucket. Wrangler login cannot create these S3 keys. Save them outside your application in a private file:

BQL_S3_ACCESS_KEY_ID=your-access-key
BQL_S3_SECRET_ACCESS_KEY=your-secret-key
chmod 600 /private/path/r2.env
bql deploy apply --r2-credentials /private/path/r2.env

Subsequent applies use the saved private credentials. Never put keys in command arguments, Git, or chat. Vercel creates private Blob storage linked only to the selected environment, and retrieves its token through the native CLI. Deployment Protection stays enabled; the saved connection includes an automation bypass credential alongside BQL's admin credential.

Apply prints a connection profile with org, project, and endpoint selectors. Use those values with existing commands, for example:

bql db create example --org deploy-vercel --project YOUR_APP --endpoint preview
bql exec example --sql 'select 42' --org deploy-vercel --project YOUR_APP --endpoint preview

The global connection selection does not change. Apply also retains a small bql_deploy_… database for its scoped SQL smoke check. Repeating the check verifies the same identity without creating another database.

To make the saved profile the default for a checkout, use the existing link command:

bql link --org deploy-vercel --project YOUR_APP --endpoint preview
bql db list
bql exec example --sql 'SELECT 42'

Use deploy-cloudflare or deploy-fly for the other providers. BQL admin and signing keys remain stable across applies. Named connections use their own saved credentials, so ambient tokens do not silently select a different database.

How a cloud write becomes durable

  1. The runtime reads the authoritative deployment root and checks authorization.
  2. It executes the operation against a local SQLite working copy.
  3. It uploads immutable, checksummed database records and any changed catalog, database settings, revocations, and retained request result.
  4. It conditionally replaces the root using the version it originally read.
  5. Only successful publication allows a successful write response.

The root points to a complete committed state. Two writers starting from the same root cannot both replace that version: one wins and the other receives a conflict. Tentative local changes are discarded on failure. Recovery loads the committed root and verifies referenced records; shutdown hooks are not required to save an already acknowledged write.

If a root update reaches storage but its response is lost, the caller receives COMMIT_UNKNOWN. Resolve it with the same idempotency key and identical request; BQL can return the saved result without executing the SQL again. A new key means a new request. The retained-result window is 24 hours, not an unlimited exactly-once guarantee. A definite CLOUD_CONFLICT requires the application to decide whether to try the operation again against newer state.

Coming back after 30 days works through the same saved endpoint: the container starts, reads Blob or R2, and restores committed state. On Fly, the machine reopens its attached volume. Data retention is independent of container uptime; deleting the store/volume or configuring an expiry policy can remove the data.

State and recovery

.bql/deployment.json in the deployment directory contains resource identity and settings, not secrets. Workstation journals, stable signing/admin keys, and staged releases live below the BQL configuration directory (~/.config/bql by default, BQL_CONFIG_DIR or --config-dir to override). Files containing credentials are 0600 and deployment directories are 0700. Back up that private configuration. Changing an existing deployment's provider, identity, environment, or region is refused. Create a separate deployment for production.

A failed apply reports resources it created and retains them. Run status, fix the reported problem, and apply again. Completed provisioning steps are reused; new BQL source produces a new release. Applying an older source version after a newer one republishes it; historical success does not mean it is currently active. An interrupted publish is tracked separately so returning to the last confirmed release also republishes when necessary. Status reports saved resources and a live readiness check, not a complete provider inventory. Readiness may wake a sleeping container and incur usage.

A command can succeed remotely even if the connection or local process dies before its resource ID is saved. If the next apply encounters an existing resource without recorded ownership, it refuses to adopt it. Reconcile that resource and recover the private journal before retrying, or use a different deployment name. There is no automatic deletion, destructive rollback, or resource adoption. Missing recorded storage or a missing previously initialized root is an error; apply will not replace it with an empty database.

The first initialization creates a root only when absent. Server startup and readiness never initialize storage. R2/Blob data remains after 30 days unless you or a provider lifecycle policy remove it; container lifetime does not determine data retention. The request-result retry window is separately limited to 24 hours. Do not configure automatic expiry on the deployment's objects. BQL currently does not garbage-collect old objects, so retained storage grows with writes.

Capabilities and limits

| Capability | Vercel / Cloudflare object mode | Fly persistent disk | | --- | --- | --- | | SQL reads and writes, atomic batch | Supported | Supported | | Catalog, database settings, token revocation | Durably published | Persistent disk | | Container replacement recovery | Private Blob / R2 | Attached volume | | Explicit keyed retry result | 24-hour window | Ordinary server semantics | | Interactive transactions, realtime | Rejected | Supported | | Import, fork, PITR, generated APIs, replication | Rejected in object mode | Ordinary server capabilities | | Multi-region writes | One global conditional root; conflicts possible | Not configured by this workflow |

Object mode refreshes authoritative metadata from the storage origin and uses conditional writes. It does not retry uncertain writes automatically or silently re-execute conflicting SQL. Keep an explicit idempotency key when resolving an unknown result. Retry retention and generation-aware read-your-writes are separate from how long your database is retained.

Cloudflare uses one named container, a basic instance, and five-minute idle sleep. Vercel containers can overlap during deployments; the global root serializes publication. Fly creates one encrypted 1 GB volume, one machine, IPv6 and shared IPv4, with automatic stop/start. This workflow does not configure high availability or independent backups. Retained volumes, objects, compute, requests and data transfer can all incur provider charges; consult current provider pricing.

Costs and resource ownership

Cloudflare's Workers Paid base subscription was $5/month when qualified on 2026-09-26, with compute/storage/request usage charged separately. BQL never upgrades the subscription automatically. Vercel and Fly charge according to their plans and resources; sleeping compute does not remove retained storage charges. Check current Cloudflare Containers, Vercel Blob, and Fly pricing before applying.

Apply retains resources after a failure and does not delete them when local configuration is removed. There is no bql deploy destroy command. Inspect and remove unwanted test resources through the provider's CLI/dashboard only after preserving any data you need. Backing up the workstation configuration preserves connection/signing credentials; it is not an independent backup of the database.

Qualification

As of 2026-09-26, deterministic local tests cover conditional publication, conflicts, ambiguous outcomes, cache invalidation, revocation, request-result recovery, empty-disk recovery, shutdown and readiness. Real R2 and private Blob passed atomic create/replace, stale-version rejection and origin visibility tests. Both also passed an injected loss of a successfully persisted root response, then empty-cache recovery and resolution of the original request key without SQL replay.

Vercel's isolated test project recovered acknowledged SQL, catalog/settings, revocations and saved retry results across two preview deployments. Cloudflare passed the same checks from local Docker to hosted Containers, through confirmed idle sleep/wake, and after a new image rollout (container version 3). These tests used small databases in a single configured region. The Fly CLI test also created and queried a database through its saved profile, restarted its machine, and recovered the persisted row from the same volume. They do not establish cross-region behavior, maximum database size, production load capacity, or all provider payload and overwrite limits. Both object-mode providers remain experimental pending those gates.

Measured storage path

The checked-in R2, Blob, and in-memory baseline reports use Bun 1.4.0 on macOS arm64. The remote measurements ran from one workstation against the isolated R2 bucket and private Blob store. They include real storage latency but exclude hosted container boot, frontend routing and client network overhead. Each nonempty row contains 1 KiB of random data. Warm numbers are medians of five reads; these are observations, not service-level guarantees.

| Provider | Payload | Empty-cache open + first query | Warm read median | Restore transfer | | --- | ---: | ---: | ---: | ---: | | R2 | 0 | 3,188 ms | 845 ms | 9.4 KB | | R2 | 100 KiB | 2,012 ms | 476 ms | 114 KB | | R2 | 8 MiB | 2,888 ms | 434 ms | 8.51 MB | | Private Blob | 0 | 1,328 ms | 396 ms | 9.4 KB | | Private Blob | 100 KiB | 1,488 ms | 377 ms | 114 KB | | Private Blob | 8 MiB | 3,434 ms | 388 ms | 8.51 MB |

In each two-writer race, one write committed and one returned CLOUD_CONFLICT; fresh recovery contained exactly the successful writes. The 8 MiB scenarios each made 97 protocol reads and 42 write attempts across setup, recovery, warm reads, contention and final verification. They transferred about 28.3 MB down / 31.0 MB up and retained 37 objects totalling about 31.0 MB, including unpublished candidates. This shows why conflict traffic and retained immutable objects matter for cost even when the logical database is small.

For an approximate operation-only estimate, multiply measured GETs by the current read rate and create/replace attempts by the current write rate. At R2 Standard's published $0.36/$4.50 per million read/write operations, 97 reads and 42 writes are about $0.000224 before free allowances and billing rounding. Storage, compute, base subscriptions and provider-specific charges are separate. R2 pricing. Vercel's rates vary by region and request classification; use its current Blob pricing. The meter counts protocol calls, not SDK-internal HTTP requests or billable usage reported by a provider.

The largest measured payload is 8 MiB, not a tested maximum. The benchmark uses a 120-second request budget; default hosted requests use 30 seconds. Observed restores were below that default, but the workstation measurements do not qualify hosted disk/memory ceilings or provider execution limits. Cross-region tests, sustained load and larger sizes remain explicit experimental gates.

Run bun packages/db/bench/cloud.ts for the local baseline. Real storage requires BQL_BILLABLE_TESTS=1 plus the provider variables described in the deployment example README and --provider r2|blob. Objects are retained under unique test roots. BQL_RECOVERY_TESTS=1 adds a lost-CAS-response/empty-cache recovery check to the provider storage smoke runners; build the pinned SQLite library first. The cloud-qualification.yml workflow runs only by manual dispatch with the confirmation BILLABLE and secrets in the cloud-qualification environment.

Testing and implementation map

Run bun run typecheck alongside the checks below. Normal tests require no provider login or billable resources. Native process and HTTP boundaries are injectable: the suite covers account selection, private credentials, interrupted provisioning, resource ownership, missing storage, release restoration, and endpoint registration with mocks. Local cloud tests use fault injection and fresh caches; pack checks install the actual tarball and exercise the deployment workflow, saved profile, and SQL against a local server.

# From the repository root, after bun install and bun run db sqlite:build:
bun test packages/db/test/deploy packages/db/test/cloud
bun install --cwd deploy/vercel --frozen-lockfile
bun test deploy/vercel/test deploy/cloudflare/test
bun run deploy:templates:check
bun run pack:check

# Local baseline: no cloud credentials or billing.
bun packages/db/bench/cloud.ts

For a real storage test, use disposable private storage and set credentials through your secret manager, never command arguments. Both providers require BQL_BILLABLE_TESTS=1 and BQL_TEST_PREFIX=bql-qualification/. R2 additionally requires S3_BUCKET, S3_ENDPOINT, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, and S3_REGION=auto. Blob requires BLOB_READ_WRITE_TOKEN and VERCEL_BLOB_RETRIES=0. Add BQL_RECOVERY_TESTS=1 to exercise a lost root-write response and empty-cache recovery, then run the chosen provider's storage:smoke command:

bun run --cwd deploy/cloudflare storage:smoke
# Or:
bun run --cwd deploy/vercel storage:smoke

These opt-in tests retain their uniquely named objects. The manual CI workflow uses BQL_TEST_BLOB_TOKEN or BQL_TEST_R2_BUCKET, BQL_TEST_R2_ENDPOINT, BQL_TEST_R2_ACCESS_KEY_ID, and BQL_TEST_R2_SECRET_ACCESS_KEY repository/environment secrets. Normal push/PR CI runs mocked tests and the clean-package check only.

| Area | Source | | --- | --- | | Native CLI orchestration, journals, profiles | packages/db/src/deploy | | Shared publication and recovery protocol | packages/db/src/cloud | | Vercel adapter, Docker image, examples | deploy/vercel | | Cloudflare Worker/container, R2 examples | deploy/cloudflare | | Shipped provider templates | packages/db/deploy-templates | | Template synchronization | scripts/deploy-templates.ts | | Measurement runner | packages/db/bench/cloud.ts | | Opt-in billable CI | .github/workflows/cloud-qualification.yml |

If you change provider examples, run bun run deploy:templates to synchronize the shipped copies, then bun run deploy:templates:check. Provider SDKs stay in their isolated packages; the database core does not gain those runtime dependencies.