Skip to content

feat: add Cloudflare Workers deployment for example app#92

Open
nicknisi wants to merge 2 commits into
mainfrom
fix/issue-90
Open

feat: add Cloudflare Workers deployment for example app#92
nicknisi wants to merge 2 commits into
mainfrom
fix/issue-90

Conversation

@nicknisi
Copy link
Copy Markdown
Member

Summary

  • Adds Cloudflare Workers deployment support to the example app via @cloudflare/vite-plugin
  • Includes a TypeScript deploy script (scripts/deploy-example-cloudflare.ts) that stages a temp copy, rewires workspace:* to the published npm version, and deploys via wrangler
  • Local dev continues to work — the Cloudflare plugin handles both local (workerd) and production runtimes

Changes

  • example/vite.config.ts — add @cloudflare/vite-plugin
  • example/wrangler.jsonc — worker config with nodejs_compat
  • scripts/deploy-example-cloudflare.ts — deploy script (runs via Node's native --experimental-strip-types)
  • scripts/tsconfig.json — tsconfig for scripts directory
  • Root package.json — add deploy:example and deploy:example:secrets scripts
  • .gitignore — ignore .wrangler/ and .dev.vars
  • example/.dev.vars.example — template for Cloudflare local secrets
  • example/README.md — Cloudflare deployment instructions

Deploy

# Push secrets from .env to Cloudflare
pnpm run deploy:example:secrets

# Deploy to Cloudflare Workers
pnpm run deploy:example

Test plan

  • pnpm dev works locally (runs on workerd via Cloudflare plugin)
  • pnpm run deploy:example builds and deploys successfully
  • Sign-in flow works end-to-end on deployed worker
  • No client bundle leaks (server-only modules stay server-side)

Closes #90

Add Cloudflare Workers support to the example app using the
@cloudflare/vite-plugin. Includes a deploy script that stages a temp
copy, rewires workspace:* to the published npm version, builds, and
deploys via wrangler.

- Add wrangler.jsonc with nodejs_compat and TanStack server entry
- Add @cloudflare/vite-plugin to vite config (works for local dev too)
- Add deploy script (scripts/deploy-example-cloudflare.ts) run via
  Node's native --experimental-strip-types
- Add deploy:example and deploy:example:secrets npm scripts at root
- Add .dev.vars.example for Cloudflare local secrets
- Update .gitignore for .wrangler/ and .dev.vars
- Update README with Cloudflare deployment instructions
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 18, 2026

Greptile Summary

This PR adds Cloudflare Workers deployment support to the example app via @cloudflare/vite-plugin, a deploy script that stages a temp copy and rewires workspace:* to the published npm version, and a wrangler config for the Worker runtime. The authkit-loader.ts module is also updated to use dynamic imports so server-only modules stay out of the client bundle.

  • scripts/deploy-example-cloudflare.ts — copies the example to a temp staging dir, rewrites the workspace SDK dependency to the published npm version, then runs pnpm install, vite build, and wrangler deploy; includes a guard that verifies the SDK resolved from npm rather than the workspace symlink
  • example/vite.config.ts + example/wrangler.jsonc — wires up @cloudflare/vite-plugin for local workerd dev and production; enables nodejs_compat compatibility flag
  • src/server/authkit-loader.ts — converts static imports of @workos/authkit-session and ./storage.js to dynamic imports so the barrel re-export no longer pulls server-only code into the client bundle

Confidence Score: 4/5

The core Cloudflare Workers setup and authkit-loader changes are solid, but the deploy script contamination guard may not work reliably across all pnpm hoisting configurations.

The authkit-loader dynamic-import refactor and the Vite/wrangler configuration are straightforward and correct. The deploy script contamination guard calls readlink on stagedExampleDir/node_modules which pnpm v9 may not populate, causing a cryptic ENOENT on a clean npm install.

scripts/deploy-example-cloudflare.ts — contamination guard symlink path and readlink reliability

Important Files Changed

Filename Overview
scripts/deploy-example-cloudflare.ts New deploy script with workspace-to-npm rewriting logic; contamination guard checks for symlink at stagedExampleDir/node_modules but pnpm v9 hoists to workspace root so the path may not exist, plus the hardcoded package path doesn't derive from sdkName
example/vite.config.ts Adds @cloudflare/vite-plugin before tanstackStart(); plugin order is correct for SSR environment handling
example/wrangler.jsonc New wrangler config with nodejs_compat flag and observability enabled; main field uses @tanstack/react-start/server-entry which is resolved by the Cloudflare Vite plugin during build
src/server/authkit-loader.ts Static imports of @workos/authkit-session and ./storage.js converted to dynamic imports; prevents server-only dependencies leaking into client bundle; module-level singleton pattern is correct for Workers
package.json Adds deploy:example and deploy:example:secrets scripts; deploy:example:secrets calls wrangler directly from root but wrangler is only in example/ devDependencies
example/tsconfig.json Adds declaration: false, declarationMap: false, and types: ["vite/client"]; JSX types remain valid because jsx: "react-jsx" uses module-based JSX runtime rather than global ambient types
example/package.json Adds @cloudflare/vite-plugin and wrangler as devDependencies; deploy script delegates correctly to the outer deploy-example-cloudflare.ts script via pnpm exec

Sequence Diagram

sequenceDiagram
    participant Dev as Developer
    participant Script as deploy-example-cloudflare.ts
    participant FS as Temp Staging Dir
    participant pnpm as pnpm install
    participant Guard as Contamination Guard
    participant Wrangler as wrangler deploy

    Dev->>Script: pnpm run deploy:example
    Script->>FS: mkdtemp() stagingRoot
    Script->>FS: cp example/ stagedExampleDir
    Script->>FS: cp package.json, pnpm-lock.yaml, pnpm-workspace.yaml, tsconfig.json
    Script->>FS: rewrite workspace to sdkVersion in example/package.json
    Script->>pnpm: pnpm install stagingRoot
    pnpm-->>FS: node_modules populated from npm
    Script->>Guard: readlink stagedExampleDir/node_modules/sdkName
    alt symlink points to stagingRoot workspace leaked
        Guard-->>Script: throw Error workspace contamination
    else symlink points to pnpm store npm installed correctly
        Guard-->>Script: ok
        Script->>Script: pnpm run build stagedExampleDir
        Script->>Wrangler: pnpm exec wrangler deploy
        Wrangler-->>Dev: Deployed to Cloudflare Workers
    end
    Script->>FS: rm -rf stagingRoot finally
Loading

Reviews (2): Last reviewed commit: "fix: lazy-import @workos/authkit-session..." | Re-trigger Greptile

Comment thread package.json
"test:coverage": "vitest run --coverage"
"test:coverage": "vitest run --coverage",
"deploy:example": "node --experimental-strip-types scripts/deploy-example-cloudflare.ts",
"deploy:example:secrets": "wrangler secret bulk example/.env --config example/wrangler.jsonc"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 wrangler is only a devDependency of example/, not the root package. In a pnpm workspace without shameful hoisting, binaries from child-workspace packages are not placed in the root node_modules/.bin, so running pnpm run deploy:example:secrets from the repo root will fail with "command not found: wrangler". The fix is to delegate to the example workspace, where wrangler is available.

Suggested change
"deploy:example:secrets": "wrangler secret bulk example/.env --config example/wrangler.jsonc"
"deploy:example:secrets": "pnpm --filter example exec wrangler secret bulk .env --config wrangler.jsonc"

Comment on lines +81 to +85
const dependencyLink = join(stagedExampleDir, 'node_modules', '@workos', 'authkit-tanstack-react-start');
const dependencyTarget = resolve(dirname(dependencyLink), await readlink(dependencyLink));
if (dependencyTarget === stagingRoot) {
throw new Error(`Staged deploy resolved ${sdkName} to the local workspace instead of npm.`);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The workspace-contamination guard calls readlink (one hop) and then manually resolves the relative link target. pnpm typically creates a single-level symlink for workspace packages (pointing directly to the package root), so this works today. However, if readlink rejects — e.g. ENOENT because pnpm skipped creating a top-level symlink for the package (possible in some hoisting modes) — the error will bubble out as a cryptic OS error rather than a diagnostic message. Using fs.promises.realpath would both follow all symlink levels and surface a clearer failure mode. The startsWith check also guards against resolved paths that are nested under the staging root but are not the root itself.

Suggested change
const dependencyLink = join(stagedExampleDir, 'node_modules', '@workos', 'authkit-tanstack-react-start');
const dependencyTarget = resolve(dirname(dependencyLink), await readlink(dependencyLink));
if (dependencyTarget === stagingRoot) {
throw new Error(`Staged deploy resolved ${sdkName} to the local workspace instead of npm.`);
}
const dependencyLink = join(stagedExampleDir, 'node_modules', '@workos', 'authkit-tanstack-react-start');
const { realpath } = await import('node:fs/promises');
const dependencyTarget = await realpath(dependencyLink);
if (dependencyTarget.startsWith(stagingRoot)) {
throw new Error(`Staged deploy resolved ${sdkName} to the local workspace instead of npm.`);
}

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +59 to +61
await cp(rootPackagePath, join(stagingRoot, 'package.json'));
await cp(join(repoRoot, 'pnpm-lock.yaml'), join(stagingRoot, 'pnpm-lock.yaml'));
await cp(join(repoRoot, 'pnpm-workspace.yaml'), join(stagingRoot, 'pnpm-workspace.yaml'));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Deploy script always fails by default because pnpm resolves SDK to the copied workspace instead of npm

The deploy script copies pnpm-workspace.yaml as-is (scripts/deploy-example-cloudflare.ts:61), which declares both . (the SDK root) and example/ as workspace packages. It also copies the root package.json (scripts/deploy-example-cloudflare.ts:59) containing @workos/authkit-tanstack-react-start@0.8.3. The example's dependency is rewritten from workspace:* to sdkSpec (line 71), which defaults to sdkPackage.version ("0.8.3") when WORKOS_AUTHKIT_TANSTACK_START_VERSION is unset. Since pnpm's link-workspace-packages defaults to true (and no .npmrc overrides this), pnpm will always link the workspace package when the version matches — which it does by default. The readlink check on line 83 catches this and throws, meaning pnpm run deploy (as documented in the README) always fails without setting an undocumented env var.

Staged workspace structure that causes the issue

The staging dir ends up as:

  • stagingRoot/package.json → SDK (name: @workos/authkit-tanstack-react-start, version: 0.8.3)
  • stagingRoot/pnpm-workspace.yamlpackages: ['.', 'example/']
  • stagingRoot/example/package.json → depends on @workos/authkit-tanstack-react-start: "0.8.3"

pnpm sees the workspace package at . satisfies the dependency and links instead of downloading from npm.

Prompt for agents
The deploy script copies pnpm-workspace.yaml from the repo root, which includes '.' as a workspace package. Since the root package.json (the SDK) is also copied with its version, pnpm's link-workspace-packages (default: true) will always resolve the example's dependency to the workspace package instead of downloading from npm.

The fix is to write a modified pnpm-workspace.yaml to the staging root that only includes 'example/' (not '.'). This way the SDK's package.json at the staging root is not a workspace member, and pnpm will resolve @workos/authkit-tanstack-react-start from the npm registry.

In scripts/deploy-example-cloudflare.ts around line 61, instead of:
  await cp(join(repoRoot, 'pnpm-workspace.yaml'), join(stagingRoot, 'pnpm-workspace.yaml'));

Write a workspace yaml that excludes the root:
  await writeFile(join(stagingRoot, 'pnpm-workspace.yaml'), 'packages:\n  - example/\n');

This ensures pnpm does not treat the staged root package.json as a workspace member and resolves the SDK from npm instead. The readlink safety check on lines 81-85 can be kept as an additional guard.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread package.json
"test:coverage": "vitest run --coverage"
"test:coverage": "vitest run --coverage",
"deploy:example": "node --experimental-strip-types scripts/deploy-example-cloudflare.ts",
"deploy:example:secrets": "wrangler secret bulk example/.env --config example/wrangler.jsonc"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 deploy:example:secrets script fails because wrangler is not in root devDependencies

The root package.json defines "deploy:example:secrets": "wrangler secret bulk example/.env --config example/wrangler.jsonc". However, wrangler is only listed in example/devDependencies (example/package.json:42), not in the root's devDependencies. In a pnpm workspace, each package has isolated node_modules/.bin/ — binaries from example/ are not available to root-level scripts. Running pnpm run deploy:example:secrets from the repo root will fail with wrangler: command not found.

Suggested change
"deploy:example:secrets": "wrangler secret bulk example/.env --config example/wrangler.jsonc"
"deploy:example:secrets": "pnpm --filter example exec wrangler secret bulk .env --config wrangler.jsonc"
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

authkit-loader.ts statically imported @workos/authkit-session, which
pulled @workos-inc/node → eventemitter3 into the client module graph
via the barrel re-export in server/index.ts. Convert to dynamic
await import() matching the lazy pattern in middleware.ts, actions.ts,
and server-functions.ts. Functions were already async — no API change.

Refs #82
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Session cookie not delivered on /api/auth/callback redirect (0.7.x–0.8.2, prod build only)

1 participant