T
TanStack•3mo ago
diverse-rose

ENV vars work at start of app but become unavailable?

I am using the following: - TanStack Start Router and Query - Generating Query options using Orval - Supabase for auth At the start of my dev server, I see no issues getting the env vars for Supabase (which I have a custom instance for fetching which injects the Supabase access_token for API) and am able to successfully list the projects on /dashboard But after a mutation and redirect back to the /dashboard page, the projects loader that worked at first now fails with:
No description
26 Replies
diverse-rose
diverse-roseOP•3mo ago
when I hard reload the /dashboard page:
No description
diverse-rose
diverse-roseOP•3mo ago
Its sucessful I have logged out on the "failed path" and confirm the the env vars become undefined.
import { parseCookies, setCookie } from "@tanstack/react-start/server";
import { createServerClient } from "@supabase/ssr";

export function getSupabaseServerClient() {
return createServerClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
cookies: {
// @ts-ignore Wait till Supabase overload works
getAll() {
return Object.entries(parseCookies()).map(([name, value]) => ({
name,
value,
}));
},
setAll(cookies) {
cookies.forEach((cookie) => {
setCookie(cookie.name, cookie.value);
});
},
},
}
);
}
import { parseCookies, setCookie } from "@tanstack/react-start/server";
import { createServerClient } from "@supabase/ssr";

export function getSupabaseServerClient() {
return createServerClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
cookies: {
// @ts-ignore Wait till Supabase overload works
getAll() {
return Object.entries(parseCookies()).map(([name, value]) => ({
name,
value,
}));
},
setAll(cookies) {
cookies.forEach((cookie) => {
setCookie(cookie.name, cookie.value);
});
},
},
}
);
}
This is how I am creating the client (copied from the Start docs)
diverse-rose
diverse-roseOP•3mo ago
No description
diverse-rose
diverse-roseOP•3mo ago
Here is the stack trace, when naviating it seems to happen
foreign-sapphire
foreign-sapphire•3mo ago
This was discussed some time ago. In your vite.config.ts
export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> => {

// Inject env vars into process.env
process.env = {
...process.env,
...loadEnv(mode, process.cwd(), ''),
}

// Force t3-oss/env-core to load env vars
// Or whatever you use for the env
await import('./src/env')
export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> => {

// Inject env vars into process.env
process.env = {
...process.env,
...loadEnv(mode, process.cwd(), ''),
}

// Force t3-oss/env-core to load env vars
// Or whatever you use for the env
await import('./src/env')
diverse-rose
diverse-roseOP•3mo ago
interesting, I didnt have a vite.config.ts Can you point to where it was discussed or where I can get the full config 🙂
foreign-sapphire
foreign-sapphire•3mo ago
are you on the latest alpha? This was an issue with the alpha, if not probably the wrong solution :p This was the discussion btw: https://discord.com/channels/719702312431386674/1371507529980444884
diverse-rose
diverse-roseOP•3mo ago
"@tanstack/react-start": "^1.120.15",
"@tanstack/start": "^1.120.15",
"@tanstack/react-start": "^1.120.15",
"@tanstack/start": "^1.120.15",
I think so?? Or maybe its still an issue lol it looked like from that thread you had some workwaround that was using the createEnv thing from t3?
optimistic-gold
optimistic-gold•3mo ago
where are you calling getSupabaseServerClient? this sounds to me like you're calling it from client code, which on the first load is ssr'd and has access to the server env but on subsequent navigation doesnt, since its client side
diverse-rose
diverse-roseOP•3mo ago
No description
diverse-rose
diverse-roseOP•3mo ago
@eastballz that is what it feels like too
optimistic-gold
optimistic-gold•3mo ago
you're calling it directly on a loader right?
diverse-rose
diverse-roseOP•3mo ago
I though that all requests would be SSRs tho
optimistic-gold
optimistic-gold•3mo ago
you need to wrap it in a serverFn and make sure you treat it as server only code
diverse-rose
diverse-roseOP•3mo ago
loader calls -> generated query wrapper from orval -> overridden client which called the getSupabaseServerClient
import { getSupabaseServerClient } from "../../auth/supabase/server";

export const serverCustomInstance = async <T>(config: any): Promise<T> => {
const supabase = await getSupabaseServerClient();

// Get auth token
const {
data: { session },
} = await supabase.auth.getSession();

let headers = {};
// Add auth header
if (session?.access_token) {
headers = {
...headers,
Authorization: `Bearer ${session.access_token}`,
};
}

// Use fetch with full URL
const response = await fetch(`${config.url}`, {
method: config.method,
headers: { ...config.headers, ...(headers || {}) },
body: config.data ? JSON.stringify(config.data) : undefined,
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

return response.json();
};
import { getSupabaseServerClient } from "../../auth/supabase/server";

export const serverCustomInstance = async <T>(config: any): Promise<T> => {
const supabase = await getSupabaseServerClient();

// Get auth token
const {
data: { session },
} = await supabase.auth.getSession();

let headers = {};
// Add auth header
if (session?.access_token) {
headers = {
...headers,
Authorization: `Bearer ${session.access_token}`,
};
}

// Use fetch with full URL
const response = await fetch(`${config.url}`, {
method: config.method,
headers: { ...config.headers, ...(headers || {}) },
body: config.data ? JSON.stringify(config.data) : undefined,
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

return response.json();
};
here is that overridden client that is calling getSupabaseServerClient I assume you mean the getSubabaseServerClient ?
optimistic-gold
optimistic-gold•3mo ago
well, all of this logic it seems. you need to have a clear understanding of whats server and client code. anything you call directly from a route (be it a loder or a component) is client code, ie it executes on the browser and therefore is insecure when you're interacting with things like your db, you need to do it exclusively in the server to not leak secrets etc
diverse-rose
diverse-roseOP•3mo ago
Thats fine if this is on the browser, its the public Supabase key and url
optimistic-gold
optimistic-gold•3mo ago
oh gotcha, my bad then
diverse-rose
diverse-roseOP•3mo ago
No worries What is strange is the loader seems to be server side at first and then becomes client, is that true?
optimistic-gold
optimistic-gold•3mo ago
if you're 100% fine with this running on the browser, then the issue is simply that you need to rely on vite's import.meta.env to access your environment. process.env is only available server side bear in mind vite's env is baked at build time, not runtime and yes, loaders run server side on the initial load, but on every subsequent navigation they run on the client
diverse-rose
diverse-roseOP•3mo ago
okay then this brings up a interesting question, for supabase there is a browser client and server client that means you would have to dynamically switch between them in the loader
import { createBrowserClient } from "@supabase/ssr";

export const getSupabaseBrowserClient = () => {
return createBrowserClient(
import.meta.env.VITE_SUPABASE_URL!,
import.meta.env.VITE_SUPABASE_ANON_KEY!
);
};
import { createBrowserClient } from "@supabase/ssr";

export const getSupabaseBrowserClient = () => {
return createBrowserClient(
import.meta.env.VITE_SUPABASE_URL!,
import.meta.env.VITE_SUPABASE_ANON_KEY!
);
};
and
/// <reference types="vinxi/types/server" />
import { parseCookies, setCookie } from "@tanstack/react-start/server";
import { createServerClient } from "@supabase/ssr";

export function getSupabaseServerClient() {
return createServerClient(
import.meta.env.VITE_SUPABASE_URL!,
import.meta.env.VITE_SUPABASE_ANON_KEY!,
{
cookies: {
// @ts-ignore Wait till Supabase overload works
getAll() {
return Object.entries(parseCookies()).map(([name, value]) => ({
name,
value,
}));
},
setAll(cookies) {
cookies.forEach((cookie) => {
setCookie(cookie.name, cookie.value);
});
},
},
}
);
}
/// <reference types="vinxi/types/server" />
import { parseCookies, setCookie } from "@tanstack/react-start/server";
import { createServerClient } from "@supabase/ssr";

export function getSupabaseServerClient() {
return createServerClient(
import.meta.env.VITE_SUPABASE_URL!,
import.meta.env.VITE_SUPABASE_ANON_KEY!,
{
cookies: {
// @ts-ignore Wait till Supabase overload works
getAll() {
return Object.entries(parseCookies()).map(([name, value]) => ({
name,
value,
}));
},
setAll(cookies) {
cookies.forEach((cookie) => {
setCookie(cookie.name, cookie.value);
});
},
},
}
);
}
that seems a bit strange, you would have to detect what env you are running in and return that correct supabase client?
optimistic-gold
optimistic-gold•3mo ago
unfortunately im not familiar with supabase (as you can tell from my previous message) and how you're expected to use it, so thats all i can help 😅
diverse-rose
diverse-roseOP•3mo ago
You can remove supbase from the picture, now its about getting envs from the different envirments it seems dynamically if on client getting from import.meta.env.VITE_SUPABASE_URL!, import.meta.env.VITE_SUPABASE_ANON_KEY! or server from process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, But thanks for your help, you are 100% sure that the loader gets called server side first and then client after?
optimistic-gold
optimistic-gold•3mo ago
yes, 100% on the last part as for the env question, personally i use t3-env to deal with all this. without it, you can do a few things like have a function that returns a different value based in where its running, or i believe createIsomorphicFn does it for you, where you pass a server and client implementation you'll have to search that here on the discord for details, i dont think its yet documented (createIsomorphicFn)
diverse-rose
diverse-roseOP•3mo ago
okay! I ended up doing this and it worked!
import { getSupabaseBrowserClient } from "@/lib/auth/supabase/browser";
import { getSupabaseServerClient } from "../../auth/supabase/server";
import { isServer } from "@tanstack/react-query";

export const customInstance = async <T>(config: any): Promise<T> => {
let access_token: string | undefined;

if (isServer) {
const supabase = await getSupabaseServerClient();
const {
data: { session },
} = await supabase.auth.getSession();

access_token = session?.access_token;
} else {
const supabase = await getSupabaseBrowserClient();
const {
data: { session },
} = await supabase.auth.getSession();
access_token = session?.access_token;
}

let headers = {};
// Add auth header
if (access_token) {
headers = {
...headers,
Authorization: `Bearer ${access_token}`,
};
}

// Use fetch with full URL
const response = await fetch(`${config.url}`, {
method: config.method,
headers: { ...config.headers, ...(headers || {}) },
body: config.data ? JSON.stringify(config.data) : undefined,
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

return response.json();
};
import { getSupabaseBrowserClient } from "@/lib/auth/supabase/browser";
import { getSupabaseServerClient } from "../../auth/supabase/server";
import { isServer } from "@tanstack/react-query";

export const customInstance = async <T>(config: any): Promise<T> => {
let access_token: string | undefined;

if (isServer) {
const supabase = await getSupabaseServerClient();
const {
data: { session },
} = await supabase.auth.getSession();

access_token = session?.access_token;
} else {
const supabase = await getSupabaseBrowserClient();
const {
data: { session },
} = await supabase.auth.getSession();
access_token = session?.access_token;
}

let headers = {};
// Add auth header
if (access_token) {
headers = {
...headers,
Authorization: `Bearer ${access_token}`,
};
}

// Use fetch with full URL
const response = await fetch(`${config.url}`, {
method: config.method,
headers: { ...config.headers, ...(headers || {}) },
body: config.data ? JSON.stringify(config.data) : undefined,
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

return response.json();
};
i assume createIsomorphicFn is the better way to do this thanks so much
rising-crimson
rising-crimson•3mo ago
Hi ! How about when I use the vite preset with t3 env, i get that MODE, DEV, PROD, and SSR are not defined, even though they should automatically be defined by vite
No description
No description
No description

Did you find this page helpful?