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

ffdb-client

v0.2.7

Published

Framework-agnostic client for FFDB — works in Node.js, browsers, React Native, and Expo

Readme

ffdb-client

A framework-agnostic client for FFDB. It ships with built-in user authentication via better-auth, a type-safe query builder via Kysely, raw SQL helpers, and optional offline sync.

Works in Node.js, browsers, React Native, and Expo.

How It Works

FFDB has a two-tier authentication model:

  1. You, the developer, connect with an admin token from the FFDB portal. That token lets you make admin requests.
  2. Your users sign in through the auth client methods. After sign-in, the client manages short-lived API keys for scoped access.

The client exposes a db object for type-safe queries, a raw sql helper, and an auth client for user authentication. It also exposes request, sync, getAccess, and subscribe for lower-level control.

Install

npm install ffdb-client

Quick Start

The recommended way to start

  • Follow the prompts and select a quick start template.
npx ffdb-cli init

Any JavaScript environment

import { createClient } from "ffdb-client";

const { db, auth, destroy } = await createClient({
  config: {
    apiUrl: "https://your-ffdb-server.com",
  },
});

await auth.signIn.email({
  email: "[email protected]",
  password: "password",
});

const users = await db.selectFrom("user").selectAll().execute();

await destroy();

React / Next.js

import { FFDBProvider, useFFDB, useRawQuery } from "ffdb-client/react";

function App() {
  return (
    <FFDBProvider
      options={{
        config: {
          apiUrl: import.meta.env.VITE_API_URL, // Vite
        },
      }}
      errorFallback={(error) => <p>Connection failed: {error.message}</p>}
    >
      <Users />
    </FFDBProvider>
  );
}

function Users() {
  const { db, auth, isLoading, error } = useFFDB();

  if (isLoading) return <p>Connecting...</p>;
  if (error) return <p>Connection failed: {error.message}</p>;

  const loadUsers = async () => {
    const rows = await db!.selectFrom("user").selectAll();
    console.log(rows);
  };

  return <button onClick={loadUsers}>Load users</button>;
}

If your app adds custom tables, create typed React bindings once so hooks keep the same schema automatically:

import { createTypedFFDBReact } from "ffdb-client/react";

type AppTables = {
  projects: {
    id: string;
    name: string;
  };
};

const ffdb = createTypedFFDBReact<AppTables>();

function App() {
  return (
    <ffdb.FFDBProvider options={{ config: { apiUrl: "https://api.example.com" } }}>
      <Projects />
    </ffdb.FFDBProvider>
  );
}

function Projects() {
  const { db } = ffdb.useFFDB();
  // `projects` is fully typed here.
  const q = db?.selectFrom("projects").selectAll();
  return <pre>{String(Boolean(q))}</pre>;
}

useQuery() is the reactive Kysely hook. useRawQuery() is the raw SQL version, which is useful for searchable pickers and other query-driven UI.

const { data, isLoading } = useQuery((db) => db.selectFrom("user").selectAll().limit(20));

// -- OR something like this --

const { data, isLoading } = useRawQuery<{ id: string; name: string }>(
  {
    sql: "SELECT id, name FROM user WHERE name LIKE ? ORDER BY name LIMIT 20",
    values: [`%${term}%`],
  },
  { deps: [term], enabled: term.length > 1 },
);

Node.js

import { createClient, createNodeLifecycle } from "ffdb-client/node";

const { db, auth } = await createClient({
  config: {
    apiUrl: process.env.API_URL,
  },
  lifecycle: createNodeLifecycle(),
});

API Surface

createClient(options)

Creates a new FFDB client.

type CreateClientOptions = {
  config?: Partial<FFDB_Config>;
  lifecycle?: LifecycleHooks;
  storage?: StorageAdapter; // Very useful for expo (secure store)
  offline?: OfflineConfig;
  skipHealthCheck?: boolean;
};

FFDBProvider accepts fallback for startup loading, errorFallback for client initialization failures, and onError for reporting. If you omit errorFallback, initialization errors are exposed through context so useFFDB() and useFFDBStatus() can render app-specific UI.

Returns:

{
  db,
  sql,
  auth,
  request,
  destroy,
  sync,
  getAccess,
  subscribe,
}

sync includes run(), sync(), pull(), push(), waitForIdle(), ensureTables(), reconcileTables(), status, and subscribe().

Use sync.ensureTables(["organization"]) before a screen writes to a table that may not have been hydrated locally yet. sync.reconcileTables(["organization"], { forceSchema: true }) forces a targeted schema/table refresh without clearing pending offline mutations.

createFetchClient(options)

Creates the underlying HTTP client if you need direct API access.

React hooks

useFFDB() returns the current client plus readiness state:

{
  client,
  db,
  sql,
  auth,
  destroy,
  sync,
  getAccess,
  subscribe,
  isLoading,
  error,
  clientVersion,
  isReady,
}

useDB() returns the Kysely instance directly. useAuth() returns the auth client directly. useFFDBStatus() returns { isLoading, error }.

useQuery() accepts a Kysely query factory and supports enabled, deps, refetchOnFocus, refetchOnReconnect, and refetchInterval.

useRawQuery() accepts { sql, values, bypassCache } and the same hook options.

Other utilities

generateId() creates a stable text ID for app data and is the recommended default for offline-safe inserts.

memoryStorage() is the default built in storage adapter. browserStorage() supports "local", "session", and "indexeddb" options.

Configuration

createClient() accepts a partial config and merges it with defaults.

type FFDB_Config = {
  origin: string; // Your window.location.origin or scheme://
  apiUrl: string; // Your FFDB instance url
  endpoint: string; // The endpoint for querying, NOT RECOMMENDED TO CHANGE
  authToken: string; // DO NOT USE IN CLIENT CODE
  apiKey: string; // DO NOT USE IN CLIENT CODE - also not necessary if you have authToken
  retryAttempts?: number; // Number of times to retry when encountering 400+ errors
  healthProbeIntervalMs?: number; // How often to poll when offline
  logLevel: "info" | "verbose";
  logEnabled: boolean;
};

In practice you usually only need to provide apiUrl and origin if your code is not running in a browser.

Session Persistence

If you need persisted sessions outside of memory / cookie storage, pass a storage adapter. Without one, the client uses memoryStorage(), which is process-local and resets on reload.

import { createClient, browserStorage } from "ffdb-client";

const { db, auth } = await createClient({
  config: { apiUrl: "...", origin: "..." },
  storage: browserStorage(),
});

browserStorage("local") survives reloads, browserStorage("session") is tab-scoped, and browserStorage("indexeddb") is the fully async browser option.

Querying

The db object is a Kysely instance. All queries are type-safe against your database schema.

await db.selectFrom("user").select(["id", "email", "name"]);
await db.insertInto("user").values({ id: "abc", name: "Alice", email: "[email protected]" });
await db.updateTable("user").set({ name: "Bob" }).where("id", "=", "abc");
await db.deleteFrom("user").where("id", "=", "abc");

If you prefer SQL strings, use client.sql.query(), client.sql.first(), or client.sql.

Authentication

The auth object is a better-auth client with the FFDB API key plugin pre-configured. After sign-in, the client automatically rotates API keys and keeps the session refreshed.

Access & Permissions

getAccess() fetches the authenticated user's permissions, including table access, column constraints, and blocked statement types.

Offline-First

If you enable offline, the client keeps a local SQLite cache and syncs with the server using POST /api/sync/pull.

Write behavior depends on runtime state:

  • When the client is online, auth is ready, and there are no older queued mutations, writes go to the backend policy endpoint first. Only accepted mutations are mirrored into local SQLite and broadcast to subscribers.
  • When the client is offline, auth is not ready, the remote endpoint is unreachable, or older offline mutations are pending, writes stay offline-first: they apply locally, enter the durable mutation queue, and are pushed in order later.

After any accepted write, the SQLite query cache and React in-memory query cache are invalidated before subscribers refetch, so focus/reconnect refreshes should not replay stale rows.

Lifecycle & Cleanup

Always call destroy() when you are done with the client. For Node.js services, createNodeLifecycle() registers cleanup handlers for SIGINT, SIGTERM, and beforeExit.

Entry Points

| Import Path | Environment | Includes | |---|---|---| | ffdb-client | Universal | createClient, createFetchClient, generateId, storage helpers, types | | ffdb-client/react | React | FFDBProvider, useQuery, useRawQuery, useDB, useAuth, useFFDB, useFFDBStatus | | ffdb-client/node | Node.js | Universal + config, loadEnvConfig, createNodeLifecycle |

License

ISC