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

@palbase/web

v11.0.1

Published

Palbe — the Palbase client SDK for web. One entry point: pb.

Readme

@palbase/web

The Palbase client SDK for web. One import, one entry point: pb.

@palbase/web is typed against your own backend. You run palbase link once, it generates a typed client from your deployed endpoints, and from then on pb.todos.create(...), pb.rooms.list(...) and friends are fully typed — request shapes, response shapes and per-endpoint errors, all inferred from the backend you shipped. Auth, feature flags, realtime and analytics come built in.

import { pb } from '@palbase/web';

await pb.auth.signIn({ email, password });
const todo = await pb.todos.create({ title: 'Ship it' }); // typed by your backend

Install

npm i @palbase/web

Then install the Palbase CLI and link your project — the CLI generates the typed client:

brew install palgroup/tap/palbase
palbase link <project>

palbase link creates ONE visible directory in your checkout — palbase/ — runs palbe-gen when @palbase/web is installed, wires the generated client into your app entry, and adds a predev/prebuild hook that keeps it in sync.

Everything that belongs to one environment lives together in that environment's own directory, and palbase/client.ts is the single line your application imports:

palbase/
  client.ts                 the one import your app makes (a re-export)
  environments/
    local/                  the stack `palbase start` runs on this machine
      openapi.json          the contract
      web-config.json       this environment's url + publishable key
      palbe.gen.ts          the generated typed client
    main/
      …                     same four files, for the deployed environment

Commit all of it. Nothing under palbase/ is a build artifact: the generated client is source your app imports, so it shows up in autocomplete before any build, changes arrive as a reviewable git diff, and a fresh clone builds with no CLI and no network.

The generated client calls __configure(...) with the linked Environment ref, URL, app ID, and publishable API key at module load, so importing palbase/client.ts once at startup is all the setup there is. If you forget to import it, every pb call throws a guided BackendError (kind: 'notConfigured').

Regenerate any time your backend changes: npx palbe-gen (or just run dev/build — the hook does it for you).

Which environment a build talks to

One key, and its default is local. PALBASE_ENV selects the environment; unset means local, the stack every checkout gets for free. Set it wherever you already set environment variables — an npm script, a .env file, your CI:

PALBASE_ENV=main npm run build

palbe-gen reads it, generates into that environment's own directory, and rewrites palbase/client.ts to re-export that environment's client. Your import never carries an environment name, so switching environments never edits your application's source — and because a bundler follows the re-export, only the selected environment's code reaches your bundle.


Calling your backend

Codegen turns each backend controller into a typed namespace on pb. The operation id is <controller>.<method>TodosController.create becomes pb.todos.create(input), HelloController.greet becomes pb.hello.greet(). Path params come first, the request body/query second:

import { pb } from '@palbase/web';

// POST /todos/create  — body typed, response typed
const todo = await pb.todos.create({ title: 'Buy milk', done: false });

// GET /todos/{id}     — path param, then options
const one = await pb.todos.get('todo_123');

// GET /todos          — typed query object
const list = await pb.todos.list({ limit: 20, done: false });

Escape hatches

When you need to call an endpoint that isn't in the generated types yet (or do a raw multipart upload), use pb.call and pb.upload:

// Untyped POST — you supply the response type
const result = await pb.call<{ ok: boolean }>('todos/archive', { id: 'todo_123' });

// Multipart upload with progress
const uploaded = await pb.upload<{ url: string }>('files/avatar', {
  file: blob,
  filename: 'avatar.png',
  onProgress: ({ sent, total }) => console.log(sent / total),
});

Auth

pb.auth is the full authentication surface — email/password, OTP, magic links, OAuth, password reset, email verification:

await pb.auth.signUp({ email: '[email protected]', password: 'secret123' });
const { user, session } = await pb.auth.signIn({ email: '[email protected]', password: 'secret123' });
await pb.auth.signOut();

// React to session changes
const unsub = pb.auth.onAuthStateChange((state) => {
  console.log(state.status); // 'signedIn' | 'signedOut'
});

pb.auth.isSignedIn;     // boolean (token presence)
pb.auth.currentUser;    // AuthUser | null
await pb.auth.refreshUser();   // re-fetch the profile (e.g. emailVerified flip)

Phone OTP, magic links and OAuth:

await pb.auth.signInWithOTP({ phone: '+1555…' });
await pb.auth.verifyOTP({ phone: '+1555…', token: '123456' });

await pb.auth.signInWithMagicLink('[email protected]');

// Browser flow: complete on a browser callback page with the same tab storage.
await pb.auth.signInWithOAuth({ provider: 'google', redirectTo: 'https://app.example.com/auth/callback' });

OAuth uses the oauth snapshot emitted by palbase link, selecting a browser client for this app and variant. Google, Apple, Microsoft and GitHub share the same API. The callback receives transaction_id and result_code:

await pb.auth.exchangeCodeForSession({ transactionId, resultCode, callbackURL: location.href });

The result is signedIn, mfaRequired, or linked; only signedIn installs a session. Proofs are scoped to this browser tab, backend and app. A transient failure can be retried with completeOAuth({ transactionId }), which reuses the stored result. Start explicit account linking with linkIdentity; accounts are never merged solely because their email addresses match.

For a Next.js handleAuthCallback route, begin the transaction with (await pbServer()).auth.beginOAuth(...) in a Server Action or Route Handler. That stores its proof in a transaction-specific HttpOnly cookie. The default MFA response is uncached HTTP 202 JSON; onMFARequired can render your MFA handoff. Browser session-storage proofs cannot be read by a server callback.


Feature flags

pb.flags polls and caches your project's feature flags (auth-aware — flags re-evaluate when the user signs in/out):

if (pb.flags.isEnabled('new-checkout')) { /* … */ }

pb.flags.getString('theme', 'light');
pb.flags.getInt('max-items', 10);

await pb.flags.getVariant('pricing-experiment'); // multivariate → variant name | null

const off = pb.flags.onChange(() => console.log('flags changed', pb.flags.all()));

Realtime

pb.realtime multiplexes every subscription over one auto-reconnecting WebSocket. Channels are client-only (browser):

const room = pb.realtime.channel('room:42');

const sub = room.on('message', (payload) => {
  console.log('got', payload);
});

room.send('message', { text: 'hello' }); // broadcast to other subscribers

sub.cancel(); // last cancel on a channel leaves it

Connection status is observable (pb.realtime.status.state / pb.realtime.status.onChange(...)).


Analytics

pb.analytics buffers events client-side and flushes in batches. It manages an anonymous distinct id and stitches it to the user on sign-in automatically:

pb.analytics.capture('checkout_started', { plan: 'pro' });
pb.analytics.screen('Dashboard');
pb.analytics.identify('user_123', { email: '[email protected]' });
await pb.analytics.flush();      // force-send buffered events
pb.analytics.setOptOut(true);    // GDPR opt-out (drops pending + future)

Errors

Every failing call throws a BackendError. Inspect kind (a coarse category) and code (the server's machine code):

import { pb, BackendError, isBackendError } from '@palbase/web';

try {
  await pb.todos.create({ title: '' });
} catch (e) {
  if (isBackendError(e)) {
    e.kind;       // 'validation' | 'unauthorized' | 'rateLimited' | 'notConfigured'
                  //   | 'network' | 'server' | 'decode'
    e.code;       // server machine code, e.g. 'invalid_input'
    e.status;     // HTTP status
    e.fields;     // field-level validation errors (when kind === 'validation')
    e.retryAfter; // seconds (when kind === 'rateLimited')
  }
}

Generated code also gives you typed per-endpoint error classes (e.g. RoomsCreateRoomLockedError) with typed .data, so you can instanceof-match the specific failures a given endpoint documents.


Next.js — @palbase/web/next

The Next.js App Router adapter shares one session cookie between the browser and the server so Server Components, Route Handlers and the proxy all see the same auth state. Requires next >= 16 (the proxy file convention).

1. Configure the browser client (a client provider/component):

'use client';
import { setupPalbeNext } from '@palbase/web/next/client';
import { useEffect } from 'react';

export function PalbeProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => { setupPalbeNext(); }, []);
  return <>{children}</>;
}

setupPalbeNext() swaps in cookieSessionStorage so the session is written to a cookie the server can read.

2. Refresh the session in the proxy (required for session-bearing RSC apps):

// proxy.ts
import { palbeProxy } from '@palbase/web/next/proxy';
import { environmentConfig } from './palbase/config';
import type { NextRequest } from 'next/server';

export function proxy(request: NextRequest) {
  return palbeProxy(request, environmentConfig);
}

export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] };

palbase link writes this file for you, and it carries no address and no key — only an import. palbe-gen writes palbase/config.ts on every run from PALBASE_ENV, the same switch that selects the generated client, so the proxy and the data client can no longer point at different stacks. Nothing in your app holds an environment value; you choose the environment, the SDK does the wiring.

Two rules the shape above encodes:

  • Import @palbase/web/next/proxy, not @palbase/web/next. Next compiles the proxy into its own bundle, and the full adapter graph reaches livekit-client and the MLS WASM loader — megabytes on every request path. This entry reaches none of it.
  • Import palbase/config, never palbase/client. The config barrel sits over an import-free leaf, so it brings nothing with it; the client barrel re-exports palbe.gen.ts, which calls __configure at import time and pulls that whole graph back in. It also means config/matcher must be declared in this file — Next reads export const config off this file's own AST, so a re-exported or imported one is silently ignored and the proxy degrades to matching every request.

3. Read data in Server Components with pbServer() — a per-request, session-isolated client:

import { pbServer } from '@palbase/web/next';

export default async function Page() {
  const pb = await pbServer();
  const todos = await pb.todos.list({ limit: 20 });
  return <TodoList items={todos} />;
}

4. Complete OAuth with a callback route handler:

// app/auth/callback/route.ts
import '@/palbase/client';
import { handleAuthCallback } from '@palbase/web/next';
export const GET = handleAuthCallback({ defaultNext: '/' });

React — @palbase/web/react

Thin, concurrent-safe hooks over the observable pb.* facades. react is an optional peer dependency — importing @palbase/web never pulls React; only @palbase/web/react does.

'use client';
import { useUser, useSession, useFlag, useFlags, useChannel } from '@palbase/web/react';

function Profile() {
  const user = useUser();                 // AuthUser | null, re-renders on auth change
  const { signedIn } = useSession();      // { signedIn, user }
  const dark = useFlag('dark-mode', false);
  const flags = useFlags();               // whole flag set

  // Subscribe to a realtime event for the component's lifetime
  const { status } = useChannel('room:42', 'message', (payload) => {
    console.log(payload);
  });

  return signedIn ? <span>{user?.email}</span> : <SignInButton />;
}

License

MIT