SDK

The React SDK gives you a typed client, auth helpers, and hooks to connect your frontend to Based.

Install

terminal
bun add @weirdscience/based-client
# or: npm install @weirdscience/based-client
# or: pnpm add @weirdscience/based-client

Create a client

lib/based.ts
import { createClient } from "@weirdscience/based-client";

export const based = createClient({
  url: process.env.NEXT_PUBLIC_BASED_URL!,
  anonKey: process.env.NEXT_PUBLIC_BASED_ANON_KEY!,
});

Wrap your app

BasedProvider makes the client available to all hooks in the component tree.

app/layout.tsx
import { BasedProvider } from "@weirdscience/based-client";
import { based } from "@/lib/based";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <BasedProvider client={based}>
      {children}
    </BasedProvider>
  );
}

Type Safety

The SDK supports end-to-end type safety through generated TypeScript interfaces. Run based typegen to generate a based.d.ts file containing typed interfaces for all your tables.

terminal
based typegen

This produces a file like the following with interfaces matching your database schema:

based.d.ts
export interface Tables {
  posts: {
    id: string;
    title: string;
    content: string | null;
    status: string;
    createdAt: number;
    updatedAt: number;
  };
}
export type TableName = keyof Tables;
export type Row<T extends TableName> = Tables[T];

Pass the Tables type and a table name as generics to useQuery and useMutation for full type inference on filters, return data, and mutation payloads.

example.tsx
import type { Tables } from "./based.d.ts";
import { useQuery, useMutation } from "@weirdscience/based-client";

// data is typed as Tables["posts"][]
const { data } = useQuery<Tables, "posts">("posts", {
  filter: { status: "published" },
});

// mutate() accepts Omit<Tables["posts"], "id" | "createdAt" | "updatedAt">
const { mutate } = useMutation<Tables, "posts">("posts", "create");
await mutate({ title: "Hello world", content: "My first post", status: "draft" });

Re-run based typegen after any schema change to keep your types in sync.

Auth methods

The client exposes auth methods directly on client.auth.

auth.ts
// Sign up a new user
await based.auth.signUp("user@example.com", "password123");

// Sign in
await based.auth.signIn("user@example.com", "password123");

// Sign out (invalidates current session)
await based.auth.signOut();

// Fetch the current user via /auth/me
const user = await based.auth.getUser();

// Manually refresh the session
await based.auth.refreshSession();

The SDK automatically refreshes tokens when a request returns a 401. You do not need to call refreshSession manually in most cases.

Session persistence

Sessions persist across page reloads via localStorage by default. On mount, the client restores the saved tokens and validates them by calling /auth/me.

Use useUser().isLoading or client.ready() to avoid flashing a logged-out UI during hydration:

example.tsx
const { user, isLoading } = useUser();
if (isLoading) return <Spinner />;
if (!user) return <LoginForm />;
return <Dashboard user={user} />;

Opt out, or use a custom storage adapter (cookies, IndexedDB, React Native AsyncStorage, etc.):

lib/based.ts
// Disable persistence entirely (in-memory only)
createClient({ url, anonKey, storage: false });

// Custom storage adapter — getItem/setItem/removeItem can return promises
createClient({
  url,
  anonKey,
  storage: {
    getItem: (k) => AsyncStorage.getItem(k),
    setItem: (k, v) => AsyncStorage.setItem(k, v),
    removeItem: (k) => AsyncStorage.removeItem(k),
  },
});