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/dbmigration 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:
- Refuses resource names containing
vegastackorvpg. Managed deployments must use fresh VegaStack resource names. - Provisions the Hyperdrive config for the Postgres database and applies migrations.
- 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:previewPreview 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.