Skip to content
  • SDKs and APIs
  • Back end SDKs

TanStack Start React SDK

Official Kinde SDK for TanStack Start—a full-stack React framework on TanStack Router.

  • A Kinde account with Admin or Engineer access (Sign up for free)
  • Node.js version 20 or higher
  • A TanStack Start project using @tanstack/react-router and @tanstack/react-start ^1.167.8 or higher

1. Create a new TanStack Start project

Link to this section

If you don’t have a TanStack Start project, create one using the following command:

Terminal window
npx @tanstack/cli@latest create kinde-tanstack-app --yes

2. Create a Kinde app and set callback URLs

Link to this section
  1. In Kinde, select Add new application, select Back-end web > Other back end
  2. Go to View details and copy the Domain (or custom domain), Client ID and Client secret values.
  3. Add callback URLs, for example:
    • Allowed callback URLshttp://localhost:3000/api/auth/callback
    • Allowed logout redirect URLshttp://localhost:3000
    For production, add the same paths using your real domain.
  4. Select Save.

3. Install the Kinde SDK

Link to this section
Terminal window
npm i @kinde/tsr

4. Add environment variables

Link to this section
  1. Create or update a .env file in the project root, and add it to your .gitignore file:

    Terminal window
    touch .env
    echo ".env" >> .gitignore
  2. Add 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.com
    VITE_KINDE_SITE_URL=http://localhost:3000

5. Add the Kinde auth route handler

Link to this section
  1. Create src/routes/api.auth.$.tsx:

    Terminal window
    touch "src/routes/api.auth.$.tsx"
  2. Add the following code:

    // src/routes/api.auth.$.tsx
    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),
    },
    },
    })

    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/health

6. Wrap the root route with KindeTanstackProvider

Link to this section
  1. In src/routes/__root.tsx, wrap the app so hooks and token refresh work:

    src/routes/__root.tsx
    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>

7. Add auth UI components

Link to this section

Use the useKindeAuth hook and link components from @kinde/tsr:

src/components/auth-ui.tsx
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>
)
}

8. Display logged-in user information

Link to this section

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>
}

9. Test the auth flow

Link to this section
  1. Start the development server:

    Terminal window
    npm run dev
  2. Navigate to http://localhost:3000 and select Create account. You will be redirected to the Kinde hosted sign up page.

  3. Sign up with a test user and you should be redirected to your application’s dashboard page.

  4. Go to your Kinde dashboard > Users to find the test user you created.

Protect routes with roles and permissions

Link to this section

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.

Authenticated users only

Link to this section
import { createFileRoute } from "@tanstack/react-router"
import { protect } from "@kinde/tsr/server"
export const Route = createFileRoute("/protected")({
beforeLoad: async () => {
await protect()
},
})

With roles, permissions, feature flags, or billing entitlements

Link to this section
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.

Protect multiple routes with a layout

Link to this section

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()
},
})

Change default /api/auth paths

Link to this section

Set a custom base path in .env:

.env
KINDE_AUTH_API_PATH=/my/custom/path

Optional 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.

Redirect after login

Link to this section

Static redirect

Link to this section

Set KINDE_POST_LOGIN_REDIRECT_URL in .env to redirect users to a specific page after login:

.env
KINDE_POST_LOGIN_REDIRECT_URL=/dashboard

Dynamic redirect

Link to this section

Pass 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>

Debug mode and health route

Link to this section

Set KINDE_DEBUG_MODE in .env:

.env
KINDE_DEBUG_MODE=true

When 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 OK
  • KINDE_DEBUG_MODE=true — returns JSON describing the resolved config and generated auth URLs

Use the health route to verify your environment variables are set correctly during setup.

Deploy to production

Link to this section
  1. Set production values for all Kinde env vars.
  2. Set VITE_KINDE_SITE_URL to your production origin.
  3. In Kinde, add production Allowed callback URLs and Allowed logout redirect URLs, for example:
    • https://yourdomain.com/api/auth/callback
    • https://yourdomain.com

API reference - KindeTanstackProvider

Link to this section

Wraps 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>

Client components

Link to this section

A 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>

A 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>

A 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>

Creates 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>

API references - useKindeAuth

Link to this section

Call 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>
)
}

isAuthenticated

Link to this section

Returns true if the user is currently authenticated.

Type: boolean

true 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_1234567890
  • givenName (string) — First name
  • familyName (string) — Last name
  • email (string) — Email address
  • picture (string) — Profile picture URL if available

Returns the state of a given permission for the current user.

Arguments:

key: string

Usage:

const { getPermission } = useKindeAuth()
const permission = await getPermission("read:todos")

Output:

{
permissionKey: "read:todos",
orgCode: "org_1234",
isGranted: true
}

getPermissions

Link to this section

Returns 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"]
}

Returns 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"]
}

Gets 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
}

Gets 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")

Returns all claims from the access or ID token.

Arguments:

tokenKey?: "accessToken" | "idToken"

Usage:

const { getClaims } = useKindeAuth()
const claims = await getClaims()

getOrganization

Link to this section

Gets the org code of the organization the user is currently signed into.

Usage:

const { getOrganization } = useKindeAuth()
const org = await getOrganization()

Output:

org_1234

getUserOrganizations

Link to this section

Gets all organizations the user has access to.

Usage:

const { getUserOrganizations } = useKindeAuth()
const orgs = await getUserOrganizations()

Output:

["org_1234", "org_5678"]

getAccessToken

Link to this section

Returns 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()

Manually 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>"
}

API references - Server

Link to this section

kindeAuthHandler

Link to this section

Handles all Kinde auth requests (login, logout, callback, register, create-org, health). Use it in the catch-all route file.

Arguments:

request: Request

Returns: 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),
},
},
})

Guards 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: "/",
})
},
})

getUserProfile

Link to this section

Returns 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"
}

isAuthenticated

Link to this section

Returns true if the current user has a valid session.

Usage:

import { isAuthenticated } from "@kinde/tsr/server"
const authed = await isAuthenticated()

Output:

true

Returns the state of a given permission for the current user.

Arguments:

key: string

Usage:

import { getPermission } from "@kinde/tsr/server"
const permission = await getPermission("read:todos")

Output:

{
permissionKey: "read:todos",
orgCode: "org_1234",
isGranted: true
}

getPermissions

Link to this section

Returns 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"]
}

Returns 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"]
}

Gets 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
}

Gets 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" }

Returns 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")

getCurrentOrganization

Link to this section

Gets 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_1234

getUserOrganizations

Link to this section

Gets all organizations the user has access to.

Usage:

import { getUserOrganizations } from "@kinde/tsr/server"
const orgs = await getUserOrganizations()

Output:

["org_1234", "org_5678"]

getEntitlement

Link to this section

Returns the state of a given billing entitlement for the current user.

Arguments:

key: string

Usage:

import { getEntitlement } from "@kinde/tsr/server"
const entitlement = await getEntitlement("pro")

getEntitlements

Link to this section

Returns 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"],
})