Skip to content
VegaStack
All docs
Operations

Self-hosting on Cloudflare

Deploy VegaStack on Cloudflare Workers, Postgres (via Hyperdrive), and R2 via OpenNext.

Updated 2026-05-24

Self-hosting on Cloudflare

VegaStack runs on Cloudflare Workers through @opennextjs/cloudflare. The full surface is one Worker, a Postgres database reached over Cloudflare Hyperdrive, and two R2 buckets.

Bindings

  • HYPERDRIVE — Cloudflare Hyperdrive binding to the Postgres database. Schema is managed by the @vegastack/db migration runner.
  • CONTENT — R2 bucket holding source artifacts, rendered artifacts, and attachments.
  • NEXT_INC_CACHE_R2_BUCKET — R2 bucket holding the OpenNext incremental cache.
  • WORKER_SELF_REFERENCE — service binding back to the same Worker so the scheduled handler can call internal routes.

Durable Object classes used by OpenNext: DOQueueHandler, DOShardedTagCache, BucketCachePurge. No other Cloudflare surface is required.

Bootstrap

The pnpm install:cloudflare script:

  1. Refuses resource names containing vegastack or vpg. Managed deployments must use fresh VegaStack resource names.
  2. Provisions the Hyperdrive config for the Postgres database and applies migrations.
  3. Creates the R2 buckets and wires the Worker vegastack_* secrets.

Run it once per environment. The bootstrap is idempotent on subsequent runs.

Migrations

For day-to-day local dev, pnpm db:migrate (alias pnpm --filter @vegastack/db migrate) applies pending migrations to the local Postgres database the dev server reads via DATABASE_URL. The pnpm dev predev hook does this automatically.

Remote (production) migrations are explicit and run only by the Release GitHub Actions workflow, which connects directly to Postgres via DATABASE_URL (not through Hyperdrive) and applies migrations before the Worker is deployed. Never run a remote migration from your laptop.

Build and deploy

pnpm --filter @vegastack/web build
pnpm --filter @vegastack/web opennext:build
pnpm --filter @vegastack/web opennext:preview

Preview runs the Worker locally with full Cloudflare emulation. Deploy is wrangler deploy from apps/web/ only after CI is green.

Scheduled work

The Worker scheduled() handler drains content jobs (render_page, publish_fanout) and GitHub backup jobs. Jobs are durable in Postgres and idempotent on retry. Search indexing is synchronous on every write — it does not need a scheduled drain.

Caching

Public pages serve the page's live rendered HTML. That render is cached in the Worker Cache API keyed by content hash, with R2 as the cold tier — because the key is the content hash, an edit produces a fresh entry automatically and no purge is needed. The per-request public-access check (effective level / password / expiry) is never cached, so turning a page's public link off or letting it expire takes effect immediately. Public images are content-addressed and served with a short immutable cache.