Client
Call your Typebase server from any JavaScript or TypeScript app.
Last updated on
The client is what your frontend uses to call the actions defined in your typebase/ directory. It's a plain TypeScript library that works in any JS/TS environment: Node, the browser, edge runtimes, and React Native.
Every call is just a function call. The types come straight from your action's .input() and .output(), so the editor knows what you can pass in and what you'll get back without any code generation step on the client side. For example:
const todo = await client.queries.todos.getOne({ id: 1 });
// ^? { id: number; value: string; completed: boolean }The client utilities
Typebase ships these helpers, all from the typebase-io/client namespace. Pick the ones you need:
| Utility | Import | Use for |
|---|---|---|
createRouterClient | typebase-io/client | Simple promise-based calls (server code, scripts, anywhere async). |
createTanstackQueryClient | typebase-io/client | Reactive UI with caching, loading states, and refetching. |
createAuthClient | typebase-io/client/auth/<framework> | Sign-in, sign-up, sign-out, and session reads on the frontend. |
consumeStream | typebase-io/client | Reading a streaming action with callbacks. |
You can use them together. A common setup is createRouterClient for server-rendered pages and createTanstackQueryClient for interactive client components, both pointing at the same Typebase server.
createRouterClient
Every action becomes a typed async function.
import { createRouterClient } from 'typebase-io/client';
import type { Router } from '../../typebase/_generated/server';
export const client = createRouterClient<Router>({
url: process.env.TYPEBASE_APP_URL_LOCAL || process.env.TYPEBASE_APP_URL_DEV || process.env.TYPEBASE_APP_URL || '',
});That chain is local run, then dev, then prod — see the server URL.
Then call any action like a regular function:
const todos = await client.queries.todos.getMany();
await client.mutations.todos.create({ value: 'Buy milk' });The Router type is generated from your actions/ folder by npx typebase-io-cli codegen. The shape of the client always mirrors the folder structure (client.queries.todos.getMany, client.mutations.todos.create, and so on).
createTanstackQueryClient
The same client, wrapped with TanStack Query utilities so each action exposes queryOptions, mutationOptions, and helpers like key() for cache invalidation.
import { createTanstackQueryClient } from 'typebase-io/client';
import type { Router } from '../../typebase/_generated/server';
export const client = createTanstackQueryClient<Router>({
url: process.env.NEXT_PUBLIC_TYPEBASE_APP_URL || '',
});const { data } = useQuery(client.queries.todos.getMany.queryOptions());
const mutation = useMutation(client.mutations.todos.create.mutationOptions());The URL must be reachable from the browser, so it needs whatever prefix your framework uses for public env vars (NEXT_PUBLIC_, PUBLIC_, EXPO_PUBLIC_, etc.).
createAuthClient
When your project has an auth.ts file, Typebase re-exports a framework-specific better-auth client so sign-in, sign-up, and session reads are wired up to your Typebase server out of the box.
import { createAuthClient } from 'typebase-io/client/auth/react';
export const authClient = createAuthClient();The framework suffix changes depending on what you're using:
| Framework | Import |
|---|---|
| React, Next.js | typebase-io/client/auth/react |
| Svelte | typebase-io/client/auth/svelte |
| Vue, Nuxt | typebase-io/client/auth/vue |
The returned authClient is the same object documented in the better-auth client docs. Typebase just configures it for you.
consumeStream
Before calling consumeStream, create or import a client with either createRouterClient or createTanstackQueryClient. An action written with .stream() resolves to an async iterable instead of a value, but the syntax for starting that stream depends on which client you chose:
| Client | Stream call |
|---|---|
createRouterClient | client.queries.todos.getMany() |
createTanstackQueryClient | client.queries.todos.getMany.call() |
You can read the result directly with for await, or use consumeStream for callbacks and an unsubscribe function. Calling consumeStream starts consuming immediately. Each call creates a separate subscription; it does not memoize or deduplicate streams for you.
In UI components, start the stream from the framework's mount lifecycle and call unsubscribe during cleanup. Never call consumeStream while a
component is rendering—not even as useRef(consumeStream(...)), because that argument is evaluated again on every render. Otherwise every event
that updates state can rerender the component, open another long-lived stream, and eventually prevent other requests from completing.
With createRouterClient
import { consumeStream, createRouterClient } from 'typebase-io/client';
import type { Router } from '../../typebase/_generated/server';
const client = createRouterClient<Router>({
url: process.env.NEXT_PUBLIC_TYPEBASE_APP_URL || '',
});
const unsubscribe = consumeStream(client.queries.todos.getMany(), {
onEvent: (todos) => render(todos),
onError: (error) => console.error(error),
onSuccess: () => console.log('stream ended'),
});
// When the code that owns this subscription stops
void unsubscribe();With createTanstackQueryClient
The TanStack client wraps each action in query helpers. Use its .call() method to start a stream before passing it to consumeStream:
import { consumeStream, createTanstackQueryClient } from 'typebase-io/client';
import type { Router } from '../../typebase/_generated/server';
const client = createTanstackQueryClient<Router>({
url: process.env.NEXT_PUBLIC_TYPEBASE_APP_URL || '',
});
const unsubscribe = consumeStream(client.queries.todos.getMany.call(), {
onEvent: (todos) => render(todos),
onError: (error) => console.error(error),
onSuccess: () => console.log('stream ended'),
});
// When the code that owns this subscription stops
void unsubscribe();React lifecycle
In React, create the subscription inside useEffect and return a synchronous cleanup function that starts the async unsubscribe:
import { consumeStream } from 'typebase-io/client';
import type { InferStreamEvent } from 'typebase-io/server';
import { useEffect, useState } from 'react';
import { client } from '../lib/typebase/client';
import type { RouterOutputs } from '../typebase/_generated/server';
type Todos = InferStreamEvent<RouterOutputs['queries']['todos']['getMany']>;
export function TodoList() {
const [todos, setTodos] = useState<Todos>([]);
useEffect(() => {
const unsubscribe = consumeStream(client.queries.todos.getMany.call(), {
onEvent: (nextTodos) => setTodos(nextTodos),
onError: (error) => console.error(error),
});
return () => {
void unsubscribe();
};
}, []);
return todos.map((todo) => <div key={todo.id}>{todo.value}</div>);
}InferStreamEvent is a type-only import, so nothing from typebase-io/server reaches your client bundle. It's covered in Typing action inputs and outputs.
This example uses a client created with createTanstackQueryClient, so the stream call ends in .call(). With createRouterClient, use client.queries.todos.getMany() instead. The same rule applies in other frameworks: subscribe when the component mounts and unsubscribe when it unmounts.
| Callback | When |
|---|---|
onEvent | Each event. |
onError | The stream failed. |
onSuccess | The stream ended normally, including after unsubscribe. |
onFinish | After onError or onSuccess, whichever happened. |
onError or onFinish. Without either, a failing stream throws an unhandled rejection.The server URL
createRouterClient and createTanstackQueryClient both take the same url, and both treat it the same way. Pass your server's base URL: when url is a string, the client appends /rpc for you and tidies up any trailing slashes, so all three of these end up at https://api.example.com/rpc:
createRouterClient<Router>({ url: 'https://api.example.com' });
createRouterClient<Router>({ url: 'https://api.example.com/' });
createRouterClient<Router>({ url: 'https://api.example.com///' });A path is kept: https://api.example.com/base becomes https://api.example.com/base/rpc.
/rpc is appended unconditionally, not only when it's missing. Passing https://api.example.com/rpc yourself gives you
https://api.example.com/rpc/rpc and every call 404s. Pass the base URL and let the client add it.
Anything that isn't a string, a URL or a function, is passed straight through untouched, so you keep full control when you need it.
Which URL to pass
The CLI writes up to three URLs into your project-root .env, under different keys so they coexist:
| Key | Written by | Points at |
|---|---|---|
TYPEBASE_APP_URL_LOCAL | start, on every local run | your own machine |
TYPEBASE_APP_URL_DEV | deploy dev | the dev branch |
TYPEBASE_APP_URL | deploy prod | the prod branch |
None of them is read automatically — Typebase has no URL-from-environment resolution, so you pick the order yourself. Local first, then dev, then prod is the usual chain: a local run wins whenever one is running, and removing the key from .env falls back to dev without touching the client.
url: process.env.TYPEBASE_APP_URL_LOCAL || process.env.TYPEBASE_APP_URL_DEV || process.env.TYPEBASE_APP_URL || '';Browser code can't read those directly and needs your framework's public prefix instead. Your integration guide has the exact form.
Plugins
Both createRouterClient and createTanstackQueryClient take a plugins array. Plugins wrap every request the client makes, and are how you add behaviour like retries, request deduplication, or batching.
The most common one is retrying, because a dropped stream stays dropped without it:
import { createRouterClient } from 'typebase-io/client';
import { ClientRetryPlugin } from 'typebase-io/client/plugins';
import type { Router } from '../../typebase/_generated/server';
export const client = createRouterClient<Router>({
url: process.env.NEXT_PUBLIC_TYPEBASE_APP_URL || '',
plugins: [new ClientRetryPlugin({ default: { retry: Number.POSITIVE_INFINITY } })],
});That also makes resuming work: with retries on, a reconnecting client sends the last event it received, and the server picks up from there instead of from the present. See Streaming for the server side of it.
Every oRPC client plugin is re-exported from typebase-io/client/plugins, so there's no extra dependency to install and no second copy of the library to keep in sync.
Only Client plugins are supported for now. Adding your own plugins to the generated server isn't supported yet — it's on the roadmap.
Typing action inputs and outputs
Sometimes you need an action's return type or input type outside of a call. Instead of re-declaring the shape by hand, use the generated RouterInputs and RouterOutputs maps:
import type { RouterInputs, RouterOutputs } from '../../typebase/_generated/server';
type Todo = RouterOutputs['queries']['todos']['getOne'];
function TodoDetails({ todo }: { todo: Todo }) {
/* ... */
}Both maps mirror the router structure, so the path is the same one you use to call the action. See Generated Code for details.
A streaming action sends many events instead of one response, so its RouterOutputs entry is the async iterable you consume, not the shape of an event. InferStreamEvent takes that entry and gives you one event:
import type { InferStreamEvent } from 'typebase-io/server';
import type { RouterOutputs } from '../../typebase/_generated/server';
type Todo = RouterOutputs['queries']['todos']['getOne']; // a handler: what it returns
type Todos = InferStreamEvent<RouterOutputs['queries']['todos']['getMany']>; // a stream: one eventIt's the type you want for the state a stream feeds and for the parameter of an onEvent callback you declare yourself. Import it with import type; it's erased at compile time, so a client component pulls nothing from typebase-io/server at runtime.
Auth helpers per framework
When your app's domain differs from the Typebase server's domain (the typical setup), auth cookies need to be proxied through your app so they can be set as secure, HttpOnly cookies. Typebase ships small framework-specific helpers for this:
| Framework | Import path | Exports |
|---|---|---|
| Next.js | typebase-io/client/auth/nextjs | proxyToTypebase, getServerSession, getServerAuthCookie |
| SvelteKit | typebase-io/client/auth/svelte-kit | proxyToTypebase, getServerSession, getServerAuthCookie |
| Nuxt | typebase-io/client/auth/nuxt | proxyToTypebase, getServerSession, getServerAuthCookie |
See your framework's integration guide below for how to wire them up.
Framework-specific guides
This page is the framework-agnostic overview. For the full setup (providers, proxies, server-side session reads, route protection, environment variables), use the guide for your framework: