@nightmar3/uauth-tanstack-start
v1.0.1
Published
TanStack Start integration for Universal Auth SDK - Zero config authentication with full SSR support
Maintainers
Readme
@nightmar3/uauth-tanstack-start
**TanStack Start integration for Universal Auth SDK. Provides server-first authentication with full SSR support.
[!IMPORTANT] Backend Required: This SDK requires a backend API. See Backend Requirements for the complete API contract, or use our Backend Implementation Guide for step-by-step instructions.
Features
- ✅ Zero-config setup - Works with environment variables
- ✅ Full SSR support - Server-first authentication
- ✅ Security-first - Read-only client hooks, server-only validation
- ✅ Type-safe - Full TypeScript support
- ✅ Middleware - Route protection with
beforeLoad - ✅ Tiny - Minimal bundle size
- ✅ Framework-native - Uses TanStack Start patterns
Installation
npm install @nightmar3/uauth-tanstack-startQuick Start
1. Set Environment Variables
# .env
AUTH_URL=https://api.yourapp.com/auth
SESSION_SECRET=your-secret-key-min-32-characters2. Set Up Root Route
// routes/__root.tsx
import { createRootRoute } from '@tanstack/react-router'
import { getSessionFn } from '@nightmar3/uauth-tanstack-start/server'
export const Route = createRootRoute({
loader: async () => {
const session = await getSessionFn()
return {
user: session?.user ?? null,
isAuthenticated: !!session?.user,
}
},
})3. Use in Components
// components/Profile.tsx
import { useAuth } from '@nightmar3/uauth-tanstack-start/client'
export function Profile() {
const { user, isAuthenticated, signOut } = useAuth()
if (!isAuthenticated) {
return <div>Please log in</div>
}
return (
<div>
<p>Hello, {user.name}</p>
<button onClick={signOut}>Sign Out</button>
</div>
)
}4. Protect Routes
// routes/dashboard.tsx
import { createFileRoute } from '@tanstack/react-router'
import { createAuthBeforeLoad } from '@nightmar3/uauth-tanstack-start/middleware'
export const Route = createFileRoute('/dashboard')({
beforeLoad: createAuthBeforeLoad({ redirectTo: '/login' }),
component: DashboardPage,
})API Reference
Server Functions
getSessionFn()
Get current session (auto-refreshes tokens if expired)
const session = await getSessionFn()
// Returns: { user, accessToken } | nullsignInFn({ data })
Sign in with email and password
const result = await signInFn({
data: { email: '[email protected]', password: 'password' }
})signUpFn({ data })
Create new user account
const result = await signUpFn({
data: { email: '[email protected]', password: 'password', name: 'John' }
})signOutFn()
Sign out and clear session
await signOutFn()Client Hooks
useAuth()
Access auth state and methods
const { user, isAuthenticated, signIn, signUp, signOut } = useAuth()Note: All data is read-only from server. Auth methods call server functions and trigger router invalidation.
useUser()
Get current user
const user = useUser()useSession()
Get full session
const session = useSession()Components
<SignedIn>
Render children only when authenticated
<SignedIn>
<UserDashboard />
</SignedIn><SignedOut>
Render children only when NOT authenticated
<SignedOut>
<LoginPrompt />
</SignedOut><AuthGate>
Conditional rendering with fallback
<AuthGate fallback={<LoginPrompt />} loading={<Spinner />}>
<UserDashboard />
</AuthGate>Middleware
createAuthBeforeLoad(options)
Route protection helper
export const Route = createFileRoute('/protected')({
beforeLoad: createAuthBeforeLoad({ redirectTo: '/login' }),
})requireAuth
Simple auth requirement
export const Route = createFileRoute('/protected')({
beforeLoad: requireAuth,
})createAuthMiddleware()
Server function middleware
const authMiddleware = createAuthMiddleware()
export const protectedFn = createServerFn()
.middleware([authMiddleware])
.handler(async ({ context }) => {
// context.user is guaranteed to exist
return { userId: context.user.id }
})Security Architecture
Server-First Design
- ✅ All auth data comes from server via loaders
- ✅ Client hooks are read-only consumers
- ✅ No client-side auth state manipulation
- ✅ Server validates every operation
- ✅ HTTP-only cookies prevent XSS
How It Works
User Action (e.g., Sign In)
↓
useAuth().signIn() (client)
↓
signInFn() (server function)
↓
Server validates credentials
↓
Server updates session (HTTP-only cookies)
↓
Router invalidates
↓
Root loader re-runs
↓
Fresh user data from server
↓
UI updatesOAuth Support
Setup OAuth Providers
// Use the useOAuth hook in your login component
import { useOAuth } from '@nightmar3/uauth-tanstack-start/client'
function LoginPage() {
const { providers, signInWithOAuth } = useOAuth()
return (
<div>
{providers.map((provider) => (
<button
key={provider.name}
onClick={async () => {
await signInWithOAuth(provider.name)
window.location.href = '/dashboard'
}}
>
Sign in with {provider.displayName}
</button>
))}
</div>
)
}OAuth Callback Route
Create a callback route to handle OAuth redirects:
// routes/auth/callback.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useEffect } from 'react'
export const Route = createFileRoute('/auth/callback')({
component: AuthCallback,
})
function AuthCallback() {
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const code = params.get('code')
const error = params.get('error')
const error_description = params.get('error_description')
if (window.opener) {
window.opener.postMessage(
{
type: 'oauth2_callback',
code,
error,
error_description,
},
window.location.origin
)
setTimeout(() => {
window.close()
}, 100)
}
}, [])
return <div>Processing authentication...</div>
}Configuration
Environment Variables (Required)
# Required for session encryption
SESSION_SECRET=your-secret-key-min-32-characters
# Required for API communication
AUTH_URL=https://api.yourapp.com/auth
# Optional: OAuth provider credentials (if using OAuth)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secretImportant:
SESSION_SECRETmust be set when running the production build. It's validated at runtime, not build time.
Programmatic Configuration
import { configureAuth } from '@nightmar3/uauth-tanstack-start'
configureAuth({
baseURL: 'https://api.yourapp.com/auth',
sessionSecret: process.env.SESSION_SECRET!,
cookies: {
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
},
})Backend Requirements
The SDK expects a backend API that implements the following endpoints. You can use our FastAPI reference implementation or build your own.
Endpoints
| Method | Path | Description | Request Body | Response Data |
|--------|------|-------------|--------------|---------------|
| POST | /sign-in/password | Sign in with email/password | { email, password } | { user, tokens } |
| POST | /sign-in/oauth2 | Exchange OAuth code for tokens | { provider, code, redirect_uri } | { user, tokens } |
| POST | /sign-up | Create new account | { email, password, name? } | { user, tokens } |
| DELETE| /session | Sign out (revoke tokens) | - | { ok: boolean } |
| GET | /session | Get current user session | - | { user } |
| POST | /token/refresh | Refresh access token | { refresh_token } | { tokens } |
| GET | /providers | List available OAuth providers | - | { providers } |
Response Format
All API responses must follow this envelope structure:
interface ApiResponse<T> {
ok: boolean
data: T | null
error: {
code: string
message: string
details?: any
} | null
}Token Structure
The backend must return tokens in this format:
interface AuthTokens {
access_token: string
refresh_token: string
expires_in: number // seconds
}OAuth Provider Response (Optional)
If implementing OAuth support, the /providers endpoint must return:
interface OAuth2Provider {
name: string
displayName: string
clientId: string
authorizationUrl: string
scope?: string
}Example:
{
"ok": true,
"data": {
"providers": [
{
"name": "google",
"displayName": "Google",
"clientId": "your-google-client-id",
"authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
"scope": "openid email profile"
}
]
},
"error": null
}Production Deployment
Building for Production
npm run buildRunning the Production Build
You must provide environment variables when running the production server:
# Option 1: Inline environment variables
SESSION_SECRET="your-secret" AUTH_URL="https://api.yourapp.com/auth" npm start
# Option 2: Load from .env file
export $(cat .env | xargs) && npm start
# Option 3: Use a process manager like PM2
pm2 start .output/server/index.mjs --name "my-app" --env productionDeployment Platforms
For platforms like Vercel, Netlify, or Railway:
- Set environment variables in your platform's dashboard
- Deploy your application
- The platform will automatically set the variables at runtime
Required Environment Variables for Production:
SESSION_SECRET(min 32 characters)AUTH_URL(your backend API URL)- OAuth credentials (if using OAuth)
Examples
Login Page
import { useAuth } from '@nightmar3/uauth-tanstack-start/client'
import { useState } from 'react'
export function LoginPage() {
const { signIn } = useAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const result = await signIn(email, password)
if (!result.ok) {
alert(result.error?.message)
}
// Router will auto-redirect after successful login
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Sign In</button>
</form>
)
}Protected Route with Data
// routes/dashboard.tsx
import { createFileRoute } from '@tanstack/react-router'
import { createAuthBeforeLoad } from '@nightmar3/uauth-tanstack-start/middleware'
export const Route = createFileRoute('/dashboard')({
beforeLoad: createAuthBeforeLoad({ redirectTo: '/login' }),
loader: async ({ context }) => {
// context.user is available from beforeLoad
const data = await fetchUserData(context.user.id)
return { data }
},
component: DashboardPage,
})
function DashboardPage() {
const { data } = Route.useLoaderData()
const { user } = useAuth()
return (
<div>
<h1>Welcome, {user.name}</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}Migration from Next.js
If you're migrating from @nightmar3/uauth-next:
Replace package:
npm uninstall @nightmar3/uauth-next npm install @nightmar3/uauth-tanstack-startUpdate imports:
// Before import { useAuth } from '@nightmar3/uauth-next/client' // After import { useAuth } from '@nightmar3/uauth-tanstack-start/client'Replace middleware with
beforeLoad:// Before (Next.js middleware) export default authMiddleware({ protectedRoutes: ['/dashboard'] }) // After (TanStack Start beforeLoad) export const Route = createFileRoute('/dashboard')({ beforeLoad: requireAuth, })
License
MIT © nightmar3
