@weirdscience/based-client
v0.4.1
Published
React SDK for Based — a minimal self-hosted BaaS
Readme
@weirdscience/based-client
React SDK for Based — a minimal self-hosted Backend-as-a-Service.
Hooks for auth, queries, and mutations. Type-safe end-to-end when paired with based typegen.
Install
bun add @weirdscience/based-client
# or: npm install @weirdscience/based-client
# or: pnpm add @weirdscience/based-clientPeer dependency: react >=18.
Quick start
import { createClient, BasedProvider } from "@weirdscience/based-client";
const based = createClient({
url: process.env.NEXT_PUBLIC_BASED_URL!,
anonKey: process.env.NEXT_PUBLIC_BASED_ANON_KEY!,
});
export default function App({ children }: { children: React.ReactNode }) {
return <BasedProvider client={based}>{children}</BasedProvider>;
}Hooks
useUser()
Current authenticated user.
import { useUser } from "@weirdscience/based-client";
function Profile() {
const { user, isLoading } = useUser();
if (isLoading) return <p>...</p>;
if (!user) return <p>Not logged in</p>;
return <p>{user.email}</p>;
}useQuery(table, options?)
Read rows from a table. Returns { data, total, isLoading, error, refetch }.
import { useQuery } from "@weirdscience/based-client";
const { data, total, isLoading } = useQuery("posts", {
filter: { status: "published" },
limit: 20,
offset: 0,
});Gate the query behind auth or any boolean with enabled:
const { user } = useUser();
const { data } = useQuery("notes", { enabled: !!user });
// Won't fire until `user` is truthy — avoids a 403 when signed out.useRecord(table, id, options?)
Fetch a single record by id via GET /api/:table/:id. Returns { data, isLoading, error, refetch } — data is a single row, not an array.
import { useRecord } from "@weirdscience/based-client";
function Post({ id }: { id: string }) {
const { data: post, isLoading } = useRecord("posts", id);
if (isLoading) return <Spinner />;
if (!post) return <NotFound />;
return <h1>{post.title}</h1>;
}Pass null/undefined as the id to skip the query (same effect as enabled: false):
const { data } = useRecord("posts", selectedId); // no fetch until selectedId is setPerfect for deterministic keys like ${userId}:${key}:
const { data: prefs } = useRecord("preferences", `${user.id}:theme`);useMutation(table, operation)
Write rows. operation is "create" | "update" | "delete". Returns { mutate, isLoading, error }.
import { useMutation } from "@weirdscience/based-client";
function NewPost() {
const { mutate, isLoading } = useMutation("posts", "create");
return (
<button
onClick={() => mutate({ title: "Hello", content: "World" })}
disabled={isLoading}
>
Create
</button>
);
}update and delete require an id field:
const { mutate: update } = useMutation("posts", "update");
await update({ id: "abc", title: "Renamed" });
const { mutate: remove } = useMutation("posts", "delete");
await remove({ id: "abc" });Auth
import { useBasedClient } from "@weirdscience/based-client";
function LoginForm() {
const client = useBasedClient();
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const fd = new FormData(e.currentTarget);
await client.auth.signIn(
fd.get("email") as string,
fd.get("password") as string
);
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" />
<input name="password" type="password" />
<button type="submit">Sign in</button>
</form>
);
}Methods on client.auth:
signUp(email, password)→ creates an account and signs insignIn(email, password)→ signs insignOut()→ invalidates the sessionrefreshSession()→ manually refresh (happens automatically on 401)
Access tokens auto-refresh on 401.
Session persistence
Sessions persist across page reloads via localStorage by default. On mount, the client restores the saved session and validates it by calling /auth/me.
Use client.ready() or useUser().isLoading to avoid flashing a logged-out UI during hydration:
const { user, isLoading } = useUser();
if (isLoading) return <Spinner />;
if (!user) return <LoginForm />;
return <Dashboard user={user} />;Opt out or use a custom storage adapter:
// Disable entirely (in-memory only)
createClient({ url, anonKey, storage: false });
// Custom storage (cookies, IndexedDB, React Native AsyncStorage, etc.)
createClient({
url,
anonKey,
storage: {
getItem: (k) => AsyncStorage.getItem(k),
setItem: (k, v) => AsyncStorage.setItem(k, v),
removeItem: (k) => AsyncStorage.removeItem(k),
},
});Storage adapters can return promises — the client awaits them.
Type safety
Generate types for your tables from the server:
based typegen
# writes based.d.tsPass the generated Tables type as a generic:
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" }, // typed keys
});
const { mutate } = useMutation<Tables, "posts">("posts", "create");
await mutate({ title: "Hello", content: "World" }); // typed payloadRe-run based typegen after any schema change.
Row-level isolation
If a table has a user_id (or userId) column, Based auto-scopes CRUD to the authenticated user. No configuration needed — just add the column:
based table create notes user_id:text:required title:text:required body:textAfter that:
useQuery("notes")only returns the caller's notesuseMutation("notes", "create")auto-fillsuser_id- Other users' rows return 404
Upsert
PUT /api/:table/:id creates if missing, updates if present. From the SDK:
const { mutate: upsert } = useMutation("preferences", "update");
// The URL id becomes the row id — perfect for deterministic keys like userId:key
await upsert({ id: "alice:theme", value: "dark" });Links
License
MIT
