Key Concepts
- Authentication: Verifying the identity of a user.
- Tanstack Start: A framework for building web applications.
- Better Off: An authentication library.
- Social Login: Authentication using third-party providers like GitHub.
- OAUTH: An open standard for access delegation, commonly used for social login.
- TRPC: A library for building end-to-end typesafe APIs.
- Shadcn UI: A collection of accessible and reusable components.
- Server Functions: Functions that run on the server.
- API Routes: Server endpoints that handle requests.
- Middleware: Functions that intercept and modify requests.
- Loaders: Functions that fetch data before a route is rendered.
- Tanstack Query: A library for fetching, caching, and updating asynchronous data.
Setting Up the Project
- Create a Tanstack Start App: Use
create-tanzack-start-appwith TRPC and Shadcn UI add-ons. The command used wascreate-tanzack-start-app better-off-setup --add trpc,shadcn-ui. - Install Better Off: Add the Better Off library using
pmppm add better-off. - Environment Variables: Create a
.envfile and add theOAUTH_SECRETvariable. Generate a secure secret for this variable. - OAUTH URL: Set the
OAUTH_URLenvironment variable to the base URL of the application (e.g.,localhost:3000in development).
Configuring GitHub OAUTH
- Create a GitHub OAUTH App: Go to GitHub Developer Settings and create a new OAUTH app.
- Homepage URL: Set the homepage URL to the application's base URL.
- Authorization Callback URL: Set the redirect URI to
/api/auth/callback/github. Note that GitHub only allows one callback URL per app, so you'll need separate apps for development and production. - Client ID and Secret: Obtain the client ID and client secret from the GitHub OAUTH app settings.
- Environment Variables: Add
GITHUB_CLIENT_IDandGITHUB_CLIENT_SECRETto the.envfile.
Better Off Instance
-
Create
o.ts: Create a file (e.g.,lib/o.ts) to instantiate the Better Off instance. -
Import Better Off: Import the
betterOfffunction from thebetter-offlibrary. -
Instantiate Better Off: Use the
betterOfffunction to create an instance of Better Off, exporting it aso. -
Define Providers: Configure the authentication providers (e.g., GitHub) with their respective client IDs and secrets.
import { betterOff } from 'better-off'; import { gitHub } from 'better-off/providers'; export const o = betterOff({ providers: [ gitHub({ clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, }), ], });
API Route for OAUTH Callback
-
Create API Route: Create a file at
routes/api/auth/[$](e.g.,routes/api/auth/[$].tsx). The[$]syntax in Tanstack Start creates a dynamic route segment. -
Import O: Import the
oinstance fromlib/o.ts. -
Create API Endpoint: Create a handler function that handles both
GETandPOSTrequests and passes them to theo.handler.import { o } from '~/lib/o'; export async function GET({ request, params }: any) { return o.handler({ request, params }); } export async function POST({ request, params }: any) { return o.handler({ request, params }); }
React Hooks
-
Create
o-client.ts: Create a file (e.g.,lib/o-client.ts) to create the React hooks. -
Import
createOClient: ImportcreateOClientfrombetter-off/react. -
Create O Client: Use
createOClientto generate theuseSession,signIn,signOut,signUp, andgetSessionhooks. -
API Endpoint Configuration: Configure the
apiEndpointoption to point to the correct API route (e.g.,http://localhost:3000/api/authin development).import { createOClient } from 'better-off/react'; export const { useSession, signIn, signOut, signUp, getSession } = createOClient({ apiEndpoint: process.env.NODE_ENV === 'development' ? 'http://localhost:3000/api/auth' : '/api/auth', });
Implementing Sign-In and Sign-Out
-
Import Hooks and Components: Import the
useSession,signIn, andsignOuthooks fromlib/o-client.ts, and theButtoncomponent from Shadcn UI. -
Use
useSessionHook: Use theuseSessionhook to get the current session data. -
Conditional Rendering: Conditionally render sign-in and sign-out buttons based on the session status.
import { Button } from '~/components/ui/button'; import { useSession, signIn, signOut } from '~/lib/o-client'; export default function Home() { const { session } = useSession(); return ( <div className="flex flex-col items-center justify-center h-screen"> {session ? ( <> <p>Signed in as {session.user.name}</p> <Button onClick={() => signOut()}>Sign Out</Button> </> ) : ( <Button onClick={() => signIn('github')}>Sign In with GitHub</Button> )} </div> ); }
Authenticated Routes
-
Create Route: Create a new route (e.g.,
routes/dashboard.tsx). -
Create Server Function (
getUserId.ts): Create a server function to get the user ID from the session. -
Create Middleware (
o-middleware.ts): Create middleware to extract the session from the request headers and pass it to the server function.// o-middleware.ts import { createMiddleware } from '@tanzack/start/server'; import { getSession } from '~/lib/o-client'; export const oMiddleware = createMiddleware({ server: async ({ fetchOptions }) => { const session = await getSession(fetchOptions); return { user: session?.user, }; }, }); // getUserId.ts import { serverFn } from '@tanzack/start/server'; import { oMiddleware } from './o-middleware'; export const getUserId = serverFn({ middleware: [oMiddleware], }, async ({ ctx }) => { return ctx.user?.id; }); -
Implement Loader: In the route's
beforeLoadcallback, call thegetUserIdserver function. If the user ID is not present, redirect to the homepage.import { redirect } from '@tanzack/start/server'; import { getUserId } from './getUserId'; export const beforeLoad = () => { return { userId: getUserId(), }; }; export async function loader({ context }:any) { if (!context.userId) { throw redirect('/'); } return { userId: context.userId, }; } export default function Dashboard() { return ( <div> <h1>Dashboard</h1> <p>User ID: {context.userId}</p> </div> ); }
Authenticating Server Functions
-
Create Server Function (
getAvatar.ts): Create a server function to get the user's avatar URL from the session. -
Use Middleware: Apply the same
oMiddlewareto the server function to access the session data.// getAvatar.ts import { serverFn } from '@tanzack/start/server'; import { oMiddleware } from './o-middleware'; export const getAvatar = serverFn({ middleware: [oMiddleware], }, async ({ ctx }) => { return ctx.user?.image; }); -
Call Server Function: Call the server function from the client using
useEffectanduseStateto display the avatar.import { useState, useEffect } from 'react'; import { getAvatar } from './getAvatar'; export default function Dashboard() { const [avatar, setAvatar] = useState<string | null>(null); useEffect(() => { getAvatar().then(setAvatar); }, []); return ( <div> <h1>Dashboard</h1> {avatar && <img src={avatar} alt="Avatar" />} </div> ); }
Authenticating API Routes
-
Create API Route: Create a new API route (e.g.,
routes/api/name.tsx). -
Get Session: Use
o.api.getSessionto get the session from the request headers. -
Check Session: If the session is not present, return an unauthorized response. Otherwise, return the requested data.
import { o } from '~/lib/o'; export async function GET({ request }: any) { const session = await o.api.getSession(request.headers); if (!session) { return new Response(null, { status: 401 }); } return Response.json({ name: 'Your Name' }); } -
Call API Route: Use Tanstack Query to call the API route and display the data.
import { useQuery } from '@tanstack/react-query'; export default function Dashboard() { const { data: username } = useQuery({ queryKey: ['name'], queryFn: () => fetch('/api/name').then((res) => res.json()), }); return ( <div> <h1>Dashboard</h1> <p>Username: {JSON.stringify(username)}</p> </div> ); }
Authenticating TRPC Routes
-
Create Context (
createContext.ts): Create a function to create the TRPC context, including the session data.// createContext.ts import { getSession } from '~/lib/o-client'; export async function createContext({ req }: any) { const session = await getSession(req.headers); return { session, }; } -
Update TRPC Handler: Update the TRPC API handler to use the
createContextfunction.// api/trpc/[trpc].ts import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { appRouter } from '~/server/router'; import { createContext } from '~/server/context'; const handler = async (req: Request): Promise<Response> => { const response = await fetchRequestHandler({ endpoint: '/api/trpc', router: appRouter, req, createContext: () => createContext({ req }), onError: process.env.NODE_ENV === 'development' ? ({ path, error }) => { console.error( `❌ tRPC failed on ${path ?? '<no-path>'}: ${error.message}`, ); } : undefined, }); return response; }; export { handler as GET, handler as POST }; -
Create Protected Procedure: Create a protected procedure that checks for a session and throws an error if it's not present.
// server/router/index.ts import { t } from './trpc'; import { TRPCError } from '@trpc/server'; const isAuthed = t.middleware(({ next, ctx }) => { if (!ctx.session) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return next({ ctx: { session: ctx.session, user: ctx.session.user, }, }); }); export const protectedProcedure = t.procedure.use(isAuthed); -
Implement Protected Route: Create a TRPC route that uses the protected procedure.
// server/router/index.ts import { protectedProcedure, publicProcedure, router } from './trpc'; export const appRouter = router({ getUsername: protectedProcedure.query(({ ctx }) => { return { username: ctx.session.user.name }; }), }); export type AppRouter = typeof appRouter; -
Call TRPC Route: Use
useTRPCand Tanstack Query to call the TRPC route and display the data.import { useQuery } from '@tanstack/react-query'; import { useTRPC } from '~/utils/trpc'; export default function Dashboard() { const trpc = useTRPC(); const { data: username } = trpc.getUsername.useQuery(); return ( <div> <h1>Dashboard</h1> <p>Username from TRPC: {JSON.stringify(username)}</p> </div> ); }
Conclusion
The video demonstrates how to integrate Better Off into a Tanstack Start application to handle authentication across various parts of the application, including React components, server functions, API routes, and TRPC endpoints. It covers setting up social login with GitHub, creating authenticated routes, and securing server-side logic. The key takeaway is that Better Off provides the core authentication mechanics, while the UI and specific implementation details are left to the developer.
AI summaries can miss context or contain errors. Check important details against the original video.





