CRUD Operations
The same five methods — get, set, update, remove, query — work identically on both client and server. The only difference is how results are returned:
Sync vs Async:
- Client: Returns a
Promise. Useawaitor.then(). - Server: Returns the result directly. No
awaitneeded (the in-memory database is accessed without a network round-trip).
Durable Writes
By default, writes (set, update, remove) are applied to the in-memory database immediately and flushed to disk on a debounce timer (~5 seconds). If you need to guarantee that a write is persisted to disk before continuing, pass the durable option:
// Durable set — waits for fsync before resolving
await set(["users", "john", "email"], "john@example.com", "root", { durable: true });
// Durable update
await update(["users", "john"], { lastLogin: Date.now() }, "root", { durable: true });
// Durable remove
await remove(["users", "john", "tempData"], "root", { durable: true });
How durable writes work:
- The write is applied to the in-memory database immediately (subscribers and triggers fire as normal).
- The response is held until the data is flushed to disk via
fsync. - Multiple durable writes in the same event-loop tick are batched into a single
fsync(group commit), so there is no per-write I/O penalty. - If the flush fails, the response returns
status: "Failed"withmessage: "Durable flush failed", even though the in-memory state was updated. A retry timer will attempt to reconcile disk later.
When to use durable: Use { durable: true } for writes where data loss on crash is unacceptable — authentication tokens, financial transactions, user-created content. Skip it for ephemeral data like presence status or analytics counters where the debounce flush is sufficient.
Client-side durable writes: The durable flag also works from the client SDK over WebSocket. The server holds the WebSocket response until the fsync completes, so the client's await only resolves once the data is on disk.
// Client (async)
const user = await get(["users", "john"]);
console.log(user.data);
// Server (sync)
const user = get(["users", "john"]);
console.log(user.data);
Examples in this section use the client (async) form. To use any of these on the server, drop await / .then() — the method calls themselves are unchanged.
Path argument shape: path must be an array for all data operations (get, set, update, remove, query) and their subscription variants — even for a single segment, write ["users"], not "users". (Internally a string path is only meaningful as the name of a registered callable when invoking callableFunction; it is not a one-segment shorthand for data ops, and would be iterated character-by-character.)
Hard limits (enforced server-side — operations that exceed any of these are rejected):
- Path depth: ≤ 64 segments
- Path segment length: ≤ 256 characters per string segment
- Numeric segments: non-negative integers ≤ 100,000 (no
NaN,Infinity, negatives, or non-integers) - Forbidden segments:
"",".","..", anything containing/,\, or null bytes, or any prototype-pollution key (__proto__,constructor,prototype, etc.) - Update merge depth: ≤ 64 levels (deeper merges are rejected)
- Query string length: ≤ 512 characters
- Subscriptions per WebSocket session: ≤ 256
requestIdlength: ≤ 128 characters (echoed back in replies)
These limits prevent individual clients from holding open too many subscriptions or amplifying server CPU/memory with adversarial payloads. Stay well under them in normal use.
Setting Data
The set() function creates or replaces data at a specific path:
Auto-creation: The set() function automatically creates any missing parent containers in the path. The type of each created container is chosen by the next segment:
- String segment → object (
{}) - Integer segment → array (
[])
Numeric path segments must be actual integers, not numeric strings — 0 and "0" behave differently. The path validator only accepts non-negative integers as numeric segments; numeric strings are treated as object keys.
// Integer segment → array container is auto-created
set(["messages", 0], "hi");
// Result: { messages: ["hi"] }
// String segment → object container is auto-created
set(["messages", "0"], "hi");
// Result: { messages: { "0": "hi" } }
// Mixed: a nested integer segment creates an array inside an object
set(["users", "matt", "scores", 0], 100);
// Result: { users: { matt: { scores: [100] } } }
// Set a complete object
set(["users", "john"], { name: "John Doe", age: 32 }).then(response => {
console.log("User created successfully");
});
// Set a single value
set(["users", "john", "email"], "john@example.com").then(response => {
console.log(response);
});
// Auto-creates parent objects - even if 'users' doesn't exist
set(["users", "alice", "profile", "preferences", "theme"], "dark").then(response => {
// Creates: { users: { alice: { profile: { preferences: { theme: "dark" } } } } }
console.log("Theme set with auto-created parent objects");
});
Getting Data
Retrieve data with the get() function:
// Get a single user
get(["users", "john"]).then(response => {
console.log(response.data); // User data
});
// Get entire collection
get(["users"]).then(response => {
const users = response.data;
// Process users...
});
Updating Data
Update existing data without replacing unspecified fields:
Auto-creation: Same behavior as set() — missing parent containers are created automatically, with the type chosen by the next segment (string → object, integer → array). See the array-vs-object example under Setting Data above.
// Update specific fields
update(["users", "john"], {
lastLogin: Date.now(),
loginCount: 42
}).then(response => {
console.log(response);
});
// Update a single property
update(["users", "john", "status"], "online").then(response => {
console.log(response);
});
// Auto-creates missing parent objects
update(["settings", "app", "notifications", "email"], true).then(response => {
// If 'settings' doesn't exist, creates the entire path
console.log("Setting created with auto-generated parents");
});
Removing Data
Delete data at a specific path:
// Remove a user
remove(["users", "john"]).then(response => {
console.log("User deleted");
});
// Remove a specific field
remove(["users", "john", "temporaryToken"]).then(response => {
console.log(response);
});
Server-Side: Direct Access via data
On the server only, the data export gives you direct read access to the raw in-memory database object. This skips the overhead of get() for fast lookups inside triggers, callables, and middleware:
module.exports = ({ data, get, set, ... }) => {
// Direct read — access the raw database object
const userName = data.users?.john?.name; // "John"
const allUsers = data.users; // { john: {...}, alice: {...} }
// Compared to using get():
const user = get(["users", "john"]);
console.log(user.data.name); // "John"
};
Read-only. Always use set(), update(), and remove() to modify data — these run subscriptions, triggers, security rules, and persistence. Writing directly to data bypasses all of these.