T
TanStack7mo ago
quickest-silver

Cloudflare Pages prod env vars inconsistently retrieved

Hi folks, I've seen a number of threads related to Cloudflare env variables and a number of example GitHub repos, but I'm still seeing inconsistent behavior when deployed to Cloudflare, specifically: 1. Public env vars like import.meta.env.VITE_BETTER_AUTH_URL that appear populated in the Cloudflare console for the worker are undefined at execution time 2. getContext("cloudflare") seems to inconsistently return undefined in general which creates bugs in my application Any idea what I'm doing wrong? I've pasted some code below and attached some files (Please let me know if I can provide any additional context).
// app/src/lib/server/middleware/session.server.ts

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,
},
});
});
// app/src/lib/server/middleware/session.server.ts

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,
},
});
});
// app/app.config.ts

const tanstackApp = defineConfig({
....
});

const routers = tanstackApp.config.routers.map((r) => {
return {
...r,
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;
// app/app.config.ts

const tanstackApp = defineConfig({
....
});

const routers = tanstackApp.config.routers.map((r) => {
return {
...r,
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;
// app/src/lib/server/middleware/requests/env.server.ts

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;
}
}
// app/src/lib/server/middleware/requests/env.server.ts

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;
}
}
22 Replies
quickest-silver
quickest-silverOP7mo ago
Okay, update, as I was posting this, I noticed my use of async in my middleware function and removed it and now results in getContext("cloudflare") reliabily returning data: For anyone, curious, this was the change:
iff --git a/app/src/lib/server/middleware/session.server.ts b/app/src/lib/server/middleware/session.server.ts
index 1927752..0371514 100644
--- a/app/src/lib/server/middleware/session.server.ts
+++ b/app/src/lib/server/middleware/session.server.ts
@@ -3,7 +3,7 @@ import { getEvent, getWebRequest } from "@tanstack/start/server";

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

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

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

const isAuthenticated = !!session;
iff --git a/app/src/lib/server/middleware/session.server.ts b/app/src/lib/server/middleware/session.server.ts
index 1927752..0371514 100644
--- a/app/src/lib/server/middleware/session.server.ts
+++ b/app/src/lib/server/middleware/session.server.ts
@@ -3,7 +3,7 @@ import { getEvent, getWebRequest } from "@tanstack/start/server";

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

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

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

const isAuthenticated = !!session;
Okay and public / client side env vars seem to be working now too. No further action necessary. Will leave this post here for any poor souls dealing with CF env var issues.
deep-jade
deep-jade7mo ago
Sorry to yoink this thread @stcobbe but I'm having a similar issue where cf pages isn't reading in any env vars. Been hacking on it all day to no avail. Am I correct in you using Better Auth as well? I'm just curious if you could share how you're creating the db instance or getting access to other env vars? I'm using drizzle with better auth and trying to deploy start to cf pages
rival-black
rival-black7mo ago
THANK YOU. I had a project working several versions ago and then everything stopped after I updated it. Excited to try this out and hopefully get it back online.
quickest-silver
quickest-silverOP7mo ago
@Jan Henning: sure thing, here's my rough structure (all referenced files attached): 1. Inject top level request middleware in app/app.config.ts. 2. Parse env vars either from cloudflare context or fallback to node context and then re-attach to request context in app/src/lib/server/middleware/requests/env.server.ts 3. Validate env vars (currently have turned off because I'm still getting some env var flakiness with CF) in app/src/lib/server/env/env.server.ts 4a. Server function middleware creates a BetterAuth auth object using the injected env context in app/src/lib/server/middleware/session.server.ts 4b. BetterAuth object created in app/src/lib/server/auth/auth.server.ts 4c. Server function middleware chain validates the auth object created in session in app/src/lib/server/middleware/auth.server.ts 5a. BetterAuth redirect handlers create a BetterAuth auth object using the injected env context in app/src/routes/api/auth/$.ts 5b. BetterAuth object created in app/src/lib/server/auth/auth.server.ts I think that should be everything. Let me know if you have any questions! (also, not totally sure this code is totally correct, please let me know if you spot any issues)
deep-jade
deep-jade7mo ago
Thank you 🙏 Would you mind sharing how your env schema file is structured also, and how you init the db to also be given the correct db url?
quickest-silver
quickest-silverOP7mo ago
- Our env schema file is in app/src/lib/shared/env/schema.ts - We're using Dgraph for our database and we initialize the db client in app/src/graphql/client.ts
quickest-silver
quickest-silverOP7mo ago
Pls lmk if you have any questions
quickest-silver
quickest-silverOP7mo ago
Sorry here's the db client file
deep-jade
deep-jade7mo ago
Thank you! I will try this a bit later. Usually I'm stuck on the part of creating a db and provide the correct context for the db url since everything has to be created such a pain working with cloudlare which is so unfortunate :SadgeBusiness:
quickest-silver
quickest-silverOP7mo ago
I feel you, it was a brutal experience for me. Post here if you have any issues!
deep-jade
deep-jade7mo ago
Sorry for the spam on this, but I appreciate all the help. I'm still a bit stuck on the initializing the db correctly. Do you have any idea how I could change my current db init to fit and get the correct env? I'm also realizing it's going to be a lot of calls to getEvent, createAuth etc just to get access to stuff. It's definitely a mess right now, but only way I found it working since pg doesn't work on CF, while Neon didn't work locally due to ws
import { Pool as NeonPool, neonConfig } from "@neondatabase/serverless"
import { drizzle as drizzleNeon } from "drizzle-orm/neon-serverless"
import { drizzle as drizzlePg } from "drizzle-orm/node-postgres"
import ws from "ws"
import * as relations from "../../drizzle/relations"
import * as schema from "../../drizzle/schema"

const connectionString = `${process.env.DATABASE_URL!}`

type DB =
| ReturnType<typeof drizzleNeon<typeof schema & typeof relations>>
| ReturnType<typeof drizzlePg<typeof schema & typeof relations>>

let db: DB

if (import.meta.env.DEV) {
import("pg").then(({ default: pg }) => {
const client = new pg.Pool({ connectionString })
db = drizzlePg(client, { schema: { ...schema, ...relations } })
})
} else {
neonConfig.webSocketConstructor = ws
const client = new NeonPool({ connectionString })
db = drizzleNeon(client, { schema: { ...schema, ...relations } })
}

export { db }
import { Pool as NeonPool, neonConfig } from "@neondatabase/serverless"
import { drizzle as drizzleNeon } from "drizzle-orm/neon-serverless"
import { drizzle as drizzlePg } from "drizzle-orm/node-postgres"
import ws from "ws"
import * as relations from "../../drizzle/relations"
import * as schema from "../../drizzle/schema"

const connectionString = `${process.env.DATABASE_URL!}`

type DB =
| ReturnType<typeof drizzleNeon<typeof schema & typeof relations>>
| ReturnType<typeof drizzlePg<typeof schema & typeof relations>>

let db: DB

if (import.meta.env.DEV) {
import("pg").then(({ default: pg }) => {
const client = new pg.Pool({ connectionString })
db = drizzlePg(client, { schema: { ...schema, ...relations } })
})
} else {
neonConfig.webSocketConstructor = ws
const client = new NeonPool({ connectionString })
db = drizzleNeon(client, { schema: { ...schema, ...relations } })
}

export { db }
quickest-silver
quickest-silverOP7mo ago
@Jan Henning what error are you getting / unexpected behavior are you seeing?
deep-jade
deep-jade7mo ago
Most of it is just around the db initialization. I'm trying to use Neon on Cloudflare but since that package doesnt work locally, I need to use pg package locally. CF doesn't support pg and gives error unless I import pg inline like in my example above. Tbh I may just deploy to a node server instead to drop this, but I'd really like to use CF because it'll integrate very nicely with other services I'm using there
quickest-silver
quickest-silverOP7mo ago
Got it. Does pg work locally? What Neon errors are you seeing on CF?
deep-jade
deep-jade7mo ago
pg works locally, neon does not. Neon works on cf, pg does not. The above db.ts file solves it with importing pg inline in dev as cf will throw some pg-cloudflare and cloudflare:socket errors if its imported at the top. The deployment works with the changes you showed, but no env vars are being read in still. Both for the db and auth. Most of my setup is similar to yours. For example, trying to use the db returns
No database host or connection string was set, and key parameters have default values (host: localhost, user: undefined, db: undefined, password: null). Is an environment variable missing? Alternatively, if you intended to connect with these parameters, please set the host to 'localhost' explicitly.
No database host or connection string was set, and key parameters have default values (host: localhost, user: undefined, db: undefined, password: null). Is an environment variable missing? Alternatively, if you intended to connect with these parameters, please set the host to 'localhost' explicitly.
and trying to log in/sign up returns Better Auth errors like
[Better Auth]: You are using the default secret. Please set BETTER_AUTH_SECRET in your environment variables or pass secret in your auth config.
[Better Auth]: You are using the default secret. Please set BETTER_AUTH_SECRET in your environment variables or pass secret in your auth config.
and
Social provider discord is missing clientId or clientSecret
Social provider discord is missing clientId or clientSecret
Bit of a mess currently, but just trying to get it working at first
// lib/server/db/db.server.ts
import * as relations from "@/drizzle/relations"
import * as schema from "@/drizzle/schema"
import type { ServerEnv } from "@/lib/shared/env/schema"

// Split imports based on environment
// These will be tree-shaken based on the environment
import { Pool as NeonPool, neonConfig } from "@neondatabase/serverless"
import { drizzle as drizzleNeon } from "drizzle-orm/neon-serverless"
import { drizzle as drizzlePg } from "drizzle-orm/node-postgres"
import type { Pool as PgPool } from "pg"
import ws from "ws"

// Declare pg at module level but only import in dev
declare const pg: { Pool: typeof PgPool }
if (import.meta.env.DEV) {
// This import will be removed in production builds
import("pg").then(module => Object.assign(globalThis, { pg: module.default }))
}

type DB =
| ReturnType<typeof drizzleNeon<typeof schema & typeof relations>>
| ReturnType<typeof drizzlePg<typeof schema & typeof relations>>

export function createDb(serverEnv: ServerEnv): DB {
if (import.meta.env.DEV) {
// biome-ignore lint/suspicious/noExplicitAny: Allow any
const client = new (globalThis as any).pg.Pool({
connectionString: serverEnv.DATABASE_URL
})
return drizzlePg(client, { schema: { ...schema, ...relations } })
}

neonConfig.webSocketConstructor = ws
const client = new NeonPool({ connectionString: serverEnv.DATABASE_URL })
return drizzleNeon(client, { schema: { ...schema, ...relations } })
}
// lib/server/db/db.server.ts
import * as relations from "@/drizzle/relations"
import * as schema from "@/drizzle/schema"
import type { ServerEnv } from "@/lib/shared/env/schema"

// Split imports based on environment
// These will be tree-shaken based on the environment
import { Pool as NeonPool, neonConfig } from "@neondatabase/serverless"
import { drizzle as drizzleNeon } from "drizzle-orm/neon-serverless"
import { drizzle as drizzlePg } from "drizzle-orm/node-postgres"
import type { Pool as PgPool } from "pg"
import ws from "ws"

// Declare pg at module level but only import in dev
declare const pg: { Pool: typeof PgPool }
if (import.meta.env.DEV) {
// This import will be removed in production builds
import("pg").then(module => Object.assign(globalThis, { pg: module.default }))
}

type DB =
| ReturnType<typeof drizzleNeon<typeof schema & typeof relations>>
| ReturnType<typeof drizzlePg<typeof schema & typeof relations>>

export function createDb(serverEnv: ServerEnv): DB {
if (import.meta.env.DEV) {
// biome-ignore lint/suspicious/noExplicitAny: Allow any
const client = new (globalThis as any).pg.Pool({
connectionString: serverEnv.DATABASE_URL
})
return drizzlePg(client, { schema: { ...schema, ...relations } })
}

neonConfig.webSocketConstructor = ws
const client = new NeonPool({ connectionString: serverEnv.DATABASE_URL })
return drizzleNeon(client, { schema: { ...schema, ...relations } })
}
// lib/server/auth/auth.server.ts
import { betterAuth } from "better-auth"

import type { ServerEnv } from "@/lib/shared/env/schema"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { admin } from "better-auth/plugins"
import { createDb } from "../db/db.server"

// Factory function that creates auth instance with provided env
export function createAuth(serverEnv: ServerEnv) {
const db = createDb(serverEnv)

return betterAuth({
secret: serverEnv.BETTER_AUTH_SECRET,
database: drizzleAdapter(db, {
provider: "pg"
}),
account: {
accountLinking: {
enabled: true,
trustedProviders: ["discord", "google"]
}
},
socialProviders: {
google: {
clientId: serverEnv.GOOGLE_CLIENT_ID,
clientSecret: serverEnv.GOOGLE_CLIENT_SECRET
},
discord: {
clientId: serverEnv.DISCORD_CLIENT_ID,
clientSecret: serverEnv.DISCORD_CLIENT_SECRET
}
},
plugins: [admin()]
})
}

export type Session = ReturnType<typeof createAuth>["$Infer"]["Session"]
export type User = Session["user"]
// lib/server/auth/auth.server.ts
import { betterAuth } from "better-auth"

import type { ServerEnv } from "@/lib/shared/env/schema"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { admin } from "better-auth/plugins"
import { createDb } from "../db/db.server"

// Factory function that creates auth instance with provided env
export function createAuth(serverEnv: ServerEnv) {
const db = createDb(serverEnv)

return betterAuth({
secret: serverEnv.BETTER_AUTH_SECRET,
database: drizzleAdapter(db, {
provider: "pg"
}),
account: {
accountLinking: {
enabled: true,
trustedProviders: ["discord", "google"]
}
},
socialProviders: {
google: {
clientId: serverEnv.GOOGLE_CLIENT_ID,
clientSecret: serverEnv.GOOGLE_CLIENT_SECRET
},
discord: {
clientId: serverEnv.DISCORD_CLIENT_ID,
clientSecret: serverEnv.DISCORD_CLIENT_SECRET
}
},
plugins: [admin()]
})
}

export type Session = ReturnType<typeof createAuth>["$Infer"]["Session"]
export type User = Session["user"]
This works fine locally, but still not being read in on CF. I do have vars/secrets added there as well
quickest-silver
quickest-silverOP7mo ago
Thanks @Jan Henning. Can you log the output of const cfContext = getContext("cloudflare"); and check the result in the CF logs? Can you also confirm the handler that's running your logic isn't doing so async (which was the problem I was having, and which means the CF env vars aren't guaranteed to be available)?
deep-jade
deep-jade7mo ago
It's possible, as I added a global session to the context as well which is async. My app relies heavily on the current global middleware
import { defineMiddleware } from "vinxi/http"

import { auth } from "@/server/auth"

export default defineMiddleware({
onRequest: async event => {
const session = await auth.api.getSession({
headers: event.headers
})

event.context.auth =
session !== null
? { isAuthenticated: true, ...session }
: { isAuthenticated: false }
}
})
import { defineMiddleware } from "vinxi/http"

import { auth } from "@/server/auth"

export default defineMiddleware({
onRequest: async event => {
const session = await auth.api.getSession({
headers: event.headers
})

event.context.auth =
session !== null
? { isAuthenticated: true, ...session }
: { isAuthenticated: false }
}
})
So I'm a bit curious, how are you accessing the session in routes currently? The above is my old global mw btw, which i just moved into the one that adds runtimeEnv to it Actually now before I got to try removing the async, a new thing popped up. Was working fine last night but I'm getting type issues in app.config.ts Property 'config' does not exist on type 'Promise<App>'.ts(2339). Edit: Actually seems to be caused by v1.103.0 and introduction of static server functions as the type was changed to Promise<App> instead.
rival-black
rival-black7mo ago
I had the same issue. Adding await to the config seemed to fix that error, but it ended up bundling the node_modules directory in my worker so idk what the fix is. I put it down for now.
quickest-silver
quickest-silverOP7mo ago
@alrightsure @Jan Henning my async "fix" was a red herring and didn't actually solve anything. I did some more deep diving and filed a GH issue here: https://github.com/TanStack/router/issues/3468
GitHub
Bug Report: Cloudflare Env Vars Not Passed to SSR in TanStack Start...
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 (event.context.cloudflare.env...
quickest-silver
quickest-silverOP7mo ago
Can you upvote it or add any context to comments?
deep-jade
deep-jade7mo ago
gave it an upboot. i can see if i can add some context later. tbh ive given up and just going to build to node and hope nitro/vinxi or whatever is the cause makes it easier in the future so I can move over.
grumpy-cyan
grumpy-cyan7mo ago
Sorry, I'm going crazy on a different issue and I saw you're able to deploy on CF pages with tanstack v1.102, when I'm failing whenever I upgrade above 1.81. Could you please share the other dependencies you're using?
GitHub
Code Splitting Issues with TanStack Start and Cloudflare Pages Depl...
Which project does this relate to? Start Describe the bug I'm experiencing issues with code splitting when deploying a TanStack Start project to Cloudflare Pages. The application appears to be ...

Did you find this page helpful?