Skip to content

feat: scaffold OpenNext Cloudflare build path#86

Merged
FrkAk merged 8 commits into
mainfrom
feat/mymr-164-opennext-cloudflare-adapter
May 18, 2026
Merged

feat: scaffold OpenNext Cloudflare build path#86
FrkAk merged 8 commits into
mainfrom
feat/mymr-164-opennext-cloudflare-adapter

Conversation

@FrkAk
Copy link
Copy Markdown
Owner

@FrkAk FrkAk commented May 18, 2026

Summary

Task Reference: [MYMR-164]

Adds the OpenNext Cloudflare build path so Mymir can deploy to Workers while keeping the self-host Docker / Postgres build working from the same repo. The two builds are gated by DEPLOY_TARGET=cloudflare; each output skips what the other needs.

What this PR ships:

  • Adapter scaffolding: open-next.config.ts (R2 incremental cache, DO queue, D1 tag cache) and wrangler.jsonc with ASSETS, WORKER_SELF_REFERENCE, KV (AUTH_KV), R2 (NEXT_INC_CACHE_R2_BUCKET), D1 (NEXT_TAG_CACHE_D1), and the MYMIR_BROKER Durable Object. Placeholder IDs land at the syntactic minimum wrangler accepts; MYMR-165 fills them in.
  • Build-time driver split: lib/db/_driver.{node,workers,ts} so the Workers bundle imports only @neondatabase/serverless and the self-host bundle imports only postgres-js. Routing happens at webpack module-resolution via NormalModuleReplacementPlugin in next.config.ts. The existing appDb / authDb / serviceRoleDb proxy surface stays — call sites are unchanged.
  • Per-request Pool lifecycle on Workers: lib/db/request-scope.workers.ts exports withRequestDb() that builds fresh Neon pools for the three roles, seeds an AsyncLocalStorage frame so the connection proxies resolve to them, and schedules ctx.waitUntil(pool.end()) so socket teardown does not block the response. Self-host falls back to a globalThis cache; the proxy surface picks the right path automatically.
  • Workers realtime broker scaffolding: lib/realtime/_broker.{node,workers,ts} mirroring the driver split, plus lib/realtime/broker-do.ts shipping a MymirBroker Durable Object skeleton. The DO is exported from worker.js via the scripts/postbuild-cf.ts patcher because OpenNext exposes no user-DO extension point.
  • Build scripts: build:cf, preview:cf, deploy:cf, cf-typegen in package.json. DEPLOY_TARGET=cloudflare is set on every command in the chain — setting it only on the leading command would lose it for OpenNext's internal next build pass.
  • proxy.ts → middleware.ts rename: Next 16's proxy.ts is locked to the Node.js runtime which workerd rejects; OpenNext needs Edge middleware. The CSP nonce generator swaps from Buffer to btoa for Edge compatibility. This slice was taken from MYMR-167's scope as a hard prerequisite; MYMR-167's remaining work shrinks to wiring CloudflareRateLimitBackend to the rate-limit bindings + integrating withRequestDb at the request gate.

What is not in this PR (deferred to downstream tasks):

  • MYMR-165 fills the PLACEHOLDER_* IDs in wrangler.jsonc with provisioned R2 / KV / D1 / DO IDs and attaches the app.mymir.dev custom domain.
  • MYMR-167 wires CloudflareRateLimitBackend and invokes withRequestDb at the middleware request gate.
  • MYMR-168 implements the KV-backed Better-auth secondaryStorage adapter.
  • MYMR-169 adds the GitHub Actions deploy workflow.
  • The MymirBroker DO ships as a skeleton (fetch returns 501). A follow-up task implements the full pub/sub fanout once MYMR-165 provisions the DO and MYMR-167 wires the realtime path through the middleware.

Deviations from the original implementation plan (recorded in the executionRecord on MYMR-164):

  1. R2 binding name: AC-6 specified NEXT_INC_CACHE_R2. OpenNext's r2-incremental-cache override hard-codes NEXT_INC_CACHE_R2_BUCKET (see node_modules/@opennextjs/cloudflare/dist/api/overrides/incremental-cache/r2-incremental-cache.js:6). Used the override's expected name; MYMR-165 should match.
  2. JWKS module cache: AC-8 asked for a module-scope JWKS cache in app/api/mcp/route.ts. BetterAuth's verifyJwsAccessToken (@better-auth/core/dist/oauth2/verify.mjs:7,32-36) already caches JWKS at module scope; no code change needed.
  3. proxy.ts rename: documented above. Done now because the CF build cannot complete otherwise.
  4. MymirBroker export: OpenNext exposes no custom-DO hook, so scripts/postbuild-cf.ts bundles broker-do.ts and appends the export to worker.js after each opennextjs-cloudflare build.

Type of change

  • New feature

Testing

  • Tested locally with bun run dev
  • Linting passes (bun run lint)
  • Typecheck passes (bun run typecheck)
  • bun run format:check passes
  • Self-host bun run build produces .next/standalone (output: "standalone" only emitted when DEPLOY_TARGET is unset)
  • bun run build:cf produces .open-next/worker.js carrying @neondatabase/serverless and zero postgres-js references (grep -c "postgresjs_" .open-next/server-functions/default/handler.mjs returns 0; grep -c "@neondatabase\|NeonPool\|neon-serverless" returns 2)
  • MymirBroker is exported from .open-next/worker.js and bundled into .open-next/.build/durable-objects/mymir-broker.js

End-to-end verification under wrangler dev is deferred to MYMR-165 because the PLACEHOLDER_* IDs in wrangler.jsonc are not yet provisioned. That gate runs as part of MYMR-165.

Notes for reviewer

  • The webpack alias originally used resolve.alias which did not reliably match relative imports of the indirection files. Switched to NormalModuleReplacementPlugin with a regex on the import specifier. Verified the plugin fires consistently against .workers for both the leading next build pass and OpenNext's internal next build pass — provided DEPLOY_TARGET=cloudflare is set on every command in the chain.
  • The Workers driver returns { pool, db } with a portable ClosablePool = { end: () => Promise<unknown> } shape so the seeding helper can pool.end() without depending on driver-specific Pool types.
  • cloudflare-env.d.ts is committed but excluded from tsconfig include / biome / eslint because its inline workers-types declarations override DOM Response / Request shapes and break unrelated tests. Local stubs in the four Workers-only source files cover the DO types we actually use.
  • The withRequestDb helper is exported but not yet invoked. MYMR-167 wires it into the middleware request gate where every route handler will run inside a withRequestDb(() => ...) frame.
  • The DO class extends nothing yet (skeleton). workerd warns about this at build time; the warning is acceptable for scaffolding and clears once the DO implementation lands.

@FrkAk FrkAk requested review from ZeyNor and ulascanzorer as code owners May 18, 2026 21:01
@FrkAk FrkAk self-assigned this May 18, 2026
@FrkAk FrkAk merged commit 6cf195d into main May 18, 2026
5 checks passed
@FrkAk FrkAk deleted the feat/mymr-164-opennext-cloudflare-adapter branch May 18, 2026 23:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant