diff --git a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx index b89c58b9463..ed252782fd5 100644 --- a/apps/docs/content/guides/auth/server-side/creating-a-client.mdx +++ b/apps/docs/content/guides/auth/server-side/creating-a-client.mdx @@ -159,6 +159,14 @@ SUPABASE_URL=supabase_project_url SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key ``` + + + +```bash .env.local +VITE_SUPABASE_URL=supabase_project_url +VITE_SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key +``` + @@ -510,7 +518,7 @@ export async function loader({ request }: LoaderFunctionArgs) { } ) - // Use `supabase` here for server-side work, e.g. await supabase.auth.getUser() + // Use `supabase` here for server-side work, e.g. await supabase.auth.getClaims() // Return the environment variables so the browser can create its own client. return json( @@ -674,7 +682,7 @@ export async function loader({ request }: LoaderFunctionArgs) { } ) - // Use `supabase` here for server-side work, e.g. await supabase.auth.getUser() + // Use `supabase` here for server-side work, e.g. await supabase.auth.getClaims() // Return the env vars so the browser can create its own client. return data( @@ -823,6 +831,81 @@ language="typescript" + + + +### Write utility functions to create Supabase clients + +TanStack Start renders matched routes on the server by default, so `beforeLoad` and `loader` run server-side on the initial request. Unlike Next.js, this means you don't need a proxy or middleware layer to keep sessions fresh — the server client reads and writes the session cookie directly on each request. + +Create a `lib/supabase` folder at the root of your project, or inside the `./src` folder if you are using one, then add a file for each type of client: + +1. **Create a browser client in `lib/supabase/client.ts`.** Use it to access Supabase from components that run in the browser. +2. **Create a server client in `lib/supabase/server.ts`.** Use it to access Supabase from loaders, server functions, and other code that runs only on the server. + +<$Partial path="auth_methods.mdx" /> + +Copy the lib utility functions below into each file: + +
+ <$CodeTabs> + <$CodeSample + path="/auth/tanstack/lib/supabase/client.ts" + meta="name=lib/supabase/client.ts" + language="typescript" + /> + <$CodeSample + path="/auth/tanstack/lib/supabase/server.ts" + meta="name=lib/supabase/server.ts" + language="typescript" + /> + +
+ +### Protecting routes + +TanStack Start has no global middleware layer, so protect each route explicitly. + +To protect your routes: + +1. Write a server function, `fetchClaims`, that calls `supabase.auth.getClaims()` and returns the claims, or `null` if the session isn't valid. +1. Call `fetchClaims` from a layout route's `beforeLoad` hook — for example, `_protected.tsx` — before any nested route renders, and redirect to `/login` when it returns `null`. + + + +Skipping the check inside the server function exposes private data to unauthenticated users. `beforeLoad` runs on the server for the initial request and on the client for later navigation, but either way it only gates the route's render — it doesn't stop the server function from being called directly. Because there's no proxy re-checking every request, the server function is the only checkpoint that always runs, so it must call `supabase.auth.getClaims()` to authorize the request itself. + + + +`getClaims()` validates the JWT signature on every call, the same check the Next.js Proxy relies on. Calling it inside the server function gives TanStack Start's per-route check that same guarantee, because the function runs on every request to a protected route. + +
+ <$CodeTabs> + <$CodeSample + path="/auth/tanstack/lib/supabase/fetch-claims-server-fn.ts" + meta="name=lib/supabase/fetch-claims-server-fn.ts" + language="typescript" + /> + <$CodeSample + path="/auth/tanstack/routes/_protected.tsx" + meta="name=routes/_protected.tsx" + language="typescript" + /> + +
+ +Any other server function that returns or mutates private data needs this same check. Don't rely on a route being nested under `_protected` alone. + +## Congratulations + +You're done! To recap, you've successfully: + +- Set up a Supabase client utility to call Supabase from a browser component. You can use this if you need to call Supabase from the browser, for example to set up a realtime subscription. +- Set up a server client utility to call Supabase from loaders and server functions. +- Protected a route with `beforeLoad`, backed by a server function that authorizes the request itself. + +You can now use any Supabase features from your client or server code! +
diff --git a/apps/docs/content/guides/getting-started/quickstarts/tanstack.mdx b/apps/docs/content/guides/getting-started/quickstarts/tanstack.mdx index b59f80d39ad..3fe6ac2a5da 100644 --- a/apps/docs/content/guides/getting-started/quickstarts/tanstack.mdx +++ b/apps/docs/content/guides/getting-started/quickstarts/tanstack.mdx @@ -11,7 +11,7 @@ breadcrumb: 'Framework Quickstarts' Create a TanStack Start app using the official CLI. ```bash -npm create @tanstack/start@latest my-app -- --package-manager npm --toolchain biome +npx @tanstack/cli@latest create my-app ``` ## 4. Install Agent Skills (optional) @@ -24,55 +24,87 @@ To install, run the following command in the root of your project: npx skills add supabase/agent-skills ``` -## 5. Install the Supabase client library +## 5. Install the Supabase client libraries -The fastest way to get started is to use the `supabase-js` client library which provides a convenient interface for working with Supabase from a TanStack Start app. - -Navigate to the TanStack Start app and install `supabase-js`. +Navigate to the TanStack Start app and install `supabase-js` and `@supabase/ssr`, the helper package that manages cookie-based sessions for server-side rendering. ```bash -cd my-app && npm install @supabase/supabase-js +cd my-app && npm install @supabase/supabase-js @supabase/ssr ``` ## 6. Declare Supabase environment variables -Create a `.env` file in the root of your project and populate with your Supabase connection variables that you can get from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true): +Create a `.env.local` file in the root of your project and populate it with your Supabase connection variables. Get the values from the helper below, or [from the project **Connect** panel](/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=tanstack). -```text name=.env +```text name=.env.local VITE_SUPABASE_URL= VITE_SUPABASE_PUBLISHABLE_KEY= ``` -<$Partial path="api_settings.mdx" variables={{ "framework": "", "tab": "" }} /> +<$Partial path="api_settings.mdx" variables={{ "framework": "tanstack", "tab": "frameworks" }} /> -## 7. Create a Supabase client utility +## 7. Create Supabase client utilities -Create a new file at `src/utils/supabase.ts` to initialize the Supabase client. +TanStack Start needs two Supabase clients: a browser client for components that run in the browser, and a server client for loaders and server functions. Create a `src/lib/supabase` folder with a file for each client. -```ts name=src/utils/supabase.ts -import { createClient } from '@supabase/supabase-js' +```ts name=src/lib/supabase/client.ts +/// +import { createBrowserClient } from '@supabase/ssr' -export const supabase = createClient( - import.meta.env.VITE_SUPABASE_URL, - import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY -) +export function createClient() { + return createBrowserClient( + import.meta.env.VITE_SUPABASE_URL!, + import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY! + ) +} ``` -## 8. Query data from the app +```ts name=src/lib/supabase/server.ts +import { createServerClient } from '@supabase/ssr' +import { getCookies, setCookie, setResponseHeader } from '@tanstack/react-start/server' -Replace the contents of `src/routes/index.tsx` with the following code to add a loader function that fetches the instruments data and displays it on the page. +export function createClient() { + return createServerClient( + process.env.VITE_SUPABASE_URL!, + process.env.VITE_SUPABASE_PUBLISHABLE_KEY!, + { + cookies: { + getAll() { + return Object.entries(getCookies()).map(([name, value]) => ({ name, value })) + }, + setAll(cookies, headers) { + cookies.forEach(({ name, value, options }) => { + setCookie(name, value, options) + }) + + Object.entries(headers).forEach(([name, value]) => { + setResponseHeader(name, value) + }) + }, + }, + } + ) +} +``` + +## 8. Query Supabase data from TanStack Start + +Replace the contents of `src/routes/index.tsx` with the following to add a loader that queries the `instruments` table through the server client. The loader runs on the server, so the data is part of the initial server-rendered response. ```tsx name=src/routes/index.tsx import { createFileRoute } from '@tanstack/react-router' -import { supabase } from '../utils/supabase' +import { createClient } from '@/lib/supabase/server' export const Route = createFileRoute('/')({ loader: async () => { + const supabase = createClient() const { data: instruments } = await supabase.from('instruments').select() return { instruments } }, @@ -102,7 +134,8 @@ npm run dev ## Next steps +- Learn how to [protect routes and check sessions](/docs/guides/auth/server-side/creating-a-client?queryGroups=framework&framework=tanstack) with the server client +- Set up a complete [login and sign-up flow](/ui/docs/tanstack/password-based-auth) from the Supabase UI Library - Explore [drop-in UI components](/ui) for your Supabase app -- Set up [Auth](/docs/guides/auth) for your app - [Insert more data](/docs/guides/database/import-data) into your database - Upload and serve static files using [Storage](/docs/guides/storage) diff --git a/examples/auth/tanstack/lib/supabase/client.ts b/examples/auth/tanstack/lib/supabase/client.ts new file mode 100644 index 00000000000..3f34921e9f6 --- /dev/null +++ b/examples/auth/tanstack/lib/supabase/client.ts @@ -0,0 +1,9 @@ +/// +import { createBrowserClient } from '@supabase/ssr' + +export function createClient() { + return createBrowserClient( + import.meta.env.VITE_SUPABASE_URL!, + import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY! + ) +} diff --git a/examples/auth/tanstack/lib/supabase/fetch-claims-server-fn.ts b/examples/auth/tanstack/lib/supabase/fetch-claims-server-fn.ts new file mode 100644 index 00000000000..69e151984e6 --- /dev/null +++ b/examples/auth/tanstack/lib/supabase/fetch-claims-server-fn.ts @@ -0,0 +1,14 @@ +import { createServerFn } from '@tanstack/react-start' + +import { createClient } from '@/lib/supabase/server' + +export const fetchClaims = createServerFn({ method: 'GET' }).handler(async () => { + const supabase = createClient() + const { data, error } = await supabase.auth.getClaims() + + if (error) { + return null + } + + return data.claims +}) diff --git a/examples/auth/tanstack/lib/supabase/server.ts b/examples/auth/tanstack/lib/supabase/server.ts new file mode 100644 index 00000000000..04ce35c6a28 --- /dev/null +++ b/examples/auth/tanstack/lib/supabase/server.ts @@ -0,0 +1,31 @@ +import { createServerClient } from '@supabase/ssr' +import { getCookies, setCookie, setResponseHeader } from '@tanstack/react-start/server' + +export function createClient() { + return createServerClient( + process.env.VITE_SUPABASE_URL!, + process.env.VITE_SUPABASE_PUBLISHABLE_KEY!, + { + cookies: { + getAll() { + return Object.entries(getCookies()).map( + ([name, value]) => + ({ + name, + value, + }) as { name: string; value: string } + ) + }, + setAll(cookies, headers) { + cookies.forEach(({ name, value, options }) => { + setCookie(name, value, options) + }) + + Object.entries(headers).forEach(([name, value]) => { + setResponseHeader(name, value) + }) + }, + }, + } + ) +} diff --git a/examples/auth/tanstack/routes/_protected.tsx b/examples/auth/tanstack/routes/_protected.tsx new file mode 100644 index 00000000000..7ce91c49366 --- /dev/null +++ b/examples/auth/tanstack/routes/_protected.tsx @@ -0,0 +1,17 @@ +import { createFileRoute, redirect } from '@tanstack/react-router' + +import { fetchClaims } from '@/lib/supabase/fetch-claims-server-fn' + +export const Route = createFileRoute('/_protected')({ + beforeLoad: async () => { + const claims = await fetchClaims() + + if (!claims) { + throw redirect({ to: '/login' }) + } + + return { + claims, + } + }, +})