Skip to content

@zenstackhq/better-auth bundles @zenstackhq/language at runtime, crashing Cloudflare Workers (createRequire(import.meta.url)) #2610

@Azzerty23

Description

@Azzerty23

Summary

When using @zenstackhq/better-auth in a Cloudflare Workers environment, the Worker crashes on startup with:

Uncaught TypeError: The argument 'path' must be a file URL object, a file URL string, or an absolute path string. Received 'undefined'
  at node:module:34:15 in createRequire

The crash happens at module initialisation — before any request is served.


Root cause

@zenstackhq/better-auth/dist/index.mjs has top-level imports from @zenstackhq/language:

// @zenstackhq/better-auth/dist/index.mjs (top of file)
import { ZModelCodeGenerator, formatDocument, loadDocument } from "@zenstackhq/language";
import { isDataModel } from "@zenstackhq/language/ast";
import { hasAttribute } from "@zenstackhq/language/utils";

These imports are used inside generateSchema(), which is wired as the createSchema callback of the adapter (for the better-auth CLI migration tool). Because the reference is live inside the exported zenstackAdapter, esbuild cannot tree-shake the imports away.

@zenstackhq/language is the Langium / VS Code grammar-tooling package. Its dist/index.mjs contains:

// @zenstackhq/language/dist/index.mjs line 16
var __require = /* @__PURE__ */ createRequire(import.meta.url);

In Cloudflare Workers, import.meta.url is undefined, so createRequire(undefined) throws immediately when the module is evaluated, killing the Worker before it can handle any request.


Import chain

apps/server/src/index.ts
  → @my-better-t-app/auth (workspace package)
    → @zenstackhq/better-auth  ← zenstackAdapter
      → @zenstackhq/language   ← createRequire(import.meta.url) 💥

Context / reproduction

The project was generated with better-t-stack targeting Cloudflare Workers and then migrated from Prisma to ZenStack:

bun create better-t-stack@latest my-better-t-app \
  --frontend tanstack-router \
  --backend hono \
  --runtime workers \
  --api trpc \
  --auth better-auth \
  --payments none \
  --database postgres \
  --orm prisma \
  --db-setup neon \
  --package-manager bun \
  --git \
  --web-deploy cloudflare \
  --server-deploy cloudflare \
  --no-install \
  --addons oxlint pwa skills turborepo wxt \
  --examples ai todo

The infrastructure is managed by Alchemy (Cloudflare Workers / Miniflare). The server bundle is built by Alchemy's internal esbuild pipeline.

Versions:

  • @zenstackhq/better-auth: 3.6.2
  • @zenstackhq/language: 3.6.2
  • better-auth: ~1.4
  • Alchemy: 0.91.2
  • Miniflare: 4.20260310.0

Workaround

We stub out @zenstackhq/language via an esbuild alias so the grammar package is never bundled into the Worker. Since generateSchema() is only called by the better-auth CLI (schema migrations) and never during request handling, replacing it with no-op exports is safe at runtime.

packages/infra/stubs/zenstack-language-stub.js

// Stub replacing @zenstackhq/language, /ast, /utils in CF Workers.
// Only needed by the better-auth CLI for schema migration — never at request time.
export const ZModelCodeGenerator = class {};
export const formatDocument = async (s) => s;
export const loadDocument = async () => null;
export const isDataModel = () => false;
export const hasAttribute = () => false;

alchemy.run.ts (Alchemy Worker config)

// ...
const __dirname = dirname(fileURLToPath(import.meta.url));
const zenstackLanguageStub = join(__dirname, "stubs/zenstack-language-stub.js");
// ...

export const server = await Worker("server", {
  // ...
  bundle: {
    alias: {
      "@zenstackhq/language": zenstackLanguageStub,
      "@zenstackhq/language/ast": zenstackLanguageStub,
      "@zenstackhq/language/utils": zenstackLanguageStub,
    },
  },
});

The same alias approach applies to any bundler (esbuild, Rollup, Vite, tsdown) targeting Cloudflare Workers or any other runtime where import.meta.url is unavailable.


Suggested fix

@zenstackhq/language is a development / CLI tool dependency — it is only needed when running zenstack generate or better-auth migrate. It should never be part of the runtime adapter bundle.

Two possible approaches:

Option A — Lazy / dynamic import (preferred)

In schema-generator.ts, replace the static top-level imports with dynamic import() calls:

// Before (crashes CF Workers on load)
import { ZModelCodeGenerator, formatDocument, loadDocument } from "@zenstackhq/language";

// After (only evaluated when generateSchema() is actually called)
const { ZModelCodeGenerator, formatDocument, loadDocument } = await import("@zenstackhq/language");

This way the import is never evaluated during request handling, and bundlers can safely exclude it or mark it external without breaking runtime behaviour.

Option B — Separate entrypoint / sub-path export

Split @zenstackhq/better-auth into two sub-path exports:

"exports": {
  ".":          "./dist/adapter.mjs",        // runtime only — no language dep
  "./cli":      "./dist/schema-generator.mjs" // CLI only — requires @zenstackhq/language
}

Users import zenstackAdapter from "@zenstackhq/better-auth" (runtime) and the CLI tool imports generateSchema from "@zenstackhq/better-auth/cli".


Additional notes

  • The tsdown.config.ts in the generated project already had external: ["@zenstackhq/language"] as a workaround — indicating this problem was previously known at the build level, but it doesn't solve the runtime crash when using Alchemy's bundler directly.
  • This issue affects any edge/serverless runtime where import.meta.url is undefined (Cloudflare Workers, Deno Deploy, etc.).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions