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

@fabappai/sdk

v0.4.0

Published

Fabapp's API client: data, auth, files, AI, functions, connectors and admin. Framework-agnostic.

Readme

@fabappai/sdk

The Fabapp API from any JavaScript runtime — a browser, Node, a serverless handler, a test.

It is the data, auth, files, AI, backend functions, connectors and admin surface of a Fabapp project, and nothing else: no React, no components, no hooks, no bundler assumption. Bring your own framework, or bring none.

npm install @fabappai/sdk
import { createClient } from "@fabappai/sdk";

const fab = createClient({ projectId: "your-project-id" });

await fab.auth.login("[email protected]", "••••••••");
const open = await fab.collection("task").list({ filter: { done: false }, sort: "-created_at", limit: 20 });
await fab.collection("task").update(open[0].id, { done: true });

Typing your data

Every collection is generic. Declare the shape once and the whole surface follows it.

type Task = { id: string; title: string; done: boolean; due?: string };

const tasks = fab.collection<Task>("task");
const overdue = await tasks.list({ filter: { done: false, due: { lt: new Date().toISOString() } } });

Reading and writing

await tasks.list({ filter: { status: "open" } });            // a primitive means equality
await tasks.list({ filter: { status: ["open", "blocked"] } }); // an array means any of these
await tasks.list({ filter: { price: { gte: 50, lt: 100 } } }); // an object applies operators
await tasks.list({ fields: ["id", "title"] });               // a projection, for a lighter list

await tasks.count({ done: false });                          // the total, without the rows
await tasks.get(id);
await tasks.create({ title: "Write the docs" });
await tasks.update(id, { done: true });
await tasks.updateMany([id1, id2], { done: true });
await tasks.updateWhere({ done: false }, { priority: "high" });
await tasks.bulkCreate(rows);                                // one transaction, up to 200
await tasks.deleteMany({ done: true });                      // the filter is required on purpose
await tasks.remove(id);

Operators: eq ne gt gte lt lte in nin contains starts null. sort: "field" ascends, sort: "-field" descends.

⚠️ A comma separates the values of an in, and nothing escapes it. { tag: ["a,b", "c"] } goes over the wire as a,b,c and comes back matching three values, not two. Where the values may contain commas, filter on something else — an id, an enum — or filter client-side.

Access rules run on the server, per record, for every one of these calls. A list returns what the caller may read, and a write the caller may not perform fails — there is no client-side flag that changes that.

Live-ish reads

poll returns only what changed since a cursor, and does not consume the project's monthly request quota.

let cursor: string | null = null;
const tick = async () => {
  const { data, cursor: next } = await tasks.poll({ since: cursor });
  cursor = next;
  merge(data);          // by id — a poll returns changes, not the whole list
};

Auth

await fab.auth.login(email, password);
await fab.auth.signup({ email, name, password, profile: { avatar } });
await fab.auth.me();
fab.auth.logout();

await fab.auth.updateProfile({ name: "Ana", avatar: url });   // this is how a profile is edited
await fab.auth.changePassword(next, current);
await fab.auth.forgotPassword(email, `${location.origin}/reset`);
await fab.auth.resetPassword(token, password);
await fab.auth.acceptInvite(token, name, password);

fab.auth.subscribe((user) => render(user));                   // returns the unsubscribe function

Social sign-in is two steps: send the browser to fab.auth.oauthStartUrl("google"), and call await fab.auth.completeOAuth() on the page the provider returns to.

Sign-in by a code sent to the email — sendEmailCode(email), then loginWithEmailCode(email, code) — works once the app owner has turned it on; (await fab.publicConfig()).emailCodeLogin says whether to offer it. With sign-up closed, only people who already have an account receive a code, and the call answers the same either way.

Sign-in by SMS — sendPhoneCode, loginWithPhone, linkPhone, verifyPhoneLink — works once the app owner has connected an SMS provider; (await fab.publicConfig()).phoneLogin says whether to offer it.

Files

const { id, url } = await fab.uploadFile(file);                        // a stable, public URL
const secret = await fab.uploadFile(file, { visibility: "private" });  // an opaque id, signed on read

Save the returned url in a field. The bytes go straight from the caller to storage through a presigned PUT — they never pass through the API.

AI, functions and agents

const { text } = await fab.ai.invokeLLM({ prompt: "Summarise this thread", tier: "fast" });
const { data } = await fab.ai.invokeLLM({ prompt, schema });        // a JSON Schema returns structured data
const { url } = await fab.ai.generateImage("a flat illustration of a calendar");
const { data } = await fab.ai.extractData({ fileId: id, schema });  // a document or an image

const result = await fab.callFunction("issue-refund", { orderId });  // runs isolated, server-side
const { answer } = await fab.agent("support").run("cancel order 123");

The model keys belong to the platform. Your code never holds one, and never can.

Notifications, integrations, connectors

await fab.notify("email", { to, subject, message, link });   // plain text; put the URL in `link`
await fab.notify("sms", { to, message });
await fab.integrations.call("whatsapp", "sendMessage", { to, body });
await fab.callConnector(connectorId, "listContacts", { query: { limit: 10 } });
await fab.google.sheetsAppend(spreadsheetId, "Sheet1!A1", [[name, email]]);

Administration

Only for a signed-in user holding the admin role.

await fab.admin.listUsers();
await fab.admin.updateUser(id, { roles: ["staff"] });
await fab.admin.createUser({ email, name });                   // exists at once; no password, no email sent
await fab.admin.createUsers(rows);                             // ≤ 200 rows; existing addresses are skipped
await fab.admin.grantSubscription(userId, { planId });        // complimentary; no gateway involved
await fab.admin.subscriptionLink(userId, { planId, successUrl, cancelUrl });

Configuration

createClient({
  projectId,          // required
  baseUrl,            // defaults to https://api.fabapp.ai
  token,              // start from a token you already hold
  storage,            // where the session lives — see below
  fetch,              // your own fetch, for a proxy, retries, or a test
  timeout,            // ms before a request is abandoned; off by default
  signal,             // an AbortSignal every request of this client obeys
  currency, locale,   // for formatMoney / formatDate
});

Timeouts and cancelling

There is no default timeout, and that is deliberate: nothing here is uniformly fast. A tier: "smart" model call legitimately runs for a minute, and a limit short enough to protect a list read would cut it off. Set it per client instead of globally low.

const reads = createClient({ projectId, timeout: 10_000 });
const ai    = createClient({ projectId, timeout: 120_000 });

signal binds a client to a lifetime — one client per screen, aborted when the screen goes away, and the in-flight requests go with it. It is also how you stop a response arriving after a component has unmounted.

useEffect(() => {
  const stop = new AbortController();
  const fab = createClient({ projectId, signal: stop.signal });
  fab.collection<Task>("task").list().then(setTasks).catch(ignoreAborts);
  return () => stop.abort();
}, []);

An abandoned request rejects with AbortError, not ApiError — it never became an answer.

The session lives in a cookie in the browser and in memory everywhere else. Two clients never share a session, which is what makes this safe to use on a server — one client per request, one token per user.

// A server route acting as the signed-in visitor.
const fab = createClient({ projectId, token: tokenFromTheRequest });

Errors

Every non-2xx throws an ApiError carrying the HTTP status and the server's message.

import { ApiError } from "@fabappai/sdk";

try {
  await tasks.create({ title });
} catch (e) {
  if (e instanceof ApiError && e.status === 403) showPermissionNotice();
  else throw e;
}

401 means no session, 403 means the access rules said no, 402 means the account is out of credits, 422 means the schema disagrees with what you sent, 429 means a rate limit.

Two statuses that are not HTTP: status: 0 is a request that never reached the API — a dropped connection, a blocked origin, DNS. And an AbortError (not an ApiError) is a request you or your timeout gave up on.

Whatever the API answers, you get an ApiError. A gateway's HTML 502, an empty error body, a validation detail that arrives as a list — all of them arrive with a status you can branch on and a message a person can read.

A Skill

The package ships skill/SKILL.md — the same content as AGENTS.md, with the header Claude Code and claude.ai use to decide when to load it. Point your tool at node_modules/@fabappai/sdk/skill/.

It is derived, not a second copy: AGENTS.md is the source, npm run skill regenerates it, and CI fails if the two drift.

And there is a test on the other side — in the platform repository — that ties what it teaches to what the code does: the access grammar, the fail-closed defaults, owner_via's one-hop limit, the canonical ids and the field types. Documentation that goes stale is worse than none: it becomes confidently wrong, and the reader has no way to tell.

Not in this package

React components and hooks. Generated Fabapp apps get those seeded as editable source in src/lib and src/components — they are app code, not a dependency.

The card form. A payment form is deliberately absent from anything published to a registry: a compromised package that reaches the field where someone types a card number is a different order of damage from one that reaches a session.

Starting a checkout. Not an oversight. Inside a native shell, a gateway checkout is a payment link inside the app — the literal example in Google Play's payments policy, and apps get pulled for it with subscribers inside. The guard that decides between the gateway and store billing has to see which shell it is running in, which this package cannot. It stays in the app layer, where useSubscribe and <SubscribeGate> enforce it. From a server, where no store policy applies, call the endpoint deliberately:

await fab.request("POST", "/checkout", { mode: "subscription", items, success_url, cancel_url });

License

MIT