diff --git a/examples/react/start-supabase-basic/README.md b/examples/react/start-supabase-basic/README.md new file mode 100644 index 00000000000..f69185a7c99 --- /dev/null +++ b/examples/react/start-supabase-basic/README.md @@ -0,0 +1,196 @@ +# TanStack Start + Supabase Basic Example + +This example demonstrates how to integrate Supabase authentication with TanStack Start. + +## Setup + +1. Create a Supabase project at https://supabase.com +2. Copy `.env` and fill in your Supabase credentials: + ``` + SUPABASE_URL=your-project-url + SUPABASE_ANON_KEY=your-anon-key + ``` +3. Install dependencies: `npm install` +4. Run: `npm run dev` + +## ⚠️ Important: Server Function Serialization + +**CRITICAL**: Server functions in TanStack Start can only return serializable data. + +### The Problem + +Supabase returns rich objects with non-serializable properties: + +```typescript +const { data } = await supabase.auth.getUser() +// data.user contains functions, metadata, internal state +``` + +### The Solution + +Extract only primitive values: + +```typescript +✅ CORRECT: +return { + email: data.user.email, // string ✅ + id: data.user.id, // string ✅ + role: data.user.role, // string ✅ +} + +❌ WRONG: +return data.user // Contains functions and metadata ❌ +``` + +### What's Serializable? + +| Type | Serializable? | Example | +|------|---------------|---------| +| String | ✅ Yes | `"hello"` | +| Number | ✅ Yes | `42` | +| Boolean | ✅ Yes | `true` | +| null | ✅ Yes | `null` | +| Plain Object | ✅ Yes | `{ name: "Alice" }` | +| Array | ✅ Yes | `[1, 2, 3]` | +| ISO String | ✅ Yes | `new Date().toISOString()` | +| undefined | ❌ No | Use `null` instead | +| Function | ❌ No | Cannot serialize | +| Class Instance | ❌ No | Extract fields | +| Date Object | ❌ No | Convert to ISO string | + +## Common Errors & Solutions + +### Error: "Cannot serialize function" +**Cause**: Returning an object with methods +**Fix**: Extract only primitive values + +```typescript +// ❌ Wrong +return data.user + +// ✅ Correct +return { + email: data.user.email, + id: data.user.id, +} +``` + +### Error: "Cannot serialize undefined" +**Cause**: A field is `undefined` instead of `null` +**Fix**: Use `null` or omit the field entirely + +```typescript +// ❌ Wrong +return { + name: data.user.name, // might be undefined +} + +// ✅ Correct +return { + name: data.user.name ?? null, // convert undefined to null +} +``` + +### Error: React Hydration Mismatch +**Cause**: Server and client render different HTML +**Fix**: Ensure consistent data structure between server/client + +## Project Structure + +``` +src/ +├── routes/ +│ ├── __root.tsx # Root layout with user fetching +│ ├── _authed.tsx # Protected route layout +│ ├── index.tsx # Home page +│ ├── login.tsx # Login page +│ ├── signup.tsx # Signup page +│ └── _authed/ +│ └── posts.$postId.tsx +├── components/ +│ ├── Auth.tsx # Authentication UI +│ └── Login.tsx +├── utils/ +│ ├── supabase.ts # Supabase client config +│ └── posts.ts +└── styles/ + └── app.css +``` + +## How It Works + +### 1. User Authentication Check (`__root.tsx`) +The root route uses a server function to check authentication: + +```typescript +const fetchUser = createServerFn({ method: 'GET' }).handler(async () => { + const supabase = getSupabaseServerClient() + const { data, error } = await supabase.auth.getUser() + + if (error || !data.user?.email) { + return null + } + + // IMPORTANT: Only return serializable fields! + return { + email: data.user.email, + } +}) +``` + +### 2. Protected Routes (`_authed.tsx`) +Routes prefixed with `_authed` redirect unauthenticated users to login: + +```typescript +beforeLoad: ({ context }) => { + if (!context.user) { + throw redirect({ to: '/login' }) + } +} +``` + +### 3. Supabase Client (`utils/supabase.ts`) +Server-side Supabase client with cookie handling: + +```typescript +export function getSupabaseServerClient() { + return createServerClient( + process.env.SUPABASE_URL!, + process.env.SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { /* ... */ }, + setAll(cookies) { /* ... */ }, + }, + }, + ) +} +``` + +## Learn More + +- [TanStack Start Documentation](https://tanstack.com/start/latest) +- [TanStack Start Server Functions](https://tanstack.com/start/latest/docs/framework/react/server-functions) +- [Supabase Auth Documentation](https://supabase.com/docs/guides/auth) +- [Supabase SSR Guide](https://supabase.com/docs/guides/auth/server-side-rendering) + +## Troubleshooting + +### Authentication not persisting +Make sure cookies are properly configured in `utils/supabase.ts` + +### TypeScript errors +Run `npm run build` to check for type errors + +### Can't connect to Supabase +Verify your `.env` file has correct credentials + +## Example Deployment + +This example works with: +- ✅ Cloudflare Pages +- ✅ Netlify +- ✅ Vercel +- ✅ Node.js servers + +See deployment guides in the TanStack Start documentation. diff --git a/examples/react/start-supabase-basic/src/routes/__root.tsx b/examples/react/start-supabase-basic/src/routes/__root.tsx index 551c57e8216..342a6bbaeda 100644 --- a/examples/react/start-supabase-basic/src/routes/__root.tsx +++ b/examples/react/start-supabase-basic/src/routes/__root.tsx @@ -15,16 +15,64 @@ import appCss from '../styles/app.css?url' import { seo } from '../utils/seo' import { getSupabaseServerClient } from '../utils/supabase' +/** + * ⚠️ IMPORTANT: Server Function Serialization Requirements + * + * Server functions in TanStack Start can ONLY return serializable data. + * This means data that can be converted to JSON and sent to the client. + * + * Supabase's `data.user` object contains NON-serializable properties: + * - Functions (e.g., user.toString, internal methods) + * - Complex metadata objects with circular references + * - Internal Supabase client state + * + * ❌ WRONG - This will cause "Cannot serialize function" errors: + * ``` + * return data.user // Contains functions and complex objects + * ``` + * + * ✅ CORRECT - Extract only primitive values: + * ``` + * return { + * email: data.user.email, // string ✅ + * id: data.user.id, // string ✅ + * role: data.user.role, // string ✅ + * } + * ``` + * + * What's serializable? + * - ✅ Primitives: string, number, boolean, null + * - ✅ Plain objects: { key: value } + * - ✅ Arrays: [1, 2, 3] + * - ❌ Functions, class instances, undefined + * - ❌ Date objects (convert to ISO string: date.toISOString()) + * + * Learn more: + * - Server Functions: https://tanstack.com/router/latest/docs/framework/react/start/server-functions + * - Supabase SSR: https://supabase.com/docs/guides/auth/server-side-rendering + */ const fetchUser = createServerFn({ method: 'GET' }).handler(async () => { const supabase = getSupabaseServerClient() - const { data, error: _error } = await supabase.auth.getUser() + const { data, error } = await supabase.auth.getUser() + + // Always handle errors explicitly - don't suppress them + if (error) { + console.error('[fetchUser] Supabase auth error:', error.message) + return null + } if (!data.user?.email) { return null } + // IMPORTANT: Only return serializable fields from the user object + // Add more fields here as needed (all must be primitives or plain objects) return { email: data.user.email, + // You can safely add more primitive fields: + // id: data.user.id, + // role: data.user.role, + // name: data.user.user_metadata?.name ?? null, } })