Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug Report: Cloudflare Env Vars Not Passed to SSR in TanStack Start #3468

Open
scobbe opened this issue Feb 18, 2025 · 0 comments
Open

Bug Report: Cloudflare Env Vars Not Passed to SSR in TanStack Start #3468

scobbe opened this issue Feb 18, 2025 · 0 comments

Comments

@scobbe
Copy link

scobbe commented Feb 18, 2025

Which project does this relate to?

Start

Describe the bug

When running a TanStack Start app on Cloudflare (Workers/Pages) with SSR, the Cloudflare environment bindings (in getContext("cloudflare")) appear not to be accessible in the SSR pipeline. They work fine for API or “server” routes, but not for SSR.

Your Example Website or App

nonexistent

Steps to Reproduce the Bug or Issue

Steps to Reproduce

  1. Project Setup

    • Using TanStack Start (@tanstack/start@^1.101.2) with a config that has multiple routers (api, ssr, client, etc.).
    • Deployed on Cloudflare Pages/Workers.
    • Also tested locally via wrangler pages dev.
  2. Code Snippets

app.config.ts (TanStack start config)

import { defineConfig } from "@tanstack/start/config";
import { cloudflare } from "unenv";
import type { App } from "vinxi";
import { checker } from "vite-plugin-checker";
import Terminal from "vite-plugin-terminal";
import tsConfigPaths from "vite-tsconfig-paths";

const tanstackApp = defineConfig({
  server: {
    preset: "cloudflare-pages",
    unenv: cloudflare,
  },
  routers: {
    api: {
      entry: "src/entry.api.ts",
    },
    ssr: {
      entry: "src/entry.server.ts",
    },
    client: {
      entry: "src/entry.client.tsx",
    },
  },
  vite: {
    plugins: [
      tsConfigPaths(),
      checker({
        typescript: true,
        overlay: true,
      }),
      Terminal(),
    ],
    resolve: {
      alias: {
        "@": "src",
      },
    },
  },
  tsr: {
    routesDirectory: "src/routes",
    appDirectory: "src",
    generatedRouteTree: "src/routeTree.gen.ts",
  },
});

const routers = tanstackApp.config.routers.map((r) => {
  return {
    ...r,
    // Attempt to inject env middleware for server routes
    middleware:
      r.target === "server"
        ? "src/lib/server/middleware/requests/env.server.ts"
        : undefined,
  };
});

const app: App = {
  ...tanstackApp,
  config: {
    ...tanstackApp.config,
    routers: routers,
  },
};

export default app;

env.server.ts (context injection middleware)

import { defineMiddleware } from "@tanstack/start/server";
import { createServerEnv } from "@/lib/server/env/env.server";

export default defineMiddleware({
  onRequest: async (event) => {
    const runtimeEnv = getRuntimeEnv();
    event.context.env = createServerEnv(runtimeEnv);
  },
});

function getRuntimeEnv() {
  const cfContext = getContext("cloudflare");
  if (cfContext && cfContext.env) {
    return cfContext.env;
  } else {
    return process.env;
  }
}

entry.server.ts (SSR entry point)

import { getRouterManifest } from "@tanstack/start/router-manifest";
import {
  createStartHandler,
  defaultStreamHandler,
} from "@tanstack/start/server";

import { createRouter } from "@/router";

export default createStartHandler({
  createRouter,
  getRouterManifest,
})(defaultStreamHandler);

entry.api.ts (API server entry point)

import {
  createStartAPIHandler,
  defaultAPIFileRouteHandler,
} from "@tanstack/start/api";

export default createStartAPIHandler(defaultAPIFileRouteHandler);

session.server.ts (auth/session middleware for TanStack start server functions)

import { createMiddleware } from "@tanstack/start";
import { getEvent, getWebRequest } from "@tanstack/start/server";

import { createAuth } from "@/lib/server/auth/auth.server";

export const sessionMiddleware = createMiddleware().server(async ({ next }) => {
  const request = getWebRequest();
  if (!request) {
    throw new Error("Request not found");
  }

  const event = getEvent();
  const auth = createAuth(event.context.env);
  const session = await auth.api.getSession({ headers: request.headers });

  const isAuthenticated = !!session;

  return next({
    context: {
      auth,
      isAuthenticated,
    },
  });
});

$.ts (API routes for auth redirect)

import { createAPIFileRoute } from "@tanstack/start/api";
import { getEvent } from "@tanstack/start/server";

import { createAuth } from "@/lib/server/auth/auth.server";

export const APIRoute = createAPIFileRoute("/api/auth/$")({
  GET: ({ request }) => {
    const event = getEvent();
    const auth = createAuth(event.context.env);
    return auth.handler(request);
  },
  POST: ({ request }) => {
    const event = getEvent();
    const auth = createAuth(event.context.env);
    return auth.handler(request);
  },
});

package.json (with build command I'm using on Cloudflare)

{
   ...
  "scripts": {
    "build:cloudflare": "vinxi build --preset cloudflare-pages",
     ....
  },

wrangler.toml (for CF deployment)

name = "sonoma"
pages_build_output_dir = "./dist"
compatibility_flags = ["nodejs_compat"]
compatibility_date = "2024-11-13"

[vars]
VITE_BETTER_AUTH_URL = "https://url.com"
VITE_GRAPHQL_URL = "https://url.com"

Expected behavior

Expected Behavior

During SSR on Cloudflare, getContext("cloudflare") should contain the same environment bindings as API routes, so we can securely access secrets at runtime.

Actual Behavior

getContext("cloudflare") appears to return an empty result on SSR. The SSR pipeline does not receive the same Worker event, forcing fallback to build-time or process env.

Screenshots or Videos

No response

Platform

  • OS: macOS 15.1.1 (M1 Pro)
  • Node/Bun version: Bun or Node 18+
  • Cloudflare: Pages with Wrangler 3.107.3
  • Browser: Chrome 133.0.6943.55 (Official Build) (arm64)
  • TanStack Start Version: @tanstack/start@^1.101.2

Additional context

Additional Context: API Routes vs. SSR Routes

In our app, API routes correctly receive the Cloudflare environment bindings, while our SSR route server function does not. Below are two examples that illustrate the difference.

API Route Example (Works as Expected)

The following API route (e.g., in src/entry.api.ts) uses TanStack Start's API routing. In this code, when the handler is executed, the event.context.env is populated with the correct Cloudflare bindings (e.g., secrets and other environment variables).

import { createAPIFileRoute } from '@tanstack/start/api';
import { getEvent } from '@tanstack/start/server';
import { createAuth } from '@/lib/server/auth/auth.server';

export const APIRoute = createAPIFileRoute("/api/auth/$")({
  GET: ({ request }) => {
    const event = getEvent();
    console.log("API event context:", event.context);
    const auth = createAuth(event.context.env);
    return auth.handler(request);
  },
});

When you inspect the logs for this API route, you’ll see that getContext("cloudflare") (and consequently event.context.env) contains the expected Cloudflare secrets.

SSR Route / Server Function Example (Does Not Work as Expected)
import { createMiddleware } from "@tanstack/start";
import { getEvent, getWebRequest } from "@tanstack/start/server";

import { createAuth } from "@/lib/server/auth/auth.server";

export const sessionMiddleware = createMiddleware().server(async ({ next }) => {
  const request = getWebRequest();
  if (!request) {
    throw new Error("Request not found");
  }

  const event = getEvent();
  const auth = createAuth(event.context.env);
  const session = await auth.api.getSession({ headers: request.headers });

  const isAuthenticated = !!session;

  return next({
    context: {
      auth,
      isAuthenticated,
    },
  });
});

When you inspect the logs for this SSR route / Server Function, you’ll observe that the getContext("cloudflare") is empty.

Observed Logs

  • For API routes (e.g. api or a router with target: "server"), getContext("cloudflare") is populated with the correct Cloudflare secrets.
  • For SSR routes (e.g. ssr), the getContext("cloudflare") is empty.

Theories About the Underlying Cause

  • Nitro/TanStack SSR adapter: Possibly the SSR request is run in a separate context that doesn’t inherit the Worker’s event.env.
  • Cloudflare Pages Functions: On Cloudflare Pages, SSR might not have the same event shape as normal Worker routes.
  • No official pass-through: The TanStack Start SSR pipeline may not explicitly forward event.env to SSR.

Any guidance on how to ensure SSR receives the Cloudflare environment at runtime would be greatly appreciated!

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

No branches or pull requests

1 participant