TanStack Start
npx gitpick keycloakify/oidc-spa/tree/main/examples/tanstack-start start-oidc
cd start-oidc
npm install
npm run dev
# By default, the example runs against Keycloak.
# You can edit the .env file to test other providers.Live example:
Step-by-step setup
1. Install dependencies
npm install oidc-spa zodNote: Zod is optional but highly recommended. Writing validators manually is error-prone, and skipping validation means losing early guarantees about what your auth server provides.
2. Configure Vite
Add the plugin to your vite.config.ts:
import { defineConfig } from "vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import viteTsConfigPaths from "vite-tsconfig-paths";
import tailwindcss from "@tailwindcss/vite";
import { oidcSpa } from "oidc-spa/vite-plugin";
export default defineConfig({
plugins: [
viteTsConfigPaths({ projects: ["./tsconfig.json"] }),
tailwindcss(),
tanstackStart(),
oidcSpa(),
viteReact(),
],
});3. Provide your OIDC environment variables
OIDC_USE_MOCK=false
OIDC_ISSUER_URI=https://cloud-iam.oidc-spa.dev/realms/oidc-spa
OIDC_CLIENT_ID=example-tanstack-startYou can use our preconfigured Keycloak, Auth0, or Google OAuth test accounts — see this sample file.
For your own configuration, refer to:
Provider configuration4. Bootstrapping the OIDC API
Create the following file:
import { oidcSpa } from "oidc-spa/react-tanstack-start";
import { z } from "zod";
export const {
bootstrapOidc,
createOidcComponent,
getOidc,
enforceLogin,
oidcFnMiddleware,
oidcRequestMiddleware,
} = oidcSpa
.withExpectedDecodedIdTokenShape({
// Client-side view of the user's identity.
// You declare what parts of the ID token you plan to use.
// (You can inspect the console to see the full object.)
decodedIdTokenSchema: z.object({
name: z.string(),
picture: z.string().optional(),
realm_access: z.object({ roles: z.array(z.string()) }).optional(),
}),
decodedIdToken_mock: {
name: "John Doe",
},
})
.withAccessTokenValidation({
type: "RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens",
// In oidc-spa, the *frontend* is the OIDC client.
// The backend (server functions or APIs) plays the role of a
// resource server — it simply validates access tokens received
// from the client. It is not part of the authentication flow itself.
accessTokenClaimsSchema: z.object({
sub: z.string(), // User ID
realm_access: z.object({ roles: z.array(z.string()) }).optional(),
}),
accessTokenClaims_mock: { sub: "123" },
expectedAudience: () => "account", // Keycloak default; depends on provider
})
.finalize();
// Can be called anywhere (even in React component bodies).
// Only the first call has an effect — subsequent calls are ignored.
// The process object allows accessing environment variables from both
// client and server seamlessly (only the ones you dereference are exposed).
bootstrapOidc(({ process }) =>
process.env.OIDC_USE_MOCK === "true"
? {
implementation: "mock",
isUserInitiallyLoggedIn: true,
}
: {
implementation: "real",
issuerUri: process.env.OIDC_ISSUER_URI,
clientId: process.env.OIDC_CLIENT_ID,
debugLogs: true,
}
);
// A fetch wrapper that automatically adds the Authorization header
// if the user is logged in.
export const fetchWithAuth: typeof fetch = async (input, init) => {
const oidc = await getOidc();
if (oidc.isUserLoggedIn) {
const accessToken = await oidc.getAccessToken();
const headers = new Headers(init?.headers);
headers.set("Authorization", `Bearer ${accessToken}`);
(init ??= {}).headers = headers;
}
return fetch(input, init);
};5. Add Header Auth Buttons


Code reference:
Redirecting users directly to the registration page varies by provider — see this Auth0 example.
Making authenticated requests
Enforcing authentication on routes and server functions: (source)
import { useState } from "react";
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { enforceLogin, oidcFnMiddleware } from "@/oidc";
import Spinner from "@/components/Spinner";
import { getTodosStore } from "@/data/todos";
const getTodos = createServerFn({ method: "GET" })
.middleware([oidcFnMiddleware({ assert: "user logged in" })])
.handler(async ({ context: { oidc } }) => {
const userId = oidc.accessTokenClaims.sub;
const todosStore = getTodosStore();
return await todosStore.readTodos({ userId });
});
const addTodo = createServerFn({ method: "POST" })
.inputValidator((d: string) => d)
.middleware([oidcFnMiddleware({ assert: "user logged in" })])
.handler(async ({ data, context: { oidc } }) => {
const userId = oidc.accessTokenClaims.sub;
const todosStore = getTodosStore();
const todos = await todosStore.readTodos({ userId });
todos.push({ id: todos.length + 1, name: data });
await todosStore.updateTodos({ userId, todos });
});
export const Route = createFileRoute("/demo/start/server-funcs")({
beforeLoad: enforceLogin,
component: Home,
loader: async () => await getTodos(),
pendingComponent: () => (
<div className="flex flex-1 items-center justify-center py-16">
<Spinner />
</div>
),
});
function Home() {
const router = useRouter();
const todos = Route.useLoaderData();
const [newTodoInputValue, setNewTodoInputValue] = useState("");
const onAddTodoButtonClick = async () => {
await addTodo({ data: newTodoInputValue });
setNewTodoInputValue("");
router.invalidate();
};
return (
<div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span className="text-lg text-white">{todo.name}</span>
</li>
))}
</ul>
<div>
<input
type="text"
value={newTodoInputValue}
onChange={e => setNewTodoInputValue(e.target.value)}
onKeyDown={e => e.key === "Enter" && onAddTodoButtonClick()}
placeholder="Enter a new todo..."
/>
<button
disabled={newTodoInputValue.trim().length === 0}
onClick={onAddTodoButtonClick}
>
Add todo
</button>
</div>
</div>
);
}Authenticate an API:
Calling an authenticated API:
Auto Logout overlay:
Last updated
Was this helpful?