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

@kaiz11/stack-client

v0.0.15

Published

A standalone TypeScript client for Supabase stack services. Supports both platform-level and tenant-level operations with automatic token management.

Readme

@kaiz11/stack-client

A standalone TypeScript client for Supabase stack services. Supports both platform-level and tenant-level operations with automatic token management.

Installation

pnpm add @kaiz11/stack-client

Quick Start

import { createTenantClient } from "@kaiz11/stack-client";

const client = createTenantClient({
  baseUrl: "https://stack.example.com",
  tenantId: "my-tenant",
});

// Sign in
await client.auth.signInWithPassword({
  email: "[email protected]",
  password: "password123",
});

// Access the user
const user = client.auth.getUser();

Client Modes

Tenant Mode

For applications serving a single tenant. Endpoints use /auth/{tenantId}/*.

import { createTenantClient } from "@kaiz11/stack-client";

const client = createTenantClient({
  baseUrl: "https://stack.example.com",
  tenantId: "acme-corp",
});

Platform Mode

For platform-level operations (admin dashboards, multi-tenant management). Endpoints use /auth/_platform/*.

import { createPlatformClient } from "@kaiz11/stack-client";

const client = createPlatformClient({
  baseUrl: "https://stack.example.com",
});

Generic Mode

When you need to specify the mode dynamically, use createClient() with tenantId for tenant mode, or without it for platform mode:

import { createClient } from "@kaiz11/stack-client";

// Tenant mode (tenantId present)
const tenantClient = createClient({
  baseUrl: "https://stack.example.com",
  tenantId: "acme-corp",
});

// Platform mode (no tenantId)
const platformClient = createClient({
  baseUrl: "https://stack.example.com",
});

Configuration

interface ClientConfig {
  /** Base URL of the stack server */
  baseUrl: string;

  /** Token storage type: "memory" | "localStorage" | custom TokenStore */
  tokenStore?: TokenStoreType;

  /** Storage key prefix (default: "stack") */
  storagePrefix?: string;

  /** Request timeout in milliseconds (default: 30000) */
  timeout?: number;

  /** Enable mock mode for testing (no HTTP requests) */
  mock?: boolean;

  /** Mock options (latency simulation, etc.) */
  mockOptions?: MockOptions;
}

interface TenantClientConfig extends ClientConfig {
  /** Tenant identifier */
  tenantId: string;
}

Token Storage

Tokens need to be stored somewhere. The client supports pluggable storage backends:

Memory Store (Default)

Tokens are stored in memory. Lost on page refresh. Good for server-side usage.

import { createTenantClient, MemoryTokenStore } from "@kaiz11/stack-client";

const client = createTenantClient({
  baseUrl: "https://stack.example.com",
  tenantId: "my-tenant",
  tokenStore: new MemoryTokenStore(), // This is the default
});

LocalStorage Store

Tokens persist in browser localStorage. Survives page refreshes.

import {
  createTenantClient,
  LocalStorageTokenStore,
} from "@kaiz11/stack-client";

const client = createTenantClient({
  baseUrl: "https://stack.example.com",
  tenantId: "my-tenant",
  tokenStore: new LocalStorageTokenStore("my-app-auth"),
});

Custom Store

Implement the TokenStore interface for custom backends (cookies, IndexedDB, etc.):

import { TokenStore } from "@kaiz11/stack-client";

class CookieTokenStore implements TokenStore {
  getAccessToken(): string | null {
    return getCookie("access_token");
  }

  getRefreshToken(): string | null {
    return getCookie("refresh_token");
  }

  setTokens(accessToken: string, refreshToken: string): void {
    setCookie("access_token", accessToken, { httpOnly: true, secure: true });
    setCookie("refresh_token", refreshToken, { httpOnly: true, secure: true });
  }

  clearTokens(): void {
    deleteCookie("access_token");
    deleteCookie("refresh_token");
  }
}

Cleanup

When done with the client, call destroy() to clear auto-refresh timers:

const client = createTenantClient({ ... });

// When cleaning up (e.g., component unmount)
client.destroy();

Server-Side Usage

The same client works on both frontend and backend. For server-side usage, use tokenStore: "memory" and pass accessToken directly.

Note: The client does not verify tokens passed via accessToken. It assumes the token is valid and uses it for outgoing API calls. Verification happens server-side when the request reaches PostgREST, Storage, or other backend services. If you need to verify incoming requests, use the Auth Middleware.

Service Role (Admin Access)

Use a service role token for admin operations that bypass RLS:

import { createTenantClient } from "@kaiz11/stack-client";

const adminClient = createTenantClient({
  baseUrl: "https://stack.example.com",
  tenantId: "acme-corp",
  tokenStore: "memory",
  accessToken: process.env.SERVICE_ROLE_TOKEN,
});

// Admin operations - bypasses RLS
await adminClient.storage.from("uploads").upload("file.pdf", data);
await adminClient.storage.from("private").list(); // Can access all files

User Context (Respects RLS)

Pass the user's access token (from a verified request) to make calls scoped to that user:

import { createTenantClient } from "@kaiz11/stack-client";

// In your API handler, after verifying the request
const userClient = createTenantClient({
  baseUrl: "https://stack.example.com",
  tenantId: "acme-corp",
  tokenStore: "memory",
  accessToken: userAccessToken, // From verified JWT
});

// User-scoped operations - respects RLS
await userClient.storage.from("user-files").list(); // Only sees their files
await userClient.accounts.list(); // Only sees their accounts

Auth Middleware (Verifying Incoming Requests)

Use the auth middleware to verify JWTs on incoming requests. The middleware automatically fetches the JWKS from GoTrue for ES256 verification.

import { Hono } from "hono";
import { createStackAuthMiddleware } from "@kaiz11/stack-client/auth/server";

const app = new Hono();

// For tenant APIs
app.use(
  "*",
  createStackAuthMiddleware({
    baseUrl: "https://stack.example.com",
    tenantId: "acme-corp",
    excludePaths: [/^\/health$/], // Optional: skip auth for these paths
  }),
);

// For platform APIs
app.use(
  "*",
  createStackAuthMiddleware({
    baseUrl: "https://stack.example.com",
    // No tenantId = platform mode
  }),
);

// Access verified user in handlers
app.get("/api/me", (c) => {
  const user = c.get("user"); // { id, email, role, aal, ... }
  const token = c.get("accessToken"); // Raw JWT for downstream calls
  return c.json({ user });
});

Optional Auth Middleware

For routes that work with or without authentication:

import { optionalStackAuthMiddleware } from "@kaiz11/stack-client/auth/server";

app.use(
  "*",
  optionalStackAuthMiddleware({
    baseUrl: "https://stack.example.com",
    tenantId: "acme-corp",
  }),
);

app.get("/api/feed", (c) => {
  const user = c.get("user"); // undefined if not authenticated
  if (user) {
    return c.json({ feed: getPersonalizedFeed(user.id) });
  }
  return c.json({ feed: getPublicFeed() });
});

Role & MFA Guards

Add additional guards after the auth middleware:

import {
  createStackAuthMiddleware,
  requireRoleMiddleware,
  requireMfaMiddleware,
} from "@kaiz11/stack-client/auth/server";

// All routes require authentication
app.use("*", createStackAuthMiddleware({ baseUrl, tenantId }));

// Admin routes require service_role
app.use("/admin/*", requireRoleMiddleware("service_role"));

// Sensitive routes require MFA (AAL2)
app.use("/settings/security/*", requireMfaMiddleware());

Complete Server Example

import { Hono } from "hono";
import { createTenantClient } from "@kaiz11/stack-client";
import { createStackAuthMiddleware } from "@kaiz11/stack-client/auth/server";

const app = new Hono();

const baseUrl = "https://stack.example.com";
const tenantId = "acme-corp";

// Verify incoming requests
app.use("/api/*", createStackAuthMiddleware({ baseUrl, tenantId }));

// User uploads a file (respects RLS)
app.post("/api/upload", async (c) => {
  const userClient = createTenantClient({
    baseUrl,
    tenantId,
    tokenStore: "memory",
    accessToken: c.get("accessToken"),
  });

  const file = await c.req.blob();
  const { data, error } = await userClient.storage
    .from("uploads")
    .upload(`${c.get("user").id}/file.pdf`, file);

  return c.json({ data, error });
});

// Admin generates a report (bypasses RLS)
app.post("/api/admin/report", async (c) => {
  const adminClient = createTenantClient({
    baseUrl,
    tenantId,
    tokenStore: "memory",
    accessToken: process.env.SERVICE_ROLE_TOKEN,
  });

  // Can access all users' files
  const { data } = await adminClient.storage.from("uploads").list();
  return c.json({ files: data });
});

export default app;

Testing

The client includes built-in mock mode for testing without HTTP requests.

Mock Mode

Enable mock mode for unit tests or local development:

const client = createTenantClient({
  baseUrl: "https://stack.example.com",
  tenantId: "test-tenant",
  mock: true,
});

// All auth methods work without network requests
await client.auth.signIn({ email: "[email protected]", password: "password" });
const user = client.auth.getUser(); // Returns mock user

Simulating Latency

Add artificial delay to simulate network conditions:

const client = createTenantClient({
  baseUrl: "https://stack.example.com",
  tenantId: "test-tenant",
  mock: true,
  mockOptions: {
    latency: 100, // 100ms delay on all operations
  },
});

Controlling Mock Behavior

Use mockState to simulate errors and edge cases:

import { createTenantClient, mockState } from "@kaiz11/stack-client";

const client = createTenantClient({ ..., mock: true });

// Simulate sign-in failure
mockState.shouldFailSignIn = true;
await client.auth.signIn({ email, password }); // Throws AuthError

// Simulate sign-up failure (user exists)
mockState.shouldFailSignUp = true;
await client.auth.signUp({ email, password }); // Throws AuthError

// Simulate token refresh failure
mockState.shouldFailRefresh = true;
await client.auth.refreshSession(); // Throws AuthError

// Reset to default behavior
mockState.reset();

Tracking Request Counts

Use requestCounts to verify operations were called:

import { createTenantClient, requestCounts } from "@kaiz11/stack-client";

const client = createTenantClient({ ..., mock: true });

await client.auth.signIn({ email, password });
console.log(requestCounts.signIn); // 1

await client.auth.signOut();
console.log(requestCounts.signOut); // 1

// Reset counts between tests
requestCounts.reset();

Vitest Example

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import {
  createTenantClient,
  mockState,
  requestCounts,
} from "@kaiz11/stack-client";

describe("auth", () => {
  let client: ReturnType<typeof createTenantClient>;

  beforeEach(() => {
    client = createTenantClient({
      baseUrl: "https://stack.example.com",
      tenantId: "test",
      mock: true,
    });
  });

  afterEach(() => {
    client.destroy();
    mockState.reset();
    requestCounts.reset();
  });

  it("signs in user", async () => {
    await client.auth.signIn({ email: "[email protected]", password: "pass" });

    expect(client.auth.isAuthenticated()).toBe(true);
    expect(requestCounts.signIn).toBe(1);
  });

  it("handles sign-in error", async () => {
    mockState.shouldFailSignIn = true;

    await expect(
      client.auth.signIn({ email: "[email protected]", password: "wrong" }),
    ).rejects.toThrow();
  });
});

Known Limitations

Some client features require backend configuration or are not yet supported by the self-hosted Stack platform.

Features Requiring Backend Configuration

| Feature | Requirement | Status | | --------------------------- | --------------------------------------------- | ---------------------------- | | Phone/SMS Auth | Twilio configuration in GoTrue | Not configured | | Manual Identity Linking | GOTRUE_SECURITY_MANUAL_LINKING_ENABLED=true | Disabled by default | | TUS Resumable Uploads | Storage service TUS endpoint | Returns 500 (platform issue) |

Features Requiring Browser Interaction

These features work correctly but cannot be fully automated in tests:

| Feature | Reason | | -------------------------- | ------------------------------------- | | OAuth Sign-In | Requires browser redirect to provider | | OAuth Identity Linking | Requires browser OAuth flow | | PKCE OAuth Flow | Requires browser for code exchange |

API Differences from Official Supabase

| Method | Difference | | ------------------------ | ------------------------------------------------- | | auth.mfa.listFactors() | Returns 405 (endpoint not available in GoTrue) | | auth.reauthenticate() | Sends OTP email, returns void (not { nonce }) |

Workarounds

For reauthenticate(): The OTP sent to email should be used as the nonce parameter in updatePassword():

// Request reauthentication (sends OTP to email)
await client.auth.reauthenticate();

// User receives OTP via email, enters it
const otp = "123456"; // from email

// Use OTP as nonce for password update
await client.auth.updatePassword({
  password: "new-password",
  nonce: otp,
});

Modules

| Module | Description | Documentation | | ----------- | --------------------------------------------------- | ------------------------------------------- | | auth | Authentication (sign in, sign up, sign out, tokens) | Auth README | | accounts | Multi-tenant accounts (teams, roles, members) | Accounts README | | storage | File storage (buckets, uploads, signed URLs, TUS) | Storage README | | functions | Edge functions | Coming soon | | realtime | Realtime subscriptions | Coming soon |

TypeScript Types

import type {
  // Client
  StackClient,
  ClientConfig,
  TenantClientConfig,
  PlatformClientConfig,
  ClientMode,

  // Token storage
  TokenStore,
  TokenStoreType,

  // Mock mode
  MockOptions,
  MockState,
  RequestCounts,
} from "@kaiz11/stack-client";

License

Copyright © 2026 Kai Zhao. All rights reserved.

This software is proprietary and confidential. Unauthorized copying, distribution, modification, or use of this software, via any medium, is strictly prohibited without prior written permission from the copyright holder.