Deploy a Kinde app on Vercel
SDKs and APIs
Official Kinde SDK for TanStack Start—a full-stack React framework on TanStack Router.
@tanstack/react-router and @tanstack/react-start ^1.167.8 or higherIf you don’t have a TanStack Start project, create one using the following command:
npx @tanstack/cli@latest create kinde-tanstack-app --yespnpm dlx @tanstack/cli@latest create kinde-tanstack-app --yesyarn dlx @tanstack/cli@latest create kinde-tanstack-app --yesbunx @tanstack/cli@latest create kinde-tanstack-app --yeshttp://localhost:3000/api/auth/callbackhttp://localhost:3000npm i @kinde/tsrpnpm add @kinde/tsryarn add @kinde/tsrbun add @kinde/tsrCreate or update a .env file in the project root, and add it to your .gitignore file:
touch .envecho ".env" >> .gitignoreAdd the following variables below. You can find these values on the Details page in your Kinde application.
VITE_KINDE_CLIENT_ID=<your-client-id>KINDE_CLIENT_SECRET=<your-client-secret>VITE_KINDE_ISSUER_URL=https://<your-domain>.kinde.comVITE_KINDE_SITE_URL=http://localhost:3000Variables prefixed with VITE_ are exposed to the client. KINDE_CLIENT_SECRET must only run on the server.
Create src/routes/api.auth.$.tsx:
touch "src/routes/api.auth.$.tsx"Add the following code:
// src/routes/api.auth.$.tsximport { kindeAuthHandler } from "@kinde/tsr/server"import { createFileRoute } from "@tanstack/react-router"
export const Route = createFileRoute("/api/auth/$")({ server: { handlers: { GET: async ({ request }) => kindeAuthHandler(request), POST: async ({ request }) => kindeAuthHandler(request), }, },})This single route handles all auth flows:
/api/auth/login/api/auth/logout/api/auth/callback/api/auth/register/api/auth/create-org/api/auth/healthKindeTanstackProviderIn src/routes/__root.tsx, wrap the app so hooks and token refresh work:
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router"import { KindeTanstackProvider } from "@kinde/tsr"import type { ReactNode } from "react"
export const Route = createRootRoute({ shellComponent: RootDocument,})
function RootDocument({ children }: { children: ReactNode }) { return ( <html lang="en"> <head> <HeadContent /> </head> <body> <KindeTanstackProvider>{children}</KindeTanstackProvider> <Scripts /> </body> </html> )}Pass waitForInitialLoad to hold rendering until the session sync completes:
<KindeTanstackProvider waitForInitialLoad>{children}</KindeTanstackProvider>Use the useKindeAuth hook and link components from @kinde/tsr:
import { useKindeAuth, LoginLink, LogoutLink, RegisterLink,} from "@kinde/tsr"
function AuthButtons() { const { isAuthenticated, isLoading, user } = useKindeAuth()
if (isLoading) { return <div>Loading...</div> }
return isAuthenticated ? ( <div> <p>Welcome, {user?.givenName ?? "friend"}.</p> <LogoutLink>Sign out</LogoutLink> </div> ) : ( <div> <LoginLink>Sign in</LoginLink> <RegisterLink>Create account</RegisterLink> </div> )}Use server helpers from @kinde/tsr/server in a route loader:
import { createFileRoute } from "@tanstack/react-router"import { getUserProfile, isAuthenticated } from "@kinde/tsr/server"
export const Route = createFileRoute("/dashboard")({ component: Dashboard,
loader: async () => { const user = await getUserProfile() const isAuthed = await isAuthenticated()
return { user, isAuthed } },})
function Dashboard() { const { user } = Route.useLoaderData()
return <div>Welcome, {user?.givenName}!</div>}Start the development server:
npm run devNavigate to http://localhost:3000 and select Create account. You will be redirected to the Kinde hosted sign up page.
Sign up with a test user and you should be redirected to your application’s dashboard page.
Go to your Kinde dashboard > Users to find the test user you created.
Use protect in beforeLoad to guard a route. It throws a TanStack redirect when the user is unauthenticated or does not satisfy the has checks — do not wrap it in a try/catch unless you plan to re-throw the redirect.
import { createFileRoute } from "@tanstack/react-router"import { protect } from "@kinde/tsr/server"
export const Route = createFileRoute("/protected")({ beforeLoad: async () => { await protect() },})import { createFileRoute } from "@tanstack/react-router"import { protect } from "@kinde/tsr/server"
export const Route = createFileRoute("/protected/admin")({ beforeLoad: async () => { await protect({ has: { roles: ["admin"], permissions: ["read:admin"], featureFlags: ["new-dashboard"], billingEntitlements: ["pro"], }, redirectTo: "/", }) },})redirectTo defaults to / if omitted. Pass forceApi: true inside has to force a Kinde API check instead of reading from tokens.
To protect a group of routes, add protect to a pathless layout route:
import { createFileRoute } from "@tanstack/react-router"import { protect } from "@kinde/tsr/server"
// Everything under _auth is protected.export const Route = createFileRoute("/_auth")({ beforeLoad: async () => { await protect() },})/api/auth pathsSet a custom base path in .env:
KINDE_AUTH_API_PATH=/my/custom/pathOptional overrides for sub-routes (defaults shown in parentheses):
KINDE_AUTH_LOGIN_ROUTE (login)KINDE_AUTH_LOGOUT_ROUTE (logout)KINDE_AUTH_REGISTER_ROUTE (register)KINDE_AUTH_CREATE_ORG_ROUTE (create-org)KINDE_AUTH_HEALTH_ROUTE (health)Example: If KINDE_AUTH_API_PATH="/my/custom/path" and KINDE_AUTH_LOGIN_ROUTE="app_login", login is at /my/custom/path/app_login.
Rename the route file to match your API path if you change KINDE_AUTH_API_PATH. The file route path must match your server routes.
Set KINDE_POST_LOGIN_REDIRECT_URL in .env to redirect users to a specific page after login:
KINDE_POST_LOGIN_REDIRECT_URL=/dashboardPass postLoginRedirectURL on the link component:
<LoginLink postLoginRedirectURL="/dashboard">Sign in</LoginLink>You can also achieve the same result by sending users to /api/auth/login?post_login_redirect_url=/dashboard.
<Link href="/api/auth/login?post_login_redirect_url=/dashboard">Sign in</Link>Set KINDE_DEBUG_MODE in .env:
KINDE_DEBUG_MODE=trueWhen enabled, you will see extra logs in the console useful for troubleshooting.
The health route at /api/auth/health also changes behavior based on this flag:
KINDE_DEBUG_MODE unset or false — returns OKKINDE_DEBUG_MODE=true — returns JSON describing the resolved config and generated auth URLsUse the health route to verify your environment variables are set correctly during setup.
VITE_KINDE_SITE_URL to your production origin.https://yourdomain.com/api/auth/callbackhttps://yourdomain.comKindeTanstackProviderWraps the Kinde React provider and syncs the server session into the client store. Place it near the root of your app in __root.tsx.
Props:
children (ReactNode) — Required. The app subtree.waitForInitialLoad (boolean) — Optional. Hold rendering until the initial session sync completes.Usage:
import { KindeTanstackProvider } from "@kinde/tsr"
<KindeTanstackProvider waitForInitialLoad> {children}</KindeTanstackProvider>LoginLinkA TanStack Router Link pointed at the configured login route (/api/auth/login by default).
Props:
children (ReactNode) — Required. Link label.Usage:
import { LoginLink } from "@kinde/tsr"
<LoginLink>Sign in</LoginLink>LogoutLinkA TanStack Router Link pointed at the configured logout route (/api/auth/logout by default).
Props:
children (ReactNode) — Required. Link label.Usage:
import { LogoutLink } from "@kinde/tsr"
<LogoutLink>Sign out</LogoutLink>RegisterLinkA TanStack Router Link pointed at the configured register route (/api/auth/register by default).
Props:
children (ReactNode) — Required. Link label.Usage:
import { RegisterLink } from "@kinde/tsr"
<RegisterLink>Create account</RegisterLink>PortalLinkCreates a link to the Kinde self-serve portal. Learn more about the self-serve portal for users.
Props:
children (ReactNode) — Required. Link label.subNav (PortalPage) — Optional. The area of the portal to land on (default: profile).returnUrl (string) — Optional. Absolute URL to return to after portal actions.Usage:
import { PortalLink } from "@kinde/tsr"
<PortalLink>Manage account</PortalLink>useKindeAuthCall this hook inside any client component to access auth state and token helpers.
Usage:
import { useKindeAuth } from "@kinde/tsr"
function MyComponent() { const { isAuthenticated, isLoading, user } = useKindeAuth()
if (isLoading) return <div>Loading...</div>
return isAuthenticated ? ( <p>Welcome, {user?.givenName}!</p> ) : ( <p>Not signed in.</p> )}isAuthenticatedReturns true if the user is currently authenticated.
Type: boolean
isLoadingtrue while the initial auth state is being resolved. Use this to avoid rendering before the session is known.
Type: boolean
The profile of the currently signed-in user. null when unauthenticated.
Available properties:
id (string) — The user’s Kinde ID, e.g. kp_1234567890givenName (string) — First namefamilyName (string) — Last nameemail (string) — Email addresspicture (string) — Profile picture URL if availablegetPermissionReturns the state of a given permission for the current user.
Arguments:
key: stringUsage:
const { getPermission } = useKindeAuth()
const permission = await getPermission("read:todos")Output:
{ permissionKey: "read:todos", orgCode: "org_1234", isGranted: true}getPermissionsReturns all permissions for the current user for the organization they are signed into.
Usage:
const { getPermissions } = useKindeAuth()
const permissions = await getPermissions()Output:
{ orgCode: "org_1234", permissions: ["create:todos", "update:todos", "read:todos"]}getRolesReturns all roles for the current user for the organization they are signed into.
Usage:
const { getRoles } = useKindeAuth()
const roles = await getRoles()Output:
{ orgCode: "org_1234", roles: ["admin", "user"]}getFlagGets a feature flag value from the feature_flags claim of the access token.
Arguments:
code: string, options?: { defaultValue?: string | boolean | number }Usage:
const { getFlag } = useKindeAuth()
const flag = await getFlag("theme")Output:
{ "code": "theme", "type": "string", "value": "pink", "is_default": false}getClaimGets a single claim from the access or ID token. Defaults to the access token.
Arguments:
claim: string, tokenKey?: "accessToken" | "idToken"Usage:
const { getClaim } = useKindeAuth()
const claim = await getClaim("given_name", "idToken")getClaimsReturns all claims from the access or ID token.
Arguments:
tokenKey?: "accessToken" | "idToken"Usage:
const { getClaims } = useKindeAuth()
const claims = await getClaims()getOrganizationGets the org code of the organization the user is currently signed into.
Usage:
const { getOrganization } = useKindeAuth()
const org = await getOrganization()Output:
org_1234getUserOrganizationsGets all organizations the user has access to.
Usage:
const { getUserOrganizations } = useKindeAuth()
const orgs = await getUserOrganizations()Output:
["org_1234", "org_5678"]getAccessTokenReturns the raw access token JWT from session storage. Does not refresh — the value may be expired. Returns undefined when the user is signed out.
Type: Promise<string | undefined>
Usage:
const { getAccessToken } = useKindeAuth()
const token = await getAccessToken()refreshTokenManually refreshes the access token and returns new token values.
Usage:
const { refreshToken } = useKindeAuth()
const result = await refreshToken({ clientId: "your_client_id", domain: "https://your_kinde_domain.kinde.com",})Output:
{ success: true, accessToken: "<access_token>", idToken: "<id_token>", refreshToken: "<refresh_token>"}kindeAuthHandlerHandles all Kinde auth requests (login, logout, callback, register, create-org, health). Use it in the catch-all route file.
Arguments:
request: RequestReturns: Promise<Response>
Usage:
import { kindeAuthHandler } from "@kinde/tsr/server"import { createFileRoute } from "@tanstack/react-router"
export const Route = createFileRoute("/api/auth/$")({ server: { handlers: { GET: async ({ request }) => kindeAuthHandler(request), POST: async ({ request }) => kindeAuthHandler(request), }, },})protectGuards a TanStack route from beforeLoad. Throws a TanStack redirect when the user is unauthenticated or does not satisfy the has checks. Do not wrap in a try/catch unless you plan to re-throw the redirect.
Arguments:
options?: { has?: { roles?: string[] permissions?: string[] featureFlags?: string[] billingEntitlements?: string[] forceApi?: boolean } redirectTo?: string // default: "/"}Returns: Promise<void> — or throws a redirect
Usage:
import { createFileRoute } from "@tanstack/react-router"import { protect } from "@kinde/tsr/server"
export const Route = createFileRoute("/admin")({ beforeLoad: async () => { await protect({ has: { roles: ["admin"], permissions: ["read:admin"], }, redirectTo: "/", }) },})getUserProfileReturns the profile of the currently signed-in user.
Usage:
import { getUserProfile } from "@kinde/tsr/server"
const user = await getUserProfile()Output:
{ id: "kp_1234567890", givenName: "Dave", familyName: "Smith", email: "dave@smith.com", picture: "public_image_url"}isAuthenticatedReturns true if the current user has a valid session.
Usage:
import { isAuthenticated } from "@kinde/tsr/server"
const authed = await isAuthenticated()Output:
truegetPermissionReturns the state of a given permission for the current user.
Arguments:
key: stringUsage:
import { getPermission } from "@kinde/tsr/server"
const permission = await getPermission("read:todos")Output:
{ permissionKey: "read:todos", orgCode: "org_1234", isGranted: true}getPermissionsReturns all permissions for the current user for the organization they are signed into.
Usage:
import { getPermissions } from "@kinde/tsr/server"
const permissions = await getPermissions()Output:
{ orgCode: "org_1234", permissions: ["create:todos", "update:todos", "read:todos"]}getRolesReturns all roles for the current user for the organization they are signed into.
Usage:
import { getRoles } from "@kinde/tsr/server"
const roles = await getRoles()Output:
{ orgCode: "org_1234", roles: ["admin", "user"]}getFlagGets a feature flag value from the feature_flags claim of the access token.
Arguments:
code: string, options?: { defaultValue?: string | boolean | number }Usage:
import { getFlag } from "@kinde/tsr/server"
const flag = await getFlag("theme")Output:
{ "code": "theme", "type": "string", "value": "pink", "is_default": false}getClaimGets a single claim from the access or ID token. Defaults to the access token.
Arguments:
claim: string, tokenKey?: "accessToken" | "idToken"Usage:
import { getClaim } from "@kinde/tsr/server"
const claim = await getClaim("given_name", "idToken")Output:
{ name: "given_name", value: "Dave" }getClaimsReturns all claims from the access or ID token.
Arguments:
tokenKey?: "accessToken" | "idToken"Usage:
import { getClaims } from "@kinde/tsr/server"
const claims = await getClaims()// or for the ID token:const idTokenClaims = await getClaims("idToken")getCurrentOrganizationGets the org code of the organization the user is currently signed into.
Usage:
import { getCurrentOrganization } from "@kinde/tsr/server"
const org = await getCurrentOrganization()Output:
org_1234getUserOrganizationsGets all organizations the user has access to.
Usage:
import { getUserOrganizations } from "@kinde/tsr/server"
const orgs = await getUserOrganizations()Output:
["org_1234", "org_5678"]getEntitlementReturns the state of a given billing entitlement for the current user.
Arguments:
key: stringUsage:
import { getEntitlement } from "@kinde/tsr/server"
const entitlement = await getEntitlement("pro")getEntitlementsReturns all billing entitlements for the current user.
Usage:
import { getEntitlements } from "@kinde/tsr/server"
const entitlements = await getEntitlements()Checks whether the current user satisfies one or more access conditions — roles, permissions, feature flags, or billing entitlements. Returns true only if all specified conditions are met.
Arguments:
options: { roles?: string[] permissions?: string[] featureFlags?: string[] billingEntitlements?: string[] forceApi?: boolean}Returns: Promise<boolean>
Usage:
import { has } from "@kinde/tsr/server"
const canAccess = await has({ roles: ["admin"], permissions: ["read:admin"], featureFlags: ["new-dashboard"], billingEntitlements: ["pro"],})