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

@zuzjs/flare

v0.2.12

Published

Official JavaScript/TypeScript client for ZuzFlare Server - Self-hosted real-time database

Downloads

1,198

Readme

ZuzFlare Client

Official JavaScript/TypeScript client for ZuzFlare Server.

npm version License: MIT

Installation

npm install @zuzjs/flare

Maintainer Rule

When adding or changing any public SDK API, update this README in the same commit and add a usage example for that API.

Quick Start

import { connectApp } from '@zuzjs/flare';

const app = connectApp({
  endpoint: 'https://flare.zuzcdn.net',
  appId: 'my-app',
  apiKey: 'app-api-key',
});

await app.collection('users').doc('alice').set({
  name: 'Alice',
  email: '[email protected]',
});

app.collection('users').onSnapshot((snapshot) => {
  console.log('snapshot', snapshot);
});

Public API Usage Examples

Core Instance

import { connectApp, getFlare, disconnectFlare } from '@zuzjs/flare';

const app = connectApp({ endpoint: 'https://flare.zuzcdn.net', appId: 'my-app', apiKey: 'ak' });
const same = getFlare();
disconnectFlare();

Auth Config And State

const app = connectApp({ endpoint: 'https://flare.zuzcdn.net', appId: 'my-app', apiKey: 'ak' });

await app.ensureCsrfProtection();
const config = await app.loadAuthConfig();

const offConfig = app.onAuthConfigLoaded((next) => {
  console.log('auth config loaded', next.providers);
});

const offState = app.onAuthStateChanged((session) => {
  console.log('auth state', session?.uid);
});

const legacyOffState = app.onAuthStateChange((session) => {
  console.log('legacy listener', session?.uid);
});

console.log('csrf cookie name', app.getCsrfCookieName());
console.log('csrf token', app.getCsrfToken());
console.log('current user', app.getCurrentUser());

offConfig();
offState();
legacyOffState();

Email Password Auth

const app = connectApp({ endpoint: 'https://flare.zuzcdn.net', appId: 'my-app', apiKey: 'ak' });

await app.createUserWithEmail('[email protected]', 'StrongPassword123!');
await app.createUserWithEmailAndPassword('[email protected]', 'StrongPassword123!');

await app.signInWithEmail('[email protected]', 'StrongPassword123!');
await app.signInWithEmailAndPassword('[email protected]', 'StrongPassword123!');

await app.signInOrCreateWithEmail('[email protected]', 'StrongPassword123!');
await app.signInOrCreateWithEmailAndPassword('[email protected]', 'StrongPassword123!');

await app.auth('<access-token>');
await app.refreshAuthSession();
await app.signOut();

Email Verification And Recovery

const app = connectApp({ endpoint: 'https://flare.zuzcdn.net', appId: 'my-app', apiKey: 'ak' });

const verifySent = await app.sendEmailVerification('[email protected]');
await app.verifyEmailWithCode('[email protected]', '123456');
await app.confirmEmailLink('<link-token>', '[email protected]');

const recoverySent = await app.sendAccountRecovery('[email protected]');
await app.recoverAccountWithCode('[email protected]', '123456', 'NextStrongPassword123!');
await app.recoverAccountWithToken('<recovery-token>', 'NextStrongPassword123!');

console.log(verifySent, recoverySent);

OAuth Helpers

const app = connectApp({ endpoint: 'https://flare.zuzcdn.net', appId: 'my-app', apiKey: 'ak' });

await app.signIn('google');
await app.signInWithGoogle();
await app.signInWithGitHub();
await app.signInWithFacebook();
await app.signInWithDropbox();

const redirectResult = await app.handleSignInRedirect();
console.log('oauth redirect result', redirectResult);

SSR Token

const app = connectApp({ endpoint: 'https://flare.zuzcdn.net', appId: 'my-app', apiKey: 'ak' });

const ssr = await app.issueSsrToken(120);
console.log(ssr.token, ssr.expires_in);

Push APIs

const app = connectApp({ endpoint: 'https://flare.zuzcdn.net', appId: 'my-app', apiKey: 'ak' });

await app.setupPushServiceWorker();
await app.requestPushPermission();

const { token } = await app.acquireBrowserPushToken();
await app.registerPushToken({ token, platform: 'web', topics: ['news'] });

await app.enableBrowserPush({ topics: ['marketing'] });

await app.sendPushNotification({
  title: 'Hello',
  body: 'Welcome back',
  topic: 'news',
});

await app.unregisterPushToken(token);

Burst Stream API (Chat, Group Chat, Activity Feeds)

Use stream() to keep a live in-memory list with batched updates. This avoids one render per incoming change during message bursts.

const messageStream = app
  .collection('messages')
  .where({ roomId: 'room-1' })
  .latest()
  .limit(200)
  .stream({
    flushMs: 24,        // collapse burst updates into short windows
    maxBatchSize: 200,  // force flush when queue gets large
    insertAt: 'start',  // keep latest-first lists stable for chat UIs
    maxDocs: 200,
  });

const stop = messageStream.subscribe((rows, meta) => {
  console.log('rows', rows.length, 'ready', meta.ready, 'reason', meta.reason);
});

// Read current snapshot any time (works well with external-store patterns)
const currentRows = messageStream.getSnapshot();

// Optional subscription-level error hooks
messageStream
  .onError((err) => console.error('stream error', err))
  .onPermissionDenied((err) => console.error('permission denied', err));

// Cleanup
stop();
messageStream.close();

React.js Example

import { useEffect, useMemo, useState } from 'react';
import { connectApp } from '@zuzjs/flare';

type Message = {
  id: string;
  roomId: string;
  text: string;
  createdAt: number;
};

const app = connectApp({ endpoint: 'https://flare.zuzcdn.net', appId: 'my-app', apiKey: 'ak' });

export function RoomMessages({ roomId }: { roomId: string }) {
  const [rows, setRows] = useState<readonly Message[]>([]);
  const [ready, setReady] = useState(false);

  const stream = useMemo(() => {
    return app
      .collection<Message>('messages')
      .where({ roomId })
      .latest()
      .limit(200)
      .stream({ flushMs: 20, maxBatchSize: 250, insertAt: 'start', maxDocs: 200 });
  }, [roomId]);

  useEffect(() => {
    const stop = stream.subscribe((nextRows, meta) => {
      setRows(nextRows);
      setReady(meta.ready);
    });

    return () => {
      stop();
      stream.close();
    };
  }, [stream]);

  if (!ready) return <p>Loading messages...</p>;

  return (
    <ul>
      {rows.map((m) => (
        <li key={m.id}>{m.text}</li>
      ))}
    </ul>
  );
}

Next.js Example (Client Component)

'use client';

import { useSyncExternalStore } from 'react';
import { connectApp } from '@zuzjs/flare';

type Message = { id: string; roomId: string; text: string; createdAt: number };

const app = connectApp({
  endpoint: process.env.NEXT_PUBLIC_FLARE_ENDPOINT!,
  appId: process.env.NEXT_PUBLIC_FLARE_APP_ID!,
  apiKey: process.env.NEXT_PUBLIC_FLARE_API_KEY,
});

export default function RoomStream({ roomId }: { roomId: string }) {
  const store = app
    .collection<Message>('messages')
    .where({ roomId })
    .latest()
    .limit(200)
    .asStore({ flushMs: 20, maxBatchSize: 250, insertAt: 'start', maxDocs: 200 });

  const rows = useSyncExternalStore(
    store.subscribe,
    store.getSnapshot,
    store.getServerSnapshot,
  );

  return (
    <section>
      {rows.map((m) => (
        <p key={m.id}>{m.text}</p>
      ))}
    </section>
  );
}

Redux Example

import { createSlice, PayloadAction, configureStore } from '@reduxjs/toolkit';
import { connectApp } from '@zuzjs/flare';

type Message = { id: string; roomId: string; text: string; createdAt: number };

const app = connectApp({ endpoint: 'https://flare.zuzcdn.net', appId: 'my-app', apiKey: 'ak' });

const messagesSlice = createSlice({
  name: 'messages',
  initialState: [] as Message[],
  reducers: {
    replaceMessages: (_state, action: PayloadAction<readonly Message[]>) => [...action.payload],
  },
});

export const { replaceMessages } = messagesSlice.actions;
export const store = configureStore({ reducer: { messages: messagesSlice.reducer } });

export function startRoomMessageStream(roomId: string): () => void {
  const stream = app
    .collection<Message>('messages')
    .where({ roomId })
    .latest()
    .limit(200)
    .stream({ flushMs: 20, maxBatchSize: 250, insertAt: 'start', maxDocs: 200 });

  const stop = stream.subscribe((rows) => {
    store.dispatch(replaceMessages(rows));
  });

  return () => {
    stop();
    stream.close();
  };
}

External Store Bridge (No Framework Dependency)

The client also exposes asStore() so app code can plug into external-store hooks while keeping this package free of framework dependencies.

const messageStore = app
  .collection('messages')
  .where({ roomId: 'room-1' })
  .latest()
  .limit(200)
  .asStore({ flushMs: 24, maxBatchSize: 200, insertAt: 'start' });

// In app code, pass these to your UI store hook:
messageStore.subscribe;      // (onStoreChange) => unsubscribe
messageStore.getSnapshot;    // () => readonly rows
messageStore.getServerSnapshot; // () => []

// Optional advanced access
messageStore.stream.onError((err) => console.error(err));

// Cleanup
messageStore.destroy();

Direct Query Helpers (Knex-Style)

collection() query chaining uses object-based logical steps and dedicated operator families:

  • Logic: where({...}), and({...}), or({...})
  • in family: in, andIn, orIn
  • notIn family: notIn, andNotIn, orNotIn
  • arrayContains family: arrayContains, andArrayContains, orArrayContains
  • arrayContainsAny family: arrayContainsAny, andArrayContainsAny, orArrayContainsAny
  • some family (array of objects): some, andSome, orSome
  • like family: like, andLike, orLike
  • notLike family: notLike, andNotLike, orNotLike
  • exists family: exists, andExists, orExists
  • notExists family: notExists, andNotExists, orNotExists
const uid = 'bDEgnSqsEDT5qdDdtOX1';

const boards = await app
  .collection('boards')
  .where({ uid })
  .orArrayContains('team', uid)
  .orderBy('createdAt', 'desc')
  .limit(20)
  .get();

const sameBoards = await app
  .collection('boards')
  .where({ uid })
  .orArrayContains('team', uid)
  .get();

const active = await app
  .collection('tasks')
  .in('status', ['todo', 'doing'])
  .andArrayContainsAny('labels', ['urgent', 'backend'])
  .andLike('title', '%bug%')
  .get();

const boardAccess = await app
  .collection('boards')
  .some('team', { uid: 'xyz', role: 1 })
  .get();

Advanced Joins And Relations

Use join() when you want direct field mapping, including array/object-path sources.

const boardWithLists = await app
  .collection('boards')
  .where({ id: boardId, uid: me.uid })
  .join('lists', {
    source: 'id',
    target: 'boardId',
    as: 'lists',
  })
  .limit(1)
  .get();

const boardWithTeamMembers = await app
  .collection('boards')
  .where({ id: boardId, uid: me.uid })
  .join('users', {
    source: 'team.uid', // team: [{ uid, role }]
    target: 'id',
    as: 'teamMembers',
  })
  .limit(1)
  .get();

Use withRelation() for SQL-style shorthand.

const boardWithTeamMembers = await app
  .collection('boards')
  .where({ id: boardId, uid: me.uid })
  .withRelation('team.uid->users.id', { as: 'teamMembers' })
  .limit(1)
  .get();

const boardWithInlineAlias = await app
  .collection('boards')
  .where({ id: boardId, uid: me.uid })
  .withRelation('team.uid->users.id as teamMembers')
  .limit(1)
  .get();

const boardWithMultipleJoins = await app
  .collection('boards')
  .where({ id: boardId, uid: me.uid })
  .join('lists', { source: 'id', target: 'boardId', as: 'lists' })
  .join('users', { source: 'team.uid', target: 'id', as: 'teamMembers' })
  .limit(1)
  .get();

const boardWithNestedJoinChain = await app
  .collection('boards')
  .join('lists', { source: 'id', target: 'boardId', as: 'lists' })
  .joinNested('lists', 'cards', { source: 'id', target: 'listId', as: 'cards' })
  .joinNested('cards', 'comments', { source: 'id', target: 'cardId', as: 'comments' })
  .get();

Nested join options are supported per relation (where, orderBy, limit, offset, select, and nested joins).

Join alias note:

  • .join('users', ...) and .join('_users', ...) both resolve to the app auth-users collection (_flare_auth_users) on server.
  • Use _users when you want an explicit auth-collection relation in query code.

Data Mapper (Collection + Join Alias)

You can provide dataMapper in connectApp(...) to shape inbound rows on the client side.

Mapping rules:

  • Base collection rows use the mapper key matching the collection name.
  • Joined rows use the mapper key matching join as.
import { connectApp } from '@zuzjs/flare';

const app = connectApp({
  endpoint: 'https://flare.zuzcdn.net',
  appId: 'my-app',
  apiKey: 'FA_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  dataMapper: {
    boards: (row) => ({
      id: row.id,
      name: row.name,
      description: row.description,
      createdAt: new Date(row.createdAt ?? row.created_at),
    }),
    team: (row) => ({
      id: row.id,
      name: row.authMeta?.additionalParams?.name || 'Unknown',
      email: row.email,
      createdAt: new Date(row.createdAt ?? row.created_at),
    }),
  },
});

const boards = await app
  .collection('boards')
  .where({ boardId: '123' })
  .join('users', {
    source: 'team.uid',
    target: 'uid',
    as: 'team',
  })
  .get();

// boards[*] is mapped by dataMapper.boards
// boards[*].team[*] is mapped by dataMapper.team (join alias)

Tip: If your join alias is team, define dataMapper.team (not dataMapper.users) for that join payload.

Template-Based Email APIs

Security Rules Example (Boards Owner Or Team Member)

For a boards document shape like:

{
  "uid": "ownerUid",
  "team": ["memberUid1", "memberUid2"]
}

Use this DSL to allow read for owner or team member, and allow write only for owner:

service cloud.firestore {
  match /databases/{database}/documents {
    function isOwner(ownerUid) {
      return auth != null && auth.uid == ownerUid;
    }

    function isTeamMember(teamUids) {
      return auth != null && auth.uid in teamUids;
    }

    match /boards/{boardId} {
      allow read: if isOwner(resourceData.uid) || isTeamMember(resourceData.team);
      allow create, update, delete: if isOwner(resourceData.uid);
    }
  }
}

Tip: if you set owner uid at create time, prefer checking requestData.uid on create and resourceData.uid on update/delete.

Emails are sent only through app-level templates stored in _flare_email_templates.

Template placeholders use {key} syntax and are replaced from values.

If template has includeVerificationLink: true, server generates a link in _flare_email_links and injects:

  • {verificationLink}
  • {verifyUrl}
  • {verificationToken}
const app = connectApp({ endpoint: 'https://flare.zuzcdn.net', appId: 'my-app', apiKey: 'ak' });

const sendRes = await app.sendEmail({
  to: '[email protected]',
  tag: 'team_invite',
  values: {
    displayName: 'Alice',
    inviterName: 'Bob',
    teamName: 'Product',
  },
});

console.log(sendRes.sent, sendRes.tag, sendRes.verifyUrl);

const verifyRes = await app.verifyEmailLink({
  token: '<token-from-link>',
  tag: 'team_invite',
  email: '[email protected]',
});

console.log(verifyRes.verified, verifyRes.tag);

Template Collection Example

Example document in _flare_email_templates:

{
  "tag": "team_invite",
  "enabled": true,
  "subject": "Hi {displayName}, you are invited to {teamName}",
  "text": "Hello {displayName},\n\n{inviterName} invited you.\n\nOpen: {verificationLink}",
  "html": "<p>Hello {displayName}</p><p>{inviterName} invited you.</p><p><a href=\"{verificationLink}\">Accept</a></p>",
  "includeVerificationLink": true,
  "verifyUrl": "https://app.example.com/accept?token=__TOKEN__&appId=__APP_ID__&tag=__TAG__",
  "verificationTtlHours": 72
}

Links

  • Server package: @zuzjs/flare-server
  • Documentation: https://flare.zuz.com.pk

License

MIT