@inai-dev/tanstack-start
v2.1.0
Published
TanStack Start integration for InAI Auth SDK
Downloads
380
Maintainers
Readme
@inai-dev/tanstack-start
Full TanStack Start integration for InAI Auth. Includes request middleware, server function middleware, server-side auth helpers, API route handlers, React hooks, and UI components.
Installation
npm install @inai-dev/tanstack-startEnvironment Variables
# Required — your publishable key (server-only, NOT exposed to the browser)
INAI_PUBLISHABLE_KEY=pk_live_...Setup
1. Request Middleware
// app/ssr.tsx
import { createStart } from "@tanstack/react-start";
import { createInAIAuthMiddleware } from "@inai-dev/tanstack-start/middleware";
const authMiddleware = createInAIAuthMiddleware({
publicRoutes: ["/", "/about", "/login", "/register"],
signInUrl: "/login",
// onUnauthorized: "null", // pass auth: null instead of redirecting (useful for API routes)
// jwksUrl: "https://apiauth.inai.dev/.well-known/jwks.json", // optional override
});
export default createStart(() => ({
requestMiddleware: [authMiddleware],
}));All tokens are cryptographically verified using ES256 (ECDSA P-256). Public keys are fetched from the JWKS endpoint and cached for 5 minutes.
2. Provider
// app/root.tsx
import { InAIAuthProvider } from "@inai-dev/tanstack-start";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<InAIAuthProvider>{children}</InAIAuthProvider>
</body>
</html>
);
}3. API Routes
// routes/api/auth/$path.ts
import { createFileRoute } from "@tanstack/react-router";
import { createAuthRouteHandlers } from "@inai-dev/tanstack-start/server";
const { handleRequest } = createAuthRouteHandlers();
export const Route = createFileRoute("/api/auth/$path")({
server: {
handlers: {
POST: ({ request, params }) => handleRequest(request, params.path),
},
},
});Handles the following endpoints automatically:
POST /api/auth/login— User loginPOST /api/auth/register— User registrationPOST /api/auth/mfa-challenge— MFA verificationPOST /api/auth/refresh— Token refreshPOST /api/auth/logout— User logoutPOST /api/auth/forgot-password— Password reset requestPOST /api/auth/reset-password— Password resetPOST /api/auth/verify-email— Email verification
Server-Side Auth
auth()
Returns a ServerAuthObject with the current authentication state. This function is synchronous (unlike Next.js's async auth()) because TanStack Start's cookie APIs are synchronous.
import { createServerFn } from "@tanstack/react-start";
import { auth } from "@inai-dev/tanstack-start/server";
const getDashboard = createServerFn({ method: "GET" }).handler(() => {
const { userId, has, protect, redirectToSignIn, getToken } = auth();
// Check if user is authenticated
if (!userId) {
redirectToSignIn({ returnTo: "/dashboard" });
}
// Check roles/permissions
if (has({ role: "admin" })) {
// admin-only logic
}
// Protect — throws redirect if unauthorized
const authed = protect({ permission: "posts:write" });
return { userId };
});ServerAuthObject:
| Property | Type | Description |
|---|---|---|
| userId | string \| null | Current user ID |
| tenantId | string \| null | Tenant ID |
| appId | string \| null | Application ID |
| envId | string \| null | Environment ID |
| orgId | string \| null | Active organization ID |
| orgRole | string \| null | Role in active organization |
| sessionId | string \| null | Session ID |
| roles | string[] | User's global roles |
| permissions | string[] | User's global permissions |
| getToken() | () => Promise<string \| null> | Get the access token |
| has(params) | ({ role?, permission? }) => boolean | Check role or permission |
| protect(params?) | ({ role?, permission?, redirectTo? }) => ProtectedAuthObject | Assert auth or redirect |
| redirectToSignIn(opts?) | ({ returnTo? }) => never | Redirect to sign-in page |
currentUser()
Returns the full user object, or null if not authenticated.
import { createServerFn } from "@tanstack/react-start";
import { currentUser } from "@inai-dev/tanstack-start/server";
const getProfile = createServerFn({ method: "GET" }).handler(async () => {
const user = await currentUser();
if (!user) return null;
return { email: user.email };
});
// Force a fresh fetch from the API (bypasses cached session cookie)
const freshUser = await currentUser({ fresh: true });Server Function Middleware
For fine-grained auth in individual server functions, use the function middleware instead of (or alongside) the request middleware.
Request vs Function middleware:
- Request middleware (
createInAIAuthMiddleware): Runs on every server request. Has access to theRequestobject. Can redirect unauthenticated users. Use increateStart({ requestMiddleware: [...] }).- Function middleware (
createInAIAuthFnMiddleware): Runs only on individual server function calls. Does NOT have access toRequest. Never redirects — setsauth: nulland lets the handler decide. Use increateServerFn().middleware([...]).- Guard middleware (
requireAuth): Composable guard. ThrowsResponse.json()with 401/403 if auth check fails. Chain aftercreateInAIAuthFnMiddleware.
createInAIAuthFnMiddleware()
Injects auth: AuthObject | null into the server function context. Never redirects — lets the handler decide the response.
import { createServerFn } from "@tanstack/react-start";
import { createInAIAuthFnMiddleware } from "@inai-dev/tanstack-start/middleware";
const authFn = createInAIAuthFnMiddleware();
const getProfile = createServerFn({ method: "GET" })
.middleware([authFn])
.handler(({ context }) => {
if (!context.auth) throw Response.json({ error: "Unauthorized" }, { status: 401 });
return fetchProfile(context.auth.userId);
});requireAuth()
Guard middleware that throws if the user is not authenticated or lacks a required role/permission. Use after createInAIAuthFnMiddleware.
import { createServerFn } from "@tanstack/react-start";
import {
createInAIAuthFnMiddleware,
requireAuth,
} from "@inai-dev/tanstack-start/middleware";
const authFn = createInAIAuthFnMiddleware();
const adminAction = createServerFn({ method: "POST" })
.middleware([authFn, requireAuth({ role: "admin" })])
.handler(({ context }) => {
// context.auth is guaranteed non-null with "admin" role
return performAdminAction(context.auth.userId);
});React Hooks
All hooks are imported from @inai-dev/tanstack-start.
useAuth()
const { isLoaded, isSignedIn, userId, roles, permissions, has, signOut } = useAuth();
has({ role: "admin" }); // check role
has({ permission: "read" }); // check permission
await signOut();useUser()
const { isLoaded, isSignedIn, user } = useUser();
// user: UserResource | nulluseSession()
const { isLoaded, isSignedIn, userId, tenantId, orgId, orgRole } = useSession();useOrganization()
const { isLoaded, orgId, orgRole } = useOrganization();useSignIn()
const { signIn, isLoading, error, status, reset } = useSignIn();
await signIn.create({ identifier: "[email protected]", password: "..." });
// status: "idle" | "loading" | "needs_mfa" | "complete" | "error"
// MFA flow
await signIn.attemptMFA({ code: "123456" });useSignUp()
const { signUp, isLoading, error, status, reset } = useSignUp();
await signUp.create({
email: "[email protected]",
password: "...",
firstName: "Jane",
lastName: "Doe",
});
// status: "idle" | "loading" | "needs_email_verification" | "complete" | "error"React Components
All components are imported from @inai-dev/tanstack-start.
<Protect>
Renders children only if the user has the required role or permission.
<Protect role="admin" fallback={<p>Access denied</p>}>
<AdminPanel />
</Protect>
<Protect permission="posts:write">
<Editor />
</Protect><SignedIn> / <SignedOut>
Conditional rendering based on authentication state.
<SignedIn>
<p>Welcome back!</p>
</SignedIn>
<SignedOut>
<p>Please sign in.</p>
</SignedOut><PermissionGate>
Permission-based access control.
<PermissionGate permission="billing:manage" fallback={<p>No access</p>}>
<BillingSettings />
</PermissionGate><UserButton>
User profile menu with avatar and dropdown.
<UserButton
afterSignOutUrl="/"
showName
menuItems={[{ label: "Settings", onClick: () => navigate({ to: "/settings" }) }]}
appearance={{ buttonSize: 36, buttonBg: "#1a1a2e" }}
/><SignIn>
Sign-in form with MFA support.
<SignIn
redirectUrl="/dashboard"
onSuccess={() => console.log("Signed in!")}
onMFARequired={(mfaToken) => navigate({ to: "/mfa" })}
/><OrganizationSwitcher>
Organization switching dropdown.
<OrganizationSwitcher />Advanced Configuration
configureAuth() / getAuthConfig()
Set global configuration early in your app (e.g., in app/ssr.tsx or a server initialization file).
import { configureAuth, getAuthConfig } from "@inai-dev/tanstack-start/server";
configureAuth({
signInUrl: "/login",
signUpUrl: "/register",
afterSignInUrl: "/dashboard",
afterSignOutUrl: "/",
publishableKey: "pk_live_...",
});
const config = getAuthConfig();
// { signInUrl, signUpUrl, afterSignInUrl, afterSignOutUrl, publishableKey }createRouteMatcher()
Create a reusable route matcher for middleware logic.
import { createRouteMatcher } from "@inai-dev/tanstack-start/middleware";
const isPublic = createRouteMatcher(["/", "/about", "/api/(.*)"]);
const authMiddleware = createInAIAuthMiddleware({
publicRoutes: isPublic,
});beforeAuth / afterAuth Hooks
Run custom logic before or after the auth check in request middleware.
import { createInAIAuthMiddleware } from "@inai-dev/tanstack-start/middleware";
const authMiddleware = createInAIAuthMiddleware({
publicRoutes: ["/", "/login"],
signInUrl: "/login",
beforeAuth: (request) => {
// Runs before auth check — return a Response to short-circuit
},
afterAuth: (auth, request) => {
// Runs after auth check on protected routes
const pathname = new URL(request.url).pathname;
if (auth.userId && pathname === "/login") {
return Response.redirect(new URL("/dashboard", request.url));
}
},
});Platform Auth
For admin panels using platform authentication:
// Middleware
const authMiddleware = createInAIAuthMiddleware({
authMode: "platform",
publicRoutes: ["/login"],
});
// API routes
import { createPlatformAuthRouteHandlers } from "@inai-dev/tanstack-start/server";
const { handleRequest } = createPlatformAuthRouteHandlers();
export const Route = createFileRoute("/api/platform/auth/$path")({
server: {
handlers: {
GET: ({ request, params }) => handleRequest(request, params.path),
POST: ({ request, params }) => handleRequest(request, params.path),
},
},
});Platform route handlers support:
POST /login,/register,/mfa-challenge,/refresh,/logoutGET /me
Exports Reference
@inai-dev/tanstack-start
| Export | Kind | Description |
|---|---|---|
| InAIAuthProvider | Component | Auth context provider |
| Protect | Component | Role/permission gate |
| SignedIn | Component | Renders when signed in |
| SignedOut | Component | Renders when signed out |
| PermissionGate | Component | Permission-based gate |
| UserButton | Component | User profile menu |
| SignIn | Component | Sign-in form |
| OrganizationSwitcher | Component | Org switcher |
| useAuth | Hook | Auth state & actions |
| useUser | Hook | User data |
| useSession | Hook | Session info |
| useSessionTimeout | Hook | Session timeout handling |
| useOrganization | Hook | Organization data |
| useSignIn | Hook | Sign-in flow |
| useSignUp | Hook | Sign-up flow |
| COOKIE_AUTH_TOKEN | Constant | "auth_token" |
| COOKIE_REFRESH_TOKEN | Constant | "refresh_token" |
| COOKIE_AUTH_SESSION | Constant | "auth_session" |
@inai-dev/tanstack-start/server
| Export | Kind | Description |
|---|---|---|
| auth | Function | Get ServerAuthObject (sync) |
| currentUser | Function | Get current user |
| createAuthRouteHandlers | Function | App auth route handlers |
| createPlatformAuthRouteHandlers | Function | Platform auth route handlers |
| configureAuth | Function | Set global config |
| getAuthConfig | Function | Get resolved config |
| setAuthCookies | Function | Set auth cookies |
| clearAuthCookies | Function | Clear auth cookies |
| isSessionExpired | Function | Check session max duration |
| getAuthTokenFromCookies | Function | Get access token |
| getRefreshTokenFromCookies | Function | Get refresh token |
@inai-dev/tanstack-start/middleware
| Export | Kind | Description |
|---|---|---|
| createInAIAuthMiddleware | Function | Request-level auth middleware |
| createInAIAuthFnMiddleware | Function | Server function auth middleware |
| requireAuth | Function | Auth guard middleware |
| createRouteMatcher | Function | Route pattern matcher |
| InAITanStackMiddlewareConfig | Type | Middleware config interface |
| RequireAuthConfig | Type | Guard config interface |
Exported Types
import type {
AuthObject,
ServerAuthObject,
ProtectedAuthObject,
UserResource,
PlatformUserResource,
SessionResource,
OrganizationResource,
InAIAuthConfig,
InAIAuthErrorBody,
SignInResult,
SignUpResult,
} from "@inai-dev/tanstack-start";
import type {
InAITanStackMiddlewareConfig,
RequireAuthConfig,
} from "@inai-dev/tanstack-start/middleware";Differences from @inai-dev/nextjs
| Concept | Next.js | TanStack Start |
|---------|---------|----------------|
| Read cookie | (await cookies()).get(name)?.value | getCookie(name) |
| Write cookie | (await cookies()).set(name, val, opts) | setCookie(name, val, opts) |
| Redirect | redirect(url) from next/navigation | throw redirect({ href }) from @tanstack/react-router |
| auth() | async (returns Promise) | sync (returns directly) |
| Middleware | export function middleware(req) returns NextResponse | createMiddleware().server(({ next }) => next({ context })) |
| Auth context | x-inai-auth header | next({ context: { auth } }) — type-safe |
| API routes | export { GET, POST } catch-all | createFileRoute()({ server: { handlers: { POST } } }) |
| Response | NextResponse.json() | Response.json() (Web API standard) |
| Error response | NextResponse.json({ error }, { status: 401 }) | throw Response.json({ error }, { status: 401 }) |
| Route handlers | createAuthRoutes() returns { GET, POST } | createAuthRouteHandlers() returns { handleRequest } |
Questions & Support
Visit https://inai.dev for documentation, guides, and support.
