diff --git a/examples/react/start-basic-better-auth/.env.example b/examples/react/start-basic-better-auth/.env.example new file mode 100644 index 0000000000..6fb0f1821f --- /dev/null +++ b/examples/react/start-basic-better-auth/.env.example @@ -0,0 +1,9 @@ +# Better Auth Configuration +BETTER_AUTH_SECRET=your-secret-key-here-min-32-chars-long + +# Better Auth URL (base URL, no /api/auth suffix) +BETTER_AUTH_URL=http://localhost:10000 + +# GitHub OAuth Configuration (https://github.com/settings/developers) +GITHUB_CLIENT_ID=your-github-client-id +GITHUB_CLIENT_SECRET=your-github-client-secret diff --git a/examples/react/start-basic-better-auth/.gitignore b/examples/react/start-basic-better-auth/.gitignore new file mode 100644 index 0000000000..3479e16707 --- /dev/null +++ b/examples/react/start-basic-better-auth/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.env +*.local +.DS_Store +.tanstack diff --git a/examples/react/start-basic-better-auth/README.md b/examples/react/start-basic-better-auth/README.md new file mode 100644 index 0000000000..fc15980da7 --- /dev/null +++ b/examples/react/start-basic-better-auth/README.md @@ -0,0 +1,66 @@ +# TanStack Start - Better Auth Example + +A TanStack Start example demonstrating authentication with Better Auth. + +- [TanStack Router Docs](https://tanstack.com/router) +- [Better Auth Documentation](https://www.better-auth.com/) + +## Start a new project based on this example + +To start a new project based on this example, run: + +```sh +npx gitpick TanStack/router/tree/main/examples/react/start-basic-better-auth start-basic-better-auth +``` + +## Setup + +This example requires environment variables for Better Auth configuration. Copy the `.env.example` file to `.env` and fill in your GitHub OAuth credentials: + +```sh +cp .env.example .env +``` + +### GitHub OAuth Setup + +1. Go to [GitHub Developer Settings](https://github.com/settings/developers) +2. Click "New OAuth App" +3. Fill in: + - Application name: Your app name + - Homepage URL: `http://localhost:10000` + - Authorization callback URL: `http://localhost:10000/api/auth/callback/github` +4. Copy the Client ID and Client Secret to your `.env` file + +## Getting Started + +From your terminal: + +```sh +pnpm install +pnpm dev +``` + +This starts your app in development mode, rebuilding assets on file changes. + +## Build + +To build the app for production: + +```sh +pnpm build +``` + +## Authentication Configuration + +This example demonstrates how to integrate Better Auth with TanStack Start. Check the source code for examples of: + +- Configuring social authentication providers +- Protecting routes with authentication +- Accessing session data in server functions + +### Key Files + +- `src/utils/auth.ts` - Better Auth server configuration +- `src/utils/auth-client.ts` - Better Auth client setup +- `src/routes/__root.tsx` - Session fetching and navigation +- `src/routes/protected.tsx` - Example of a protected route diff --git a/examples/react/start-basic-better-auth/package.json b/examples/react/start-basic-better-auth/package.json new file mode 100644 index 0000000000..eec836df46 --- /dev/null +++ b/examples/react/start-basic-better-auth/package.json @@ -0,0 +1,32 @@ +{ + "name": "tanstack-start-example-basic-better-auth", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "start": "pnpx srvx --prod -s ../client dist/server/server.js" + }, + "dependencies": { + "@tanstack/react-router": "^1.147.0", + "@tanstack/react-router-devtools": "^1.147.0", + "@tanstack/react-start": "^1.147.0", + "better-auth": "^1.4.10", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwind-merge": "^2.6.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.18", + "@types/node": "^22.5.4", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.6.0", + "tailwindcss": "^4.1.18", + "typescript": "^5.7.2", + "vite": "^7.1.7", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/examples/react/start-basic-better-auth/src/components/DefaultCatchBoundary.tsx b/examples/react/start-basic-better-auth/src/components/DefaultCatchBoundary.tsx new file mode 100644 index 0000000000..2b0e48c6c8 --- /dev/null +++ b/examples/react/start-basic-better-auth/src/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from "@tanstack/react-router"; +import type { ErrorComponentProps } from "@tanstack/react-router"; + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter(); + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }); + + console.error("DefaultCatchBoundary Error:", error); + + return ( +
+ +
+ + {isRoot ? ( + + Home + + ) : ( + { + e.preventDefault(); + window.history.back(); + }} + > + Go Back + + )} +
+
+ ); +} diff --git a/examples/react/start-basic-better-auth/src/components/NotFound.tsx b/examples/react/start-basic-better-auth/src/components/NotFound.tsx new file mode 100644 index 0000000000..f22fabddca --- /dev/null +++ b/examples/react/start-basic-better-auth/src/components/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from "@tanstack/react-router"; + +export function NotFound({ children }: { children?: any }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ); +} diff --git a/examples/react/start-basic-better-auth/src/routeTree.gen.ts b/examples/react/start-basic-better-auth/src/routeTree.gen.ts new file mode 100644 index 0000000000..2f59e4ba33 --- /dev/null +++ b/examples/react/start-basic-better-auth/src/routeTree.gen.ts @@ -0,0 +1,122 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as ProtectedRouteImport } from './routes/protected' +import { Route as LoginRouteImport } from './routes/login' +import { Route as IndexRouteImport } from './routes/index' +import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$' + +const ProtectedRoute = ProtectedRouteImport.update({ + id: '/protected', + path: '/protected', + getParentRoute: () => rootRouteImport, +} as any) +const LoginRoute = LoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({ + id: '/api/auth/$', + path: '/api/auth/$', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/login': typeof LoginRoute + '/protected': typeof ProtectedRoute + '/api/auth/$': typeof ApiAuthSplatRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/login': typeof LoginRoute + '/protected': typeof ProtectedRoute + '/api/auth/$': typeof ApiAuthSplatRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/login': typeof LoginRoute + '/protected': typeof ProtectedRoute + '/api/auth/$': typeof ApiAuthSplatRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/login' | '/protected' | '/api/auth/$' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/login' | '/protected' | '/api/auth/$' + id: '__root__' | '/' | '/login' | '/protected' | '/api/auth/$' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + LoginRoute: typeof LoginRoute + ProtectedRoute: typeof ProtectedRoute + ApiAuthSplatRoute: typeof ApiAuthSplatRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/protected': { + id: '/protected' + path: '/protected' + fullPath: '/protected' + preLoaderRoute: typeof ProtectedRouteImport + parentRoute: typeof rootRouteImport + } + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/api/auth/$': { + id: '/api/auth/$' + path: '/api/auth/$' + fullPath: '/api/auth/$' + preLoaderRoute: typeof ApiAuthSplatRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + LoginRoute: LoginRoute, + ProtectedRoute: ProtectedRoute, + ApiAuthSplatRoute: ApiAuthSplatRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/examples/react/start-basic-better-auth/src/router.tsx b/examples/react/start-basic-better-auth/src/router.tsx new file mode 100644 index 0000000000..338900bf6f --- /dev/null +++ b/examples/react/start-basic-better-auth/src/router.tsx @@ -0,0 +1,19 @@ +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; +import { DefaultCatchBoundary } from "./components/DefaultCatchBoundary"; +import { NotFound } from "./components/NotFound"; +import type { RouterContext } from "./routes/__root"; + +export function getRouter() { + const router = createRouter({ + routeTree, + defaultPreload: "intent", + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + scrollRestoration: true, + context: { + session: null, + } satisfies RouterContext, + }); + return router; +} diff --git a/examples/react/start-basic-better-auth/src/routes/__root.tsx b/examples/react/start-basic-better-auth/src/routes/__root.tsx new file mode 100644 index 0000000000..ff38c30e0c --- /dev/null +++ b/examples/react/start-basic-better-auth/src/routes/__root.tsx @@ -0,0 +1,125 @@ +/// +import type { ReactNode } from "react"; +import { + HeadContent, + Link, + Outlet, + Scripts, + createRootRouteWithContext, + useRouter, +} from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; +import { createServerFn } from "@tanstack/react-start"; +import { getRequestHeaders } from "@tanstack/react-start/server"; +import { auth, type AuthSession } from "~/utils/auth"; +import { authClient } from "~/utils/auth-client"; +import appCss from "~/styles/app.css?url"; + +export interface RouterContext { + session: AuthSession | null; +} + +const fetchSession = createServerFn({ method: "GET" }).handler(async () => { + const headers = getRequestHeaders(); + const session = await auth.api.getSession({ headers }); + return session; +}); + +export const Route = createRootRouteWithContext()({ + beforeLoad: async () => { + const session = await fetchSession(); + return { + session, + }; + }, + head: () => ({ + meta: [ + { + charSet: "utf-8", + }, + { + name: "viewport", + content: "width=device-width, initial-scale=1", + }, + { + title: "TanStack Start Auth Example", + }, + ], + links: [{ rel: "stylesheet", href: appCss }], + }), + component: RootComponent, +}); + +function RootComponent() { + return ( + + + + ); +} + +function RootDocument({ children }: { children: ReactNode }) { + return ( + + + + + + +
{children}
+ + + + + ); +} + +function NavBar() { + const router = useRouter(); + const routeContext = Route.useRouteContext(); + + const handleSignOut = async () => { + await authClient.signOut(); + await router.invalidate(); + await router.navigate({ to: "/" }); + }; + + return ( + + ); +} diff --git a/examples/react/start-basic-better-auth/src/routes/api/auth/$.ts b/examples/react/start-basic-better-auth/src/routes/api/auth/$.ts new file mode 100644 index 0000000000..79a36b6164 --- /dev/null +++ b/examples/react/start-basic-better-auth/src/routes/api/auth/$.ts @@ -0,0 +1,15 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { auth } from "~/utils/auth"; + +/** + * Better Auth API route handler + * Handles all auth routes: /api/auth/* + */ +export const Route = createFileRoute("/api/auth/$")({ + server: { + handlers: { + GET: ({ request }) => auth.handler(request), + POST: ({ request }) => auth.handler(request), + }, + }, +}); diff --git a/examples/react/start-basic-better-auth/src/routes/index.tsx b/examples/react/start-basic-better-auth/src/routes/index.tsx new file mode 100644 index 0000000000..afc0c137f2 --- /dev/null +++ b/examples/react/start-basic-better-auth/src/routes/index.tsx @@ -0,0 +1,50 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ + component: Home, +}); + +function Home() { + const { session } = Route.useRouteContext(); + const user = session?.user; + + return ( +
+

+ TanStack Start Better Auth Example +

+

+ This example demonstrates Better Auth integration with TanStack Start + using GitHub OAuth. +

+ +
+

Auth Status

+ + {session ? ( +
+

Authenticated

+ {user?.image && ( + Avatar + )} +

+ Name: {user?.name ?? "N/A"} +

+

+ Email: {user?.email ?? "N/A"} +

+
+ ) : ( +

+ You are not signed in. Click "Sign In" in the navigation bar to + authenticate with GitHub. +

+ )} +
+
+ ); +} diff --git a/examples/react/start-basic-better-auth/src/routes/login.tsx b/examples/react/start-basic-better-auth/src/routes/login.tsx new file mode 100644 index 0000000000..9f67a0bd78 --- /dev/null +++ b/examples/react/start-basic-better-auth/src/routes/login.tsx @@ -0,0 +1,43 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; +import { authClient } from "~/utils/auth-client"; + +export const Route = createFileRoute("/login")({ + beforeLoad: ({ context }) => { + if (context.session) { + throw redirect({ to: "/" }); + } + }, + component: Login, +}); + +function Login() { + const handleGitHubSignIn = () => { + authClient.signIn.social({ + provider: "github", + callbackURL: "/", + }); + }; + + return ( +
+

Sign In

+ +
+ + +

+ You'll be redirected to GitHub to complete the sign-in process. +

+
+
+ ); +} diff --git a/examples/react/start-basic-better-auth/src/routes/protected.tsx b/examples/react/start-basic-better-auth/src/routes/protected.tsx new file mode 100644 index 0000000000..05e8ebd83c --- /dev/null +++ b/examples/react/start-basic-better-auth/src/routes/protected.tsx @@ -0,0 +1,55 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; + +export const Route = createFileRoute("/protected")({ + beforeLoad: ({ context }) => { + if (!context.session) { + throw redirect({ to: "/login" }); + } + }, + component: Protected, +}); + +function Protected() { + const { session } = Route.useRouteContext(); + const user = session?.user; + + return ( +
+

Protected Page

+

+ This page is only accessible to authenticated users. +

+ +
+

+ Welcome, {user?.name ?? "User"}! +

+ + {user && ( +
+

+ Email: {user?.email ?? "N/A"} +

+ {user?.image && ( +
+ Avatar: + User avatar +
+ )} +
+ )} +
+ +
+

Session Data (Debug)

+
+          {JSON.stringify(session, null, 2)}
+        
+
+
+ ); +} diff --git a/examples/react/start-basic-better-auth/src/styles/app.css b/examples/react/start-basic-better-auth/src/styles/app.css new file mode 100644 index 0000000000..f1d8c73cdc --- /dev/null +++ b/examples/react/start-basic-better-auth/src/styles/app.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/examples/react/start-basic-better-auth/src/utils/auth-client.ts b/examples/react/start-basic-better-auth/src/utils/auth-client.ts new file mode 100644 index 0000000000..f1012dd4ac --- /dev/null +++ b/examples/react/start-basic-better-auth/src/utils/auth-client.ts @@ -0,0 +1,3 @@ +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient(); diff --git a/examples/react/start-basic-better-auth/src/utils/auth.ts b/examples/react/start-basic-better-auth/src/utils/auth.ts new file mode 100644 index 0000000000..05cf771468 --- /dev/null +++ b/examples/react/start-basic-better-auth/src/utils/auth.ts @@ -0,0 +1,28 @@ +import { betterAuth } from "better-auth"; +import { tanstackStartCookies } from "better-auth/tanstack-start"; + +/** + * Better Auth configuration for TanStack Start with GitHub + */ +export const auth = betterAuth({ + socialProviders: { + github: { + clientId: process.env.GITHUB_CLIENT_ID!, + clientSecret: process.env.GITHUB_CLIENT_SECRET!, + }, + }, + plugins: [ + // Must be last plugin in the array + tanstackStartCookies(), + ], + // Stateless sessions (no database required) + session: { + cookieCache: { + enabled: true, + maxAge: 60 * 60 * 24 * 7, + }, + }, +}); + +// Export session type for use in router context +export type AuthSession = typeof auth.$Infer.Session; diff --git a/examples/react/start-basic-better-auth/tsconfig.json b/examples/react/start-basic-better-auth/tsconfig.json new file mode 100644 index 0000000000..70cbbc5ce3 --- /dev/null +++ b/examples/react/start-basic-better-auth/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + } + }, + "include": ["src", "env.d.ts", "vite.config.ts"] +} diff --git a/examples/react/start-basic-better-auth/vite.config.ts b/examples/react/start-basic-better-auth/vite.config.ts new file mode 100644 index 0000000000..3a326e95cf --- /dev/null +++ b/examples/react/start-basic-better-auth/vite.config.ts @@ -0,0 +1,20 @@ +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import { defineConfig } from "vite"; +import tsConfigPaths from "vite-tsconfig-paths"; +import viteReact from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + server: { + port: 10000, + strictPort: true, + }, + plugins: [ + tailwindcss(), + tsConfigPaths({ + projects: ["./tsconfig.json"], + }), + tanstackStart(), + viteReact(), + ], +});