mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
docs(tanstack): add a proper SSR quick start with cookie-based auth (#48105)
## Summary TanStack Start's quickstart only ever wired up an anonymous `supabase-js` client — no cookies, no `@supabase/ssr`, no auth. This ports the real `@supabase/ssr` client/server split and password-based auth flow (already shipped in `apps/ui-library`) into the quickstart and adds a matching tab to the SSR guide. ## Where this changed - `apps/docs/content/guides/getting-started/quickstarts/tanstack.mdx` — quickstart now installs the cookie-based auth flow via the Supabase UI Library registry and queries data through the SSR-aware server client. - `apps/docs/content/guides/auth/server-side/creating-a-client.mdx` — new TanStack Start tab (client/server setup + protecting routes). - `examples/auth/tanstack/` (new) — source files backing the `$CodeSample` snippets above, ported from `apps/ui-library`'s registry. ## Test plan - [x] Scaffolded a real TanStack Start app and ran the quickstart commands end-to-end - [x] Confirmed SSR loader + protected-route redirect work as documented - [x] `pnpm lint:mdx` and `pnpm build:guides-markdown` pass <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added TanStack Start SSR setup examples for Supabase, including browser and server client helpers with cookie-based session support. * Included a protected route example that checks authentication on the server and redirects unauthenticated users to the login page. * Added a server-side claims fetch helper for authorization checks. * **Documentation** * Expanded the “creating a client” guide with TanStack Start-specific route protection and environment variable examples. * Updated the TanStack Start quickstart to use the official CLI and refined server-side authorization guidance. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
2d5ec97df8
commit
ac714e81ba
6 files changed
+211
-24
No files matched your search
@@ -159,6 +159,14 @@ SUPABASE_URL=supabase_project_url
|
||||
SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="tanstack" label="TanStack Start">
|
||||
|
||||
```bash .env.local
|
||||
VITE_SUPABASE_URL=supabase_project_url
|
||||
VITE_SUPABASE_PUBLISHABLE_KEY=supabase_publishable_key
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
@@ -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"
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="tanstack" label="TanStack Start">
|
||||
|
||||
### 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:
|
||||
|
||||
<div className="mt-12">
|
||||
<$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"
|
||||
/>
|
||||
</$CodeTabs>
|
||||
</div>
|
||||
|
||||
### 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`.
|
||||
|
||||
<Admonition type="danger">
|
||||
|
||||
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.
|
||||
|
||||
</Admonition>
|
||||
|
||||
`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.
|
||||
|
||||
<div className="mt-12">
|
||||
<$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"
|
||||
/>
|
||||
</$CodeTabs>
|
||||
</div>
|
||||
|
||||
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!
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
<Button variant="primary" asChild>
|
||||
<a href="/dashboard/project/_?showConnect=true">Open Connect panel</a>
|
||||
<a href="/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=tanstack">
|
||||
Open Connect panel
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
```text name=.env
|
||||
```text name=.env.local
|
||||
VITE_SUPABASE_URL=<SUBSTITUTE_SUPABASE_URL>
|
||||
VITE_SUPABASE_PUBLISHABLE_KEY=<SUBSTITUTE_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
|
||||
/// <reference types="vite/types/importMeta.d.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)
|
||||
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/types/importMeta.d.ts" />
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
|
||||
export function createClient() {
|
||||
return createBrowserClient(
|
||||
import.meta.env.VITE_SUPABASE_URL!,
|
||||
import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
})
|
||||
Reference in new issue
Block a user