realtimehttpauthclient
v1.0.5
Published
A professional real-time HTTP client with automatic authentication lifecycle, JWT token rotation, CSRF protection, and Socket.IO integration for React, Bun, Node.js, and TanStack Start.
Downloads
768
Maintainers
Readme
realtimehttpauthclient
A professional HTTP + Realtime client for TanStack Start, React, Bun, and Node.js. It focuses on the transport layer: typed HTTP requests, Socket.IO realtime communication, CSRF protection, token lifecycle management, offline queueing, and React hooks.
It does not include application auth screens or auth UI helpers like login, register, logout, useAuth, useRequireAuth, or useRBAC. Those belong in your auth system, for example Better Auth. Better Auth provides client session access with useSession(), server session lookup with auth.api.getSession({ headers }), and a TanStack Start integration guide. TanStack Start also emphasizes that beforeLoad is useful for route UX but is not the security boundary; private data must be protected in the endpoint that serves it. ([Better Auth][1])
What this package gives you
- Typed HTTP requests with automatic
Authorization: Bearer ...support. - Automatic retry on
401 Unauthorizedwith a refresh flow. - CSRF protection for mutating requests.
- Socket.IO connection management, reconnect support, and room rejoin.
- Offline event queueing.
TokenManagerfor access token storage and refresh scheduling.RefreshManagerfor silent token renewal.- React hooks and a provider for clean UI integration.
Installation
npm install realtimehttpauthclientbun add realtimehttpauthclientRecommended architecture
Use Better Auth for identity and session, Hono for API routes, and realtimehttpauthclient for HTTP + realtime transport. Better Auth supports client-side session access with createAuthClient() and useSession(), and Hono documents the CORS setup needed for browser clients calling an API from another origin. ([Better Auth][2])
A common flow is:
- User logs in with Better Auth.
- Server stores the auth session securely.
- Client requests a short-lived app access token from your backend.
createRealtimeClient()uses that token for HTTP and Socket.IO.- Protected API routes verify the session or token on the server.
Full example: TanStack Start app + Hono + Better Auth + Todo CRUD + realtime
1. Backend setup
src/server/auth.ts
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { db } from "./db";
export const auth = betterAuth({
database: prismaAdapter(db, {
provider: "postgresql",
}),
trustedOrigins: [
"http://localhost:3000",
"http://localhost:5173",
],
emailAndPassword: {
enabled: true,
},
});Better Auth documents that you create a server instance first, then a client via createAuthClient() on the frontend. ([Better Auth][2])
src/server/token.ts
import jwt from "jsonwebtoken";
const APP_JWT_SECRET = process.env.APP_JWT_SECRET!;
export function signAppToken(payload: { userId: string; email: string }, expiresIn: string) {
return jwt.sign(payload, APP_JWT_SECRET, { expiresIn });
}
export function verifyAppToken(token: string) {
return jwt.verify(token, APP_JWT_SECRET) as { userId: string; email: string; iat: number; exp: number };
}src/server/app.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { getCookie, setCookie, deleteCookie } from "hono/cookie";
import { auth } from "./auth";
import { signAppToken, verifyAppToken } from "./token";
type Variables = {
session?: Awaited<ReturnType<typeof auth.api.getSession>>;
};
const app = new Hono<{ Variables: Variables }>();
app.use(
"/api/*",
cors({
origin: "http://localhost:5173",
credentials: true,
allowHeaders: ["Content-Type", "Authorization", "X-CSRF-Token"],
exposeHeaders: ["X-CSRF-Token"],
})
);Hono’s CORS middleware documentation shows the standard browser cross-origin setup, including credentials support. Hono also provides cookie helpers like getCookie, setCookie, and deleteCookie. ([Hono][3])
Better Auth routes
app.on(["GET", "POST"], "/api/auth/*", (c) => {
return auth.handler(c.req.raw);
});Better Auth documents the TanStack Start integration pattern and the general client/server usage model. The Hono example page also demonstrates Better Auth with Hono. ([Better Auth][1])
Session middleware
async function requireSession(c: any, next: any) {
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
return c.json({ error: "Unauthorized" }, 401);
}
c.set("session", session);
await next();
}Better Auth documents server-side session retrieval with auth.api.getSession({ headers }). ([Better Auth][4])
CSRF endpoint
app.get("/api/auth/csrf", (c) => {
const csrfToken = crypto.randomUUID();
setCookie(c, "csrf-token", csrfToken, {
httpOnly: false,
secure: process.env.NODE_ENV === "production",
sameSite: "Lax",
path: "/",
});
return c.json({ csrfToken });
});App access token endpoint for realtimehttpauthclient
app.post("/api/realtime/token", requireSession, async (c) => {
const session = c.get("session");
const accessToken = signAppToken(
{
userId: session.user.id,
email: session.user.email,
},
"15m"
);
return c.json({ accessToken });
});This token is separate from the Better Auth session. The session stays secure in cookies, while the app token is used for HTTP bearer auth and Socket.IO auth.
Todo CRUD routes
app.get("/api/todos", requireSession, async (c) => {
const session = c.get("session");
const todos = await db.todo.findMany({
where: { userId: session.user.id },
orderBy: { createdAt: "desc" },
});
return c.json(todos);
});
app.post("/api/todos", requireSession, async (c) => {
const session = c.get("session");
const body = await c.req.json<{ title: string }>();
const todo = await db.todo.create({
data: {
title: body.title,
completed: false,
userId: session.user.id,
},
});
return c.json(todo, 201);
});
app.patch("/api/todos/:id", requireSession, async (c) => {
const session = c.get("session");
const id = c.req.param("id");
const body = await c.req.json<{ title?: string; completed?: boolean }>();
const existing = await db.todo.findFirst({
where: { id, userId: session.user.id },
});
if (!existing) {
return c.json({ error: "Not found" }, 404);
}
const todo = await db.todo.update({
where: { id },
data: body,
});
return c.json(todo);
});
app.delete("/api/todos/:id", requireSession, async (c) => {
const session = c.get("session");
const id = c.req.param("id");
const existing = await db.todo.findFirst({
where: { id, userId: session.user.id },
});
if (!existing) {
return c.json({ error: "Not found" }, 404);
}
await db.todo.delete({ where: { id } });
return c.json({ ok: true });
});TanStack Start documentation specifically warns that protecting data must happen in the endpoint, not only in beforeLoad. ([TanStack][5])
2. Frontend auth client
src/client/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_API_URL,
});Better Auth documents createAuthClient() and useSession() for React. ([Better Auth][2])
3. Frontend realtime client
src/client/realtime-client.ts
import { createRealtimeClient } from "realtimehttpauthclient";
const API_URL = import.meta.env.VITE_API_URL;
async function fetchRealtimeToken() {
const res = await fetch(`${API_URL}/api/realtime/token`, {
method: "POST",
credentials: "include",
});
if (!res.ok) return null;
const data = await res.json();
return data.accessToken ?? null;
}
let browserClient: ReturnType<typeof createRealtimeClient> | null = null;
export function getRealtimeClient() {
if (typeof window === "undefined") {
return createRealtimeClient({
apiBaseUrl: API_URL,
socketUrl: API_URL,
withCredentials: true,
persistToken: false,
getAccessToken: async () => null,
csrf: {
enabled: true,
fetchEndpoint: "/api/auth/csrf",
},
refresh: {
enabled: false,
endpoint: `${API_URL}/api/realtime/token`,
},
});
}
if (!browserClient) {
browserClient = createRealtimeClient({
apiBaseUrl: API_URL,
socketUrl: API_URL,
withCredentials: true,
persistToken: false,
getAccessToken: async () => {
const cached = localStorage.getItem("app_access_token");
if (cached) return cached;
const token = await fetchRealtimeToken();
if (token) localStorage.setItem("app_access_token", token);
return token;
},
setAccessToken: (token) => {
if (token) localStorage.setItem("app_access_token", token);
else localStorage.removeItem("app_access_token");
},
csrf: {
enabled: true,
fetchEndpoint: "/api/auth/csrf",
},
refresh: {
enabled: true,
endpoint: `${API_URL}/api/realtime/token`,
method: "POST",
},
offlineQueue: {
enabled: true,
storageKey: "todos-realtime-queue",
maxItems: 100,
},
rooms: {
autoRejoin: true,
},
});
}
return browserClient;
}
export const realtimeClient = getRealtimeClient();For isomorphic apps, keeping a singleton in the browser is practical, but you should avoid sharing mutable request-scoped state on the server. TanStack Start’s execution model and server-function docs explain why code location matters and why private data must remain server-protected per request. ([TanStack][6])
4. TanStack Start provider
src/App.tsx
import { RealtimeProvider } from "realtimehttpauthclient";
import { realtimeClient } from "./client/realtime-client";
export function AppShell({ children }: { children: React.ReactNode }) {
return (
<RealtimeProvider client={realtimeClient} autoConnect={true}>
{children}
</RealtimeProvider>
);
}5. Login page with Better Auth
import { useState } from "react";
import { authClient } from "../client/auth-client";
import { useNavigate } from "@tanstack/react-router";
export function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
const submit = async (e: React.FormEvent) => {
e.preventDefault();
await authClient.signIn.email({
email,
password,
fetchOptions: {
onSuccess: async () => {
navigate({ to: "/todos" });
},
},
});
};
return (
<form onSubmit={submit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input value={password} type="password" onChange={(e) => setPassword(e.target.value)} />
<button type="submit">Login</button>
</form>
);
}Better Auth documents the client pattern and session hook for React. ([Better Auth][2])
6. Protected route in TanStack Start
import { createFileRoute, redirect } from "@tanstack/react-router";
import { authClient } from "../client/auth-client";
export const Route = createFileRoute("/todos")({
beforeLoad: async () => {
const { data: session } = await authClient.getSession();
if (!session) {
throw redirect({ to: "/login" });
}
return { session };
},
component: TodosPage,
});TanStack Start documentation explains that beforeLoad is good for route UX, but your server endpoints still need to enforce security. ([TanStack][7])
7. Todo page with HTTP + realtime
import { useEffect, useState } from "react";
import { useHttpClient, useSocketEvent, useSocketClient } from "realtimehttpauthclient";
type Todo = {
id: string;
title: string;
completed: boolean;
};
export function TodosPage() {
const http = useHttpClient();
const { connected, emit } = useSocketClient();
const [todos, setTodos] = useState<Todo[]>([]);
const [title, setTitle] = useState("");
const loadTodos = async () => {
const data = await http.get<Todo[]>("/api/todos");
setTodos(data);
};
useEffect(() => {
void loadTodos();
}, []);
useSocketEvent<Todo>("todo:created", (todo) => {
setTodos((prev) => [todo, ...prev]);
});
useSocketEvent<Todo>("todo:updated", (todo) => {
setTodos((prev) => prev.map((t) => (t.id === todo.id ? todo : t)));
});
useSocketEvent<{ id: string }>("todo:deleted", ({ id }) => {
setTodos((prev) => prev.filter((t) => t.id !== id));
});
const createTodo = async () => {
const todo = await http.post<Todo, { title: string }>("/api/todos", { title });
setTitle("");
emit("todo:create", todo);
};
const toggleTodo = async (id: string, completed: boolean) => {
await http.patch(`/api/todos/${id}`, { completed });
};
const deleteTodo = async (id: string) => {
await http.delete(`/api/todos/${id}`);
};
return (
<div>
<div>{connected ? "connected" : "offline"}</div>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button onClick={createTodo}>Add</button>
{todos.map((todo) => (
<div key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={(e) => toggleTodo(todo.id, e.target.checked)}
/>
{todo.title}
</label>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</div>
))}
</div>
);
}realtimehttpauthclient API
createRealtimeClient(config)
Creates a client instance that bundles:
httpsockettokenrefreshconnect()disconnect()emit()emitAck()joinRoom()leaveRoom()- React-aware state helpers
initializeHttp(config)
Initializes the global http export.
Use this when you want to call http.get(...) directly without a React provider.
http
Standalone HTTP client.
import { http } from "realtimehttpauthclient";
const todos = await http.get("/api/todos");TokenManager
The TokenManager handles access token storage, token change events, optional sessionStorage persistence, and proactive JWT expiry handling.
Typical usage:
import { createTokenManager } from "realtimehttpauthclient";
const tokenManager = createTokenManager({
getAccessToken: async () => localStorage.getItem("app_access_token"),
setAccessToken: (token) => {
if (token) localStorage.setItem("app_access_token", token);
else localStorage.removeItem("app_access_token");
},
persistToken: false,
});RefreshManager
The RefreshManager calls your refresh endpoint and updates the TokenManager when a fresh token is returned.
import { createRefreshManager } from "realtimehttpauthclient";
const refreshManager = createRefreshManager(
{
enabled: true,
endpoint: "/api/realtime/token",
method: "POST",
maxRetries: 3,
},
tokenManager,
() => {
console.log("Unauthorized");
}
);React hooks
useRealtimeClient()
Returns the full client instance.
useHttpClient()
Returns the HTTP client.
useConnectionState()
Returns { connected, connecting }.
useSocketClient()
Returns socket helpers such as emit, emitAck, joinRoom, and leaveRoom.
useSocketEvent(event, handler)
Listens to a global socket event and cleans up automatically.
useRoomEvent(room, event, handler)
Joins a room on mount, listens to an event, and leaves the room on unmount.
Security and best practices
Browser singleton only
In the browser, a singleton client is convenient and efficient. On the server, do not share mutable token state between requests. TanStack Start’s docs on execution model and server functions make it clear that the server-side context is request-scoped and that private data must be protected in the endpoint itself. ([TanStack][6])
Protect data on the server
Even if the UI redirects away from a page, your Hono routes must still verify session or token before reading or writing private data. TanStack Start explicitly recommends protecting data in the endpoint that serves it. ([TanStack][5])
Always enable CORS correctly
If frontend and backend are on different origins, you need credentials: true on the browser side and matching CORS settings on the API side. Hono documents the CORS middleware for this exact use case. ([Hono][3])
Keep access tokens short-lived
A short-lived app access token is a good fit for HTTP bearer auth and Socket.IO handshake auth. The long-lived login session should remain managed by Better Auth.
Troubleshooting
401 Unauthorized
Usually means:
- the Better Auth session is missing,
- the app access token is expired,
- the refresh endpoint failed,
getAccessTokenreturnednull.
403 CSRF token mismatch
Usually means:
- CSRF cookie is missing,
- the CSRF header name is wrong,
withCredentialsis disabled,- CORS is not configured correctly.
Socket connection fails
Usually means:
- the token passed in
socket.authis invalid, - the socket server rejected the handshake,
- the token refresh endpoint returned no token.
Exports
createRealtimeClient
initializeHttp
http
RealtimeProvider
useRealtimeClient
useHttpClient
useConnectionState
useSocketClient
useSocketEvent
useRoomEvent
TokenManager
RefreshManager
createTokenManager
createRefreshManagerLicense
MIT
