NukeBase

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. Use await or .then().
  • Server: Returns the result directly. No await needed (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 write examples (server-side)
// 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" with message: "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.

Same method, two return styles
// 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
  • requestId length: ≤ 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.

Array vs object auto-creation
// 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] } } }
Setting data examples
// 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:

Getting data examples
// 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.

Updating data examples
// 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:

Removing data examples
// 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:

Using the data export (server only)
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.