npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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 Unauthorized with a refresh flow.
  • CSRF protection for mutating requests.
  • Socket.IO connection management, reconnect support, and room rejoin.
  • Offline event queueing.
  • TokenManager for access token storage and refresh scheduling.
  • RefreshManager for silent token renewal.
  • React hooks and a provider for clean UI integration.

Installation

npm install realtimehttpauthclient
bun add realtimehttpauthclient

Recommended 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:

  1. User logs in with Better Auth.
  2. Server stores the auth session securely.
  3. Client requests a short-lived app access token from your backend.
  4. createRealtimeClient() uses that token for HTTP and Socket.IO.
  5. 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:

  • http
  • socket
  • token
  • refresh
  • connect()
  • 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,
  • getAccessToken returned null.

403 CSRF token mismatch

Usually means:

  • CSRF cookie is missing,
  • the CSRF header name is wrong,
  • withCredentials is disabled,
  • CORS is not configured correctly.

Socket connection fails

Usually means:

  • the token passed in socket.auth is 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
createRefreshManager

License

MIT