@guren/plugin-cloudflare
v0.12.3
Published
Deploy a Guren application to Cloudflare Workers, with D1 as the database.
Maintainers
Readme
@guren/plugin-cloudflare
Deploy a Guren application to Cloudflare Workers, with D1 as the database.
bunx guren plugin @guren/plugin-cloudflare
bun add @guren/plugin-cloudflareInstalling registers a cloudflare:build command and scaffolds wrangler.jsonc on the first build.
Build and deploy
bunx guren cloudflare:build
bunx wrangler deploycloudflare:build runs your app's build script, then assembles a .cloudflare/ directory containing the worker entry, static assets for Workers Static Assets, and flattened D1 migrations. It is generated output — add it to .gitignore and rebuild before every deploy.
Before the app build it runs the deploy-runtime checks guren doctor reports and warns, without failing, when sessions or OAuth state would sit in process memory, a Bun-only password hasher (Argon2Hasher, hasher: 'argon2', or new Hash({ algorithm: 'argon2' })) is selected, or providers are discovered from the filesystem. Each works locally and breaks on Workers, and the warning prints where you are still reading rather than after the Vite output.
bunx guren cloudflare:size (or cloudflare:build --report-size) measures the bundle a deploy would upload through wrangler's dry run and lists its largest sources by package. The platform limit is 64 MiB uncompressed on every plan; the report warns from half of it.
API
createWorkersHandler(app)— wraps a GurenApplicationin a Workers module handler. Boot is lazy and deduplicated on the first request, becauseboot()performs I/O that workerd forbids in global scope. The handler deduplicates boot itself, so boot-once holds for anything matchingWorkersAppLike, not only Guren'sApplication. It also exposesboot(env), for an entrypoint that holdsenvbut no request — an agent Durable Object woken by an alarm. The latch behind both isbootWorkersApp(app, env)/bootAndFetch(app, request, env, ctx), keyed on the app.Durable agents — when the app has a
config/agents.ts(see@guren/plugin-agents),cloudflare:buildappends a named export per registered class to the generated worker, verifies the committedwrangler.jsonchosts each one as a SQLite-backed Durable Object (failing with the exact JSON to add), and mounts/agents/*deny-all behind the registry'srouting.authorize.getWorkersEnv<Env>()andisWorkersRuntime()— from@guren/plugin-cloudflare/env, an import-free subpath, soconfig/*.tsdoes not drag the deploy generator into every boot.getWorkersEnvexposes the first request's bindings to boot-time config through a write-once holder;isWorkersRuntime()is the workerd check the snippets below branch on. workerd can also expose bindings at module scope viacloudflare:workers, but importing that from shared config would break every other runtime (Bun, Lambda, Vercel) — the holder keepsconfig/*.tsportable. Use it to hand a D1 binding to the ORM:import { createD1Database } from '@guren/core' import { getWorkersEnv } from '@guren/plugin-cloudflare/env' const database = createD1Database({ binding: () => getWorkersEnv<{ DB: unknown }>().DB, })The binding is a resolver, not a value — it must be read lazily.
R2Driver— a storage driver over the R2 bucket binding, forStorageManager.registerDisk(). Same lazybindingcontract as D1; no credentials and no AWS SDK in the bundle:import { createStorageManager, LocalStorageDriver } from '@guren/core' import { getWorkersEnv, isWorkersRuntime } from '@guren/plugin-cloudflare/env' import { R2Driver } from '@guren/plugin-cloudflare' const storage = createStorageManager({ default: 'media' }) storage.registerDisk('media', () => isWorkersRuntime() ? new R2Driver({ binding: () => getWorkersEnv<{ MEDIA: unknown }>().MEDIA, publicUrl: 'https://media.example.com' }) : new LocalStorageDriver({ root: './storage/app/public', url: '/storage' }), )with
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "my-app-media" }]inwrangler.jsonc. Three methods differ from the S3 driver, because the binding differs from S3:url()needspublicUrl(a custom domain or the r2.dev subdomain — R2 has no derivable public URL);temporaryUrl()needs the optionalpresigncredentials, and throws with guidance otherwise (bindings cannot sign URLs);setVisibility()/put({ visibility })throw when asked for the opposite of the bucket's declaredvisibility, because R2 has no per-object ACL to honour the request with.putFile()throws — Workers has no filesystem.cloudflarePlugin()— the service provider factory, registered automatically byguren plugin.
Things Workers changes
Workers has no filesystem and shares no memory between requests, so a few defaults do not apply:
- Sessions and OAuth state must be database-backed. Each request may land on a different isolate. The in-memory defaults work locally and then drop every session in production. Use
DatabaseSessionStoreandDatabaseOAuthStateStore. - Migrations are applied out of band.
wrangler d1 migrations applyowns the lifecycle; the app never migrates itself. APP_KEYis required. Sessions and CSRF are signed with it, and the worker throws at startup without it.
See the Cloudflare Workers deployment guide for the full path from an empty account to a deployed app, including D1 setup, secrets, free-plan limits, and local development.
License
MIT
