@zuzjs/flare-admin
v0.1.16
Published
Privileged server-side access for Flare. Designed for secure environments to perform administrative tasks, manage user identities at scale, and orchestrate system-wide notifications with full bypass of client-side security rules.
Maintainers
Readme
@zuzjs/flare-admin
Server-side admin SDK for FlareServer — the self-hosted Firebase alternative.
Works likefirebase-admin: runs only on your backend, never in a browser.
How it works
Your Express app HTTP REST FlareServer
────────────────── ──────────► ─────────────────────
connectApp({ POST /admin/token Validates adminKey
serverUrl, { uid, role, claims } Signs JWT w/ jwtSecret
appId, ◄──────────────── Returns { token }
adminKey,
})
admin.auth()
.createCustomToken(uid)
→ Promise<string> ──────────────────────► client: flare.auth(token)
socket elevated ✓No MongoDB URI is ever shared. The SDK talks to FlareServer's /admin/token REST endpoint using only the adminKey.
Installation
npm install @zuzjs/flare-admin
# or
pnpm add @zuzjs/flare-adminQuick start
1. Get your keys
flare app create my-app| Config | Safe for... |
|---|---|
| apiKey | Browser / client-side |
| adminKey | Server-side only — never expose to browser |
2. Set environment variables
# .env (server-side only)
FLARE_URL=https://flare.zuzcdn.net
FLARE_APP_ID=my-app
FLARE_ADMIN_KEY=FA_ADMIN_xxxxxxxxxxxx3. Initialize once at boot
import { connectApp } from "@zuzjs/flare-admin";
const admin = connectApp({
serverUrl: process.env.FLARE_URL!,
appId: process.env.FLARE_APP_ID!,
adminKey: process.env.FLARE_ADMIN_KEY!,
});4. Mint a token in your login route
import { getApp } from "@zuzjs/flare-admin";
app.post("/login", async (req, res) => {
const user = await myAuthLogic(req.body);
if (!user) return res.status(401).json({ error: "invalid credentials" });
const flareToken = await getApp().auth().createCustomToken(user.id, {
role: user.isAdmin ? "admin" : "user",
claims: { email: user.email, plan: user.plan },
});
res.json({ user, flareToken });
});5. Authenticate the Flare client
// Browser / React Native
import FlareClient from "@zuzjs/flare";
const flare = new FlareClient({
endpoint: "https://flare.zuzcdn.net",
appId: "my-app",
apiKey: "FA_xxxxxxxx",
});
flare.connect();
const { flareToken } = await fetch("/login", { method: "POST", body: ... }).then(r => r.json());
await flare.auth(flareToken);
// ✅ Socket is now elevated — auth.uid and auth.role are setAPI
connectApp(config, name?)
Initialize a FlareAdmin app. Idempotent — safe to call at module scope.
const admin = connectApp({
serverUrl: string; // FlareServer base URL
appId: string; // App ID from `flare app create`
adminKey: string; // Admin key — server-side only
httpBase?: string; // Override base URL for all admin HTTP APIs (e.g. proxy)
defaultTtl?: string; // Default token TTL, e.g. "24h" (default)
dataMapper?: Record<string, (row: any) => any>; // Per-collection response mappers
});httpBase routes all admin HTTP traffic through a different base URL (useful for proxies or gateways). When set, it replaces serverUrl as the base for every /admin/* HTTP request. Trailing slashes are normalized automatically.
getApp(name?)
Retrieve an already-initialized app instance. Throws if not initialized.
disconnectApp(name?)
Disconnect and remove an app from the registry. Returns true if the app existed.
disconnectApp(); // default app
disconnectApp("app-a"); // named appdisconnectAllApps()
Disconnect and clear every initialized app.
disconnectAllApps();app.disconnect()
Close the WebSocket connection on a specific app instance. Safe to call multiple times.
admin.disconnect();Auth
admin.auth().createCustomToken(uid, opts?)
Mint a custom auth token for use by the browser client.
const token = await admin.auth().createCustomToken(uid, {
role?: "user" | "admin" | "anon", // default: "user"
claims?: Record<string, unknown>,
ttl?: string, // e.g. "1h", "7d"
});admin.auth().getTicket(uid, opts?)
Mint a one-time ticket for WebSocket auth flows.
const ticket = await admin.auth().getTicket(uid, {
role?: "user" | "admin" | "anon",
email?: string,
sid?: string,
ttlSeconds?: number,
ip?: string,
});
// ticket shape:
// {
// ticket: "websocket:550e8400-...",
// tag: "websocket",
// uuid: "550e8400-...",
// expires_at: "2026-04-15T12:34:56Z",
// one_time: true,
// uid: "user_123",
// role: "user",
// ip: "203.0.113.20"
// }Database
// One-shot queries (bypasses security rules)
const users = await admin.db().collection("users").get();
await admin.db().collection("users").doc("alice").set({ name: "Alice" });
await admin.db().collection("users").update("alice", { plan: "pro" });
await admin.db().collection("users").doc("alice").update({ plan: "pro" });
await admin.db().collection("users").doc("alice").delete();
// Rich queries
const seniors = await admin.db()
.collection("users")
.where({ age: ">= 60" })
.orderBy("name")
.limit(10)
.get();Query Builder
admin.db().collection(...) supports the full structured query API:
Filters: where, and, or, in, andIn, orIn, notIn, andNotIn, orNotIn, arrayContains, andArrayContains, orArrayContains, arrayContainsAny, andArrayContainsAny, orArrayContainsAny, some, andSome, orSome, like, andLike, orLike, notLike, andNotLike, orNotLike, exists, andExists, orExists, notExists, andNotExists, orNotExists
Sort / cursor / aggregate: latest, newest, oldest, orderBy, limit, offset, startAt, startAfter, endAt, endBefore, count, sum, avg, min, max, distinct, groupBy, having, select, distinctField, vectorSearch
Joins: join, Join, joinNested, JoinNested, withRelation
const rows = await admin.db()
.collection("boards")
.where({ uid: userId })
.orSome("team", { uid: userId })
.join("lists", { source: "id", target: "boardId", as: "lists" })
.join("users", { source: "team.uid", target: "id", as: "teamMembers" })
.joinNested("lists", "cards", { source: "id", target: "listId", as: "cards" })
.withRelation("team.uid->users.id as collaborators")
.orderBy("updatedAt", "desc")
.limit(20)
.get();Bulk Writes (Memory Efficient)
addMany, updateMany, and id-based deleteMany run in bounded chunks so large datasets can be processed with controlled memory usage.
const users = admin.db().collection<{ name: string; plan?: string }>("users");
const addResult = await users.addMany(
[{ name: "Alice" }, { name: "Bob" }],
{
batchSize: 500,
concurrency: 8,
onProgress: (p) => {
console.log("addMany", p.processed, p.total, p.percent);
},
},
);
const updateResult = await users.updateMany(
[
{ id: "user_1", data: { plan: "pro" } },
{ id: "user_2", data: { plan: "team" } },
],
{ continueOnError: true },
);
// Delete specific ids with progress
const deleteByIdsResult = await users.deleteMany(["user_3", "user_4"], {
onProgress: (p) => console.log("deleteMany(ids)", p.processed, p.total),
});
// Existing query-based deleteMany remains available
const deletedByQueryCount = await admin.db()
.collection("users")
.where({ plan: "free" })
.deleteMany();
console.log({ addResult, updateResult, deleteByIdsResult, deletedByQueryCount });// Stream input from an async source (best for huge datasets)
async function* rows() {
for (let i = 0; i < 1_000_000; i += 1) {
yield { name: `user-${i}` };
}
}
await admin.db().collection("users").addMany(rows(), { batchSize: 1000, concurrency: 4 });allowSensitiveAuthUserFields(false)
Auth-user joins default to full fields. Pass false to restrict to public-profile-only output:
const safeBoards = await admin.db()
.collection("boards")
.join("users", { source: "team.uid", target: "id", as: "team" })
.allowSensitiveAuthUserFields(false)
.get();getRawQuery()
Inspect the structured query that will be sent:
const raw = admin.db()
.collection("boards")
.where({ uid: userId })
.getRawQuery();
console.log(raw.collection, raw.query);Realtime
admin.live().collection(...).onSnapshot(cb)
Single-snapshot subscription — fires once on the initial data load, then auto-unsubscribes:
const unsub = admin.live()
.collection("orders")
.where({ status: "pending" })
.orderBy("createdAt", "desc")
.onSnapshot((snap) => {
console.log(snap.type, snap.data);
});.stream(options?) — live batched stream
Returns a long-lived AdminCollectionStream<T> that maintains a local snapshot and fans out batched change events to listeners.
const stream = admin.live()
.collection("orders")
.where({ status: "pending" })
.stream({
flushMs?: number, // batch flush delay in ms (default: 24)
maxBatchSize?: number, // max changes per flush (default: 200)
insertAt?: "start" | "end", // where new docs land (default: "end")
maxDocs?: number, // cap the local snapshot size
sort?: (a: T, b: T) => number, // comparator applied after each flush
idField?: string, // identity field name (default: "id")
getId?: (doc: T) => string, // custom id extractor
});
// Subscribe to changes
const off = stream.listen((docs, meta) => {
console.log(meta.reason, meta.version, docs.length);
// meta.reason: "snapshot" | "change-batch"
// meta.ready: true after the first snapshot arrives
});
// Read the current snapshot without subscribing
const current = stream.getSnapshot();
// Remove a specific listener (does not close the stream)
off();
// Close the stream and release the WebSocket subscription
stream.close();.asStore(options?) — framework-agnostic external store
Returns an AdminCollectionExternalStore<T> compatible with React useSyncExternalStore or any subscribe/getSnapshot pattern:
const store = admin.live()
.collection("orders")
.asStore({ flushMs: 50 });
// React
import { useSyncExternalStore } from "react";
const orders = useSyncExternalStore(store.subscribe, store.getSnapshot);
// Or directly
const unsub = store.subscribe(() => {
console.log(store.getSnapshot());
});
store.close(); // release when doneRealtime query builder parity
The same query builder surface (where, orderBy, join, limit, etc.) is available on admin.live().collection(...) just like on admin.db().collection(...).
Security rules
Once a client calls flare.auth(token), the FlareServer socket has:
auth.uid = the uid you passed to createCustomToken()
auth.role = "user" | "admin" | "anon"{
"users": { ".read": "auth != null", ".write": "auth.uid == $docId" },
"posts": { ".read": "true", ".write": "auth != null" },
"settings": { ".read": "auth != null", ".write": "auth.role == 'admin'" },
"*": { ".read": "false", ".write": "false" }
}Multi-tenant / multiple apps
const adminA = connectApp({ serverUrl, appId: "app-a", adminKey: "..." }, "a");
const adminB = connectApp({ serverUrl, appId: "app-b", adminKey: "..." }, "b");
const tokenA = await getApp("a").auth().createCustomToken(userId);
const tokenB = await getApp("b").auth().createCustomToken(userId);
disconnectAllApps();Data Mapper
Pass dataMapper in connectApp(...) to shape inbound data. Keys match collection names or join aliases (as).
const admin = connectApp({
serverUrl: process.env.FLARE_URL!,
appId: process.env.FLARE_APP_ID!,
adminKey: process.env.FLARE_ADMIN_KEY!,
dataMapper: {
boards: (row) => ({
id: row.id,
name: row.name,
createdAt: new Date(row.createdAt ?? row.created_at),
}),
team: (row) => ({
id: row.id,
name: row.authMeta?.additionalParams?.name || "Unknown",
email: row.email,
}),
},
});For join(..., { as: "team" }), define dataMapper.team.
Storage API (S3-like)
import { connectApp, AdminStorageSignedAction } from "@zuzjs/flare-admin";
const storage = admin.storage();
await storage.createBucket("reports");
await storage.putObject({
bucket: "reports",
key: "weekly/summary.json",
body: Buffer.from(JSON.stringify({ ok: true })),
contentType: "application/json",
access: "public",
// encrypt defaults to false when omitted
});
const uploaded = await storage.putObject({
bucket: "reports",
key: "weekly/private-summary.json",
body: Buffer.from(JSON.stringify({ ok: true, scope: "internal" })),
contentType: "application/json",
access: "private",
encrypt: true,
});
console.log(uploaded.key);
console.log(uploaded.access);
console.log(uploaded.url);
const meta = await storage.headObject({ bucket: "reports", key: "weekly/summary.json" });
console.log(meta.access, meta.url);
const downloaded = await storage.getObject({ bucket: "reports", key: "weekly/summary.json", decrypt: true });
const listed = await storage.listObjects({ bucket: "reports", prefix: "weekly/" });
console.log(listed.objects[0]?.access, listed.objects[0]?.url);
await storage.deleteObjects({ bucket: "reports", keys: ["weekly/summary.json"] });Signed URLs
const signedUpload = await admin.createSignedUrl({
bucket: "reports",
key: "uploads/big-video.mp4",
action: AdminStorageSignedAction.Upload,
expiresInSeconds: 300,
contentType: "video/mp4",
access: "private",
encrypt: true,
});
await fetch(signedUpload.url, {
method: signedUpload.method,
headers: { "Content-Type": "video/mp4" },
body: videoBuffer,
});
const signedDownload = await admin.createSignedUrl({
bucket: "reports",
key: "uploads/big-video.mp4",
action: AdminStorageSignedAction.Download,
expiresInSeconds: 300,
decrypt: true,
allowedOrigins: ["https://app.example.com"],
});Notes:
putObject()defaults toencrypt: falseandaccess: "public".- Upload, head, and list responses all surface
accessandurlso you can carry public/private intent alongside the object key. forceDownloadandembedOnlyare mutually exclusive.allowedOriginsdefaults to['*']if omitted.
Direct download helpers
const directUrl = await admin.getObjectUrl({
bucket: "reports",
key: "weekly/summary.json",
expiresInSeconds: 120,
allowedOrigins: ["*"],
});
const triggered = await admin.downloadObject({
bucket: "reports",
key: "weekly/summary.json",
filename: "summary.json",
forceDownload: true,
allowedOrigins: ["https://app.example.com"],
});Storage rules
service zuz.storage {
match /storage/{bucket}/objects {
match /{document=**} {
allow read: if auth != null;
allow write: if auth != null
&& requestData.storage.usage.storageBytes + requestData.size
<= requestData.storage.plan.storageBytes;
allow delete: if auth != null;
}
}
}External AWS SDK from Flare /aws config
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const aws = await admin.storage().awsConfig("storage-server-id");
const s3 = new S3Client({
endpoint: aws.endpoint,
region: aws.region,
forcePathStyle: Boolean(aws.forcePathStyle),
credentials: {
accessKeyId: aws.accessKeyId,
secretAccessKey: aws.secretAccessKey,
},
});
await s3.send(new PutObjectCommand({
Bucket: aws.bucket,
Key: `${aws.prefix ?? ""}manual/test.txt`,
Body: "hello",
ContentType: "text/plain",
}));Push Notifications
await admin.notifications().send({
uid: "user_123",
title: "Hello",
body: "Your order shipped!",
data: { orderId: "abc" },
});