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

@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-client

Peer 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 set

Perfect 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 in
  • signIn(email, password) → signs in
  • signOut() → invalidates the session
  • refreshSession() → 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.ts

Pass 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 payload

Re-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:text

After that:

  • useQuery("notes") only returns the caller's notes
  • useMutation("notes", "create") auto-fills user_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