Deploy to Cloudflare
The Worker entry is apps/server/src/entry/worker.ts. Bindings only exist
inside fetch, so the application context is built per request; Hyperdrive
keeps the real connection pool warm at the edge, which is what makes a fresh
Postgres client per request affordable. Each request opens a pool of one and
closes it in ctx.waitUntil.
| Runtime | Database | Storage |
|---|---|---|
| Workers | Postgres via Hyperdrive | R2 through the S3 API |
Bindings
Section titled “Bindings”apps/server/wrangler.toml:
name = "open-ota"main = "src/entry/worker.ts"compatibility_date = "2025-01-01"# postgres-js and the AWS SDK both need the Node built-ins.compatibility_flags = ["nodejs_compat"]
[[hyperdrive]]binding = "HYPERDRIVE"id = "<hyperdrive-id>"
[[r2_buckets]]binding = "BUNDLES"bucket_name = "ota-bundles"
[vars]OTA_MODE = "self"STORAGE_DRIVER = "s3"STORAGE_BUCKET = "ota-bundles"STORAGE_REGION = "auto"DATABASE_URL is not a variable on this target: the Worker reads
env.HYPERDRIVE.connectionString and passes it to the config loader itself.
Secrets go in with wrangler secret put, never in the file: OTA_MASTER_KEY,
STORAGE_ACCESS_KEY, STORAGE_SECRET_KEY, plus RESEND_API_KEY and the
Stripe keys if you use them.
Why R2 goes through the S3 adapter
Section titled “Why R2 goes through the S3 adapter”The BUNDLES binding attaches the bucket to the Worker, but a bucket binding
cannot mint a signed upload URL. The CLI needs one: it uploads the zip straight
to the bucket so the bundle never crosses the API. Only R2’s S3-compatible
endpoint can issue that signed PUT, so the server uses the existing S3
adapter and needs an R2 API token even though the bucket is also bound.
STORAGE_ENDPOINT=https://<account-id>.r2.cloudflarestorage.comPUBLIC_BUNDLE_BASE_URL=https://<public-r2-or-cdn-hostname>STORAGE_FORCE_PATH_STYLE stays false for R2.
What ota init --provider cloudflare runs
Section titled “What ota init --provider cloudflare runs”packages/cli/src/providers/cloudflare.ts is a flat list of five steps, each
printed before it runs and confirmed when it changes remote state. Without
wrangler on PATH, or with --dry-run, the list is printed and nothing
executes.
| Step | Command |
|---|---|
| Create the R2 bucket | wrangler r2 bucket create ota-bundles |
| Create the Hyperdrive config | wrangler hyperdrive create open-ota --connection-string <postgres://…> (copy the printed id into wrangler.toml) |
| Apply the migrations | DATABASE_URL=postgres://… pnpm --filter @open-ota/server db:migrate — over a direct connection, not through the Worker |
| Store the master key | wrangler secret put OTA_MASTER_KEY (wrangler reads the value from stdin; the CLI prints the generated key to paste) |
| Deploy the Worker | wrangler deploy |
The database itself lives wherever you run Postgres — Supabase, Neon, RDS or a box of your own. Hyperdrive is the pooled path from the Worker to it, not a database; there is one SQL dialect across every target.
Doing it by hand
Section titled “Doing it by hand”wrangler hyperdrive create open-ota --connection-string="postgres://..." # id -> wrangler.tomlwrangler r2 bucket create ota-bundleswrangler secret put OTA_MASTER_KEYwrangler secret put STORAGE_ACCESS_KEY # R2 API token idwrangler secret put STORAGE_SECRET_KEY # R2 API token secretDATABASE_URL=postgres://... pnpm --filter @open-ota/server db:migratewrangler deployMigrations do not run on this target. Point DATABASE_URL at the same database
Hyperdrive fronts and run them before the first deploy, and again after every
upgrade.
The dashboard
Section titled “The dashboard”The Worker does not serve static files. Run the dashboard locally against the
deployed API with ota console, or deploy apps/dashboard/dist to Pages.
What is verified
Section titled “What is verified”src/app.ts is the same object on all three targets and is covered by the
server test suite, including the real migrations run against a real Postgres
engine in-process. Signing is Web Crypto only, so it runs unchanged on Workers.