@zuzjs/flare
v0.2.12
Published
Official JavaScript/TypeScript client for ZuzFlare Server - Self-hosted real-time database
Downloads
1,198
Maintainers
Readme
ZuzFlare Client
Official JavaScript/TypeScript client for ZuzFlare Server.
Installation
npm install @zuzjs/flareMaintainer 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({...}) infamily:in,andIn,orInnotInfamily:notIn,andNotIn,orNotInarrayContainsfamily:arrayContains,andArrayContains,orArrayContainsarrayContainsAnyfamily:arrayContainsAny,andArrayContainsAny,orArrayContainsAnysomefamily (array of objects):some,andSome,orSomelikefamily:like,andLike,orLikenotLikefamily:notLike,andNotLike,orNotLikeexistsfamily:exists,andExists,orExistsnotExistsfamily: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
_userswhen 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
