NukeBase

Server-Side API

NukeBase is a managed service that provides instant provisioning and deployment. Your project structure includes:

  • server/database.js: The core database engine
  • server/data.json: Your database file
  • server/rules.js: Security rules configuration
  • server/app.js: Your application configuration file
  • public/: Frontend files (index.html, css, js, etc.)
  • sys/deploy.js: Deployment configuration
  • sys/config.json: System configuration
  • node_modules/: Dependencies (auto-generated)
  • package.json: NPM package configuration
  • package-lock.json: Dependency lock file

Setup and Initialization

Getting started with NukeBase is simple - provision your project through our managed service and start developing immediately.

Step 1: Create Your Project

Getting started is as simple as visiting a URL in your browser:

Create your project
1. Visit: https://nukebase.com/createuser
2. Fill in your project details (username, project name)
3. Click "Provision & Download"
4. Your project zip will download automatically

Instant Deployment: Your project is automatically provisioned, deployed, and live at:
https://username-project.nukebase.com

No build steps, no server configuration - just download the zip and start coding!

Step 2: Local Development Setup

After provisioning, extract the downloaded zip file and set up your VS Code workspace:

Setup in VS Code
# 1. Extract the downloaded project zip file
# Right-click the .zip file and select "Extract All"

# 2. Open VS Code
# File → Add Folder to Workspace → Select your extracted project folder

# 3. Open Terminal in VS Code
# Terminal → New Terminal → Select Folder As Directory

# 4. Install NukeBase CLI globally
npm install -g

# 5. Now you can use NukeBase commands:
nukebase push   # Push local changes to live server
nukebase pull   # Pull live server changes to local

NukeBase CLI Commands:

  • nukebase push - Upload your local changes to the live server
  • nukebase pull - Download the latest changes from the live server

Changes are synced in real-time, allowing you to develop locally and deploy instantly.

Push or pull will instantly remove or add folder/files to server/client unless sys/config "exclude": ["sys", "server/data.json"]

Step 3: Start Developing

Your project structure is ready to use:

  • /public: Edit your frontend files (HTML, CSS, JavaScript)
  • /server/app.js: Configure backend logic, domains, and database triggers
  • /server/rules.js: Define security rules for data access
  • /server/data.json: Your real-time database (auto-synced)

Hot Reload: Changes to your /public files are instantly reflected on your live site. Backend changes in /server/app.js are automatically deployed.

Basic Server Configuration Structure

Your server/app.js file uses a module export pattern that provides access to all NukeBase APIs:

server/app.js structure
module.exports = ({
  addFunction,
  addWsFunction,
  get,
  set,
  update,
  remove,
  query,
  generateRequestId,
  data,
  addDomain,
  startDB,
  withBody
}) => {

  const path = require("path");

  const nukebase = addDomain({
    authPath: ["users"],
    host: "127.0.0.1", // optional - defaults to "127.0.0.1"
    port: 3000 // optional - defaults to 3000
  });

  nukebase.app.serveStatic("/*", path.join(__dirname, "../public"),
    (req, res) => { return true; }
  );

  startDB(nukebase);
}

Security Rules

NukeBase uses a JSON-based security rules system to control access to your database. Rules are defined in server/rules.js and are evaluated for every database operation.

Available Variables in Rules:

  • admin - The authenticated user object with properties:
    • admin.uid - User's unique ID
    • admin.username - User's username
    • Any other custom properties from your user auth data
  • data - The current/old value at the path being accessed
  • newData - The new value being written (for write/validate rules)
  • newRoot - Access to the entire database with the new changes applied
  • $variables - Wildcard captures like $userId, $postId

Rule Types

Six types of rules control different aspects of data access:

  • read - Controls who can read data at a path (triggered by get() operations)
  • write - Controls who can create, update, or delete data (triggered by set(), update(), and remove() operations)
  • validate - Ensures data meets specific requirements (triggered by set() and update() operations)
  • query - Controls which items can be returned in query results (triggered by query() operations)
  • readFile - Controls who can download files from storage (triggered by file download requests)
  • writeFile - Controls who can upload files to storage (triggered by file upload requests)

How Rules Are Checked:

Parent rules override child rules. When checking a path like users.john.email, NukeBase checks rules starting from the top:

  1. Check users - If this denies access, STOP (don't check deeper rules)
  2. Check users.john - If this denies access, STOP
  3. Check users.john.email - Final check

If ANY parent rule denies access, the operation fails. All matching rules must pass.

Rule Matching at Same Level:

  • Read/Write rules: If you have both exact (pets) and wildcard ($other) rules at the same level, BOTH must pass for access to pets.
  • Validate rules: Only the most specific rule matches. Exact match (pets) takes priority over wildcard ($other).
Rule matching example
// These two rules are at the SAME LEVEL (both are direct children of the parent)
module.exports = {
  "pets": {
    "read": "true",  // Rule 1: Anyone can read pets
    "write": "admin.role == 'petOwner'",  // Rule 2: Must be pet owner
    "validate": "newData.type == 'cat' || newData.type == 'dog'"  // Only cats/dogs
  },
  "$other": {  // ← This is at the SAME LEVEL as "pets" above
    "read": "admin.role == 'admin'",  // Rule 3: Must be admin
    "write": "false",  // Rule 4: No writes allowed
    "validate": "newData != null"  // Not empty
  }
}

// When accessing "pets":
// Read: BOTH "true" AND "admin.role == 'admin'" must pass → Fails for non-admins!
// Write: BOTH "admin.role == 'petOwner'" AND "false" must pass → Always fails!
// Validate: ONLY the "pets" rule applies (most specific)

Basic Example

Simple security rules
module.exports = {
  "users": {
    "$userId": {
      "read": "true",  // Anyone can read user profiles
      "write": "admin.uid == $userId",  // Only the user can edit their profile
      "email": {
        "read": "admin.uid == $userId"  // Email is private
      }
    }
  }
};

Path Patterns

Rules support different path patterns to match your data structure:

Pattern Description Example
users.john Exact path matching Matches only users.john
users.$userId Wildcard matching Matches users.alice, users.bob, etc.
The $userId variable captures the actual key
posts.$postId Wildcard for collections Matches any child: posts.abc, posts.xyz, etc.
messages.$msgId Works with arrays too Arrays are objects with numeric keys
Matches messages.0, messages.1, messages.2

Arrays and Path Matching:

JavaScript arrays like ["red", "blue", "green"] are stored as objects with numeric keys:

{ "0": "red", "1": "blue", "2": "green" }

This means:

  • colors.0 - Exact match for first element
  • colors.$index - Wildcard matches all elements (0, 1, 2, etc.)
  • colors - Matches the array itself

Operations and Their Rules

Different database operations trigger different combinations of rules:

Operation Rules Triggered Description
get() read Only read rules are checked when retrieving data
set() write + validate Both write permission and data validation are required
update() write + validate Same as set() - must have permission and valid data
remove() write Only write rules are checked (newData is null)
query() query Query rules filter which items are returned
File Upload writeFile File upload permissions are checked before allowing file storage
File Download readFile File access permissions are checked before serving files

Rule Types in Detail

Read Rules

Control who can read data at a specific path:

Read rule examples
// Simple read rule
"posts": {
  "read": "true",  // Anyone can read posts
  "$postId": {
    "draft": {
      "read": "admin.uid == data.authorId"  // Only author can read drafts
    }
  }
}

// Using variables in paths
"users": {
  "$userId": {
    "read": "true",  // Anyone can read user profiles
    "email": {
      "read": "admin.uid == $userId"  // Only the user can read their own email
    }
  }
}
Write Rules

Control who can create, update, or delete data:

Write rule examples
// Basic write rule
"posts": {
  "$postId": {
    "write": "admin.uid == data.authorId",  // Only author can edit
    "createdAt": {
      "write": "!data"  // Can only set createdAt when creating (no previous data)
    }
  }
}

// Demonstrating rule override hierarchy
"store": {
  "write": "false",  // No one can write to store (overrides all child rules)
  "products": {
    "write": "admin.role == 'manager'",  // This is ignored due to parent rule
    "$productId": {
      "write": "admin.uid == data.ownerId"  // This is also ignored
    }
  }
}
Validate Rules

Ensure data integrity and format requirements:

Validate rule examples
// Simple field validation
"users": {
  "$userId": {
    "age": {
      "validate": "newData >= 13 && newData <= 120"
    },
    "email": {
      "validate": "newData.includes('@') && newData.includes('.')"
    }
  }
}

// Validating objects with required fields
"posts": {
  "$postId": {
    "validate": "newData.title && newData.content && newData.title.length <= 200"
  }
}

// Using data and newData to compare old and new values
"users": {
  "$userId": {
    "credits": {
      // Ensure credits can only increase, not decrease
      "validate": "newData >= data"
    }
  }
}

// Using newRoot to access other paths in validation
"users": {
  "$userId": {
    "friendRequests": {
      "$requestId": {
        // Verify the requester exists in the users collection
        "validate": "newRoot.users[newData.fromUserId] != null"
      }
    }
  }
}

// Complex validation with multiple conditions
"products": {
  "$productId": {
    "validate": "newData.name && newData.price > 0 && newData.stock >= 0"
  }
}
Array Validation

Arrays are validated using the same rule system, but understanding how paths are generated is essential for proper validation.

How Array Validation Works:

When you set/update an array, NukeBase generates validation paths for:

  • The array itself - Path to the array as a whole
  • Each array element - Individual paths like ["tags", "0"], ["tags", "1"]

Arrays are treated as objects with numeric keys: ["red", "blue"] becomes {"0": "red", "1": "blue"}

Array validation methods
// Example: update(["users", "john", "tags"], ["red", "blue", "green"])
// This generates paths:
// 1. ["users", "john", "tags"]     ← Entire array
// 2. ["users", "john", "tags", "0"] ← Element 0: "red"
// 3. ["users", "john", "tags", "1"] ← Element 1: "blue"
// 4. ["users", "john", "tags", "2"] ← Element 2: "green"

// METHOD 1: Validate the ENTIRE array
"users": {
  "$userId": {
    "tags": {
      // newData = entire array ["red", "blue", "green"]
      "validate": "Array.isArray(newData) && newData.length <= 5"
    }
  }
}

// METHOD 2: Validate EACH element using wildcard
"users": {
  "$userId": {
    "tags": {
      "$index": {  // $index matches "0", "1", "2", etc.
        // newData = individual element ("red", "blue", or "green")
        "validate": "typeof newData === 'string' && newData.length < 20"
      }
    }
  }
}

// METHOD 3: COMBINE both approaches
"users": {
  "$userId": {
    "tags": {
      // Validate array properties
      "validate": "Array.isArray(newData) && newData.length <= 5",
      "$index": {
        // Validate each element
        "validate": "typeof newData === 'string' && newData.length < 20"
      }
    }
  }
}

// Using newRoot to check against other data
"users": {
  "$userId": {
    "tags": {
      "$index": {
        // Verify each tag exists in the allowed tags list
        "validate": "newRoot.allowedTags[newData] != null"
      }
    }
  }
}

// Complex array validation with element uniqueness check
"users": {
  "$userId": {
    "favoriteColors": {
      "$index": {
        // Each color must be a valid hex code
        "validate": "typeof newData === 'string' && /^#[0-9A-F]{6}$/i.test(newData)"
      }
    }
  }
}

Important: Both the array-level rule AND element-level rules must pass. If you have rules at both levels, all of them are checked.

Query Rules

Control which items can be returned in query results:

Query rule examples
// Filter products by price for free users
"products": {
  "query": "child.price <= 100 || admin.role == 'premium'"  // Free users only see cheap products
}

// Private messaging system
"messages": {
  "query": "child.to == admin.uid || child.from == admin.uid"  // Only see your messages
}

// Show only published posts or user's own drafts
"posts": {
  "query": "child.published == true || child.authorId == admin.uid"
}
File Security Rules

Control file upload and download permissions using specialized file rules:

File security rule examples
// File paths are converted to dot notation in rules
"files": {
  "users": {
    "profile-pictures": {
      // Anyone can read profile pictures
      "readFile": "true",
      // Only authenticated users can upload
      "writeFile": "admin.uid != null"
    },
    "$userId": {
      "private": {
        // Only the user can access their private files
        "readFile": "admin.uid == $userId",
        "writeFile": "admin.uid == $userId"
      }
    }
  },
  "public": {
    // Public files accessible to everyone
    "readFile": "true",
    "writeFile": "admin.role == 'admin'"
  }
}

File Rules Notes:

  • File paths like users/profile-pictures/image.jpg become users.profile-pictures in security rules
  • Use readFile and writeFile rules (not read/write) for file operations
  • The system automatically stores file references in your database at the specified path
  • Files are automatically checked for upload completion and database references are updated

Available Variables

Rules have access to several context variables:

Variable Description Available In
data Current value at the path (before changes) All rule types
newData Value after the write operation write, validate
root Current database root All rule types
newRoot Database root after the write write, validate
admin Authentication object with user info All rule types
$variables Values from wildcard path segments All rule types
child Individual item in a query query only

Best Practices

  • Start with restrictive rules, then add exceptions as needed
  • Use validate rules to ensure data integrity
  • Test rules thoroughly before deploying to production
  • Keep rules simple and readable
  • Only one validate rule per path - combine conditions with && or ||
  • Remember that multiple read/write rules can match at the same depth, but higher rules override deeper ones
  • Validate rules only match the most specific rule at a given path

Common Mistakes to Avoid

Mistake 1: Multiple validate rules on same path

// WRONG - Only the last validate rule will be used!
"email": {
  "validate": "newData.includes('@')",
  "validate": "newData.includes('.')"  // This overwrites the first rule!
}

// CORRECT - Combine with &&
"email": {
  "validate": "newData.includes('@') && newData.includes('.')"
}

Database Triggers

Create event-driven functions that respond to database changes:

Database trigger example
// Create a trigger for when a request is updated
addFunction("onUpdate", ["requests", "$requestId"], async function(context) {
  // The context object contains all relevant information about the change
  const beforeNotes = context.dataBefore?.notes;
  const afterNotes = context.dataAfter?.notes;
  // Replace "pizza" with pizza emoji
  const newNotes = afterNotes.replaceAll("pizza", "🍕");
  // Avoid infinite loop by checking if we already replaced
  if (newNotes === afterNotes) {
    return;
  }
  // Update the data with our modified version
  update(context.path, { notes: newNotes });
});

Key components of database triggers:

  • addFunction(eventType, pathArray, callbackFunction)
  • Path arrays use wildcards like $userId to match any value at that position

Event Types

  • "onSet" - Triggered when data is created or completely replaced
  • "onUpdate" - Triggered when data is partially updated
  • "onRemove" - Triggered when data is deleted
  • "onValue" - Triggered for all changes (set, update, remove)

Path Patterns

Use an array path with wildcards to match specific data paths:

  • ["users", "$userId"] - Matches any user path like ["users", "john"] or ["users", "alice"]
  • ["posts", "$postId", "comments", "$commentId"] - Matches any comment on any post

Context Object

Your callback function receives a context object containing:

  • context.path - The complete path array that was changed (e.g., ["orders", "abc123"])
  • context.dataAfter - The data after the change (null for remove operations)
  • context.dataBefore - The data before the change (null for new data)

Important: When modifying data within a trigger that affects the same path you're watching, always implement safeguards to prevent infinite loops, as shown in the example.

Complete Example: Order Processing

Processing new orders
// React to new orders being created
addFunction("onSet", ["orders", "$orderId"], async function(context) {
  // Only run if this is a new order (no previous data)
  if (!context.dataBefore && context.dataAfter) {
    // Extract orderId from the path array
    const orderId = context.path[1];
      // Update the order status
    await update(context.path, {
      status: "processing",
      processingStart: Date.now()
    });
  }
});

WebSocket Functions

Create custom server functions that clients can call through wsFunction:

WebSocket function definition
addWsFunction("getUsersCount", async function (data, admin, sessionId) {
  //get all users
  var res = await get(["users"])
  //Count how many users
  count = Object.keys(res.data).length
  //return number
  return count
});

WebSocket functions receive:

  • Client-sent data
  • Admin flag (for protected operations)
  • User's session ID

Connection Events

Track client connections and disconnections:

Connection event handlers
// When a client connects
onConnection(function (admin, sessionId) {
    // Record session start time
    update(["sessions", admin.uid, sessionId], {
        start: Date.now()
    });
});

// When a client disconnects
onClose(function (admin, sessionId) {
    // Record session end time
    update(["sessions", admin.uid, sessionId], {
        end: Date.now()
    });
});

Starting the Database

Start the NukeBase server with configuration options by calling startDB() once at the end of your configuration:

Starting the database
// Basic setup - pass the domain object to startDB
const nukebase = addDomain({
  authPath: ["users"],
  host: "127.0.0.1", // optional
  port: 3000 // optional
});

startDB(nukebase);

addDomain Configuration Options:

  • authPath: Array - path to user authentication data (e.g., ["users"])
  • host: String (optional) - the IP address to bind to
    • Use "127.0.0.1" to accept connections only from the local machine (default)
    • Use a specific IP address like "126.23.45.1" to bind to that server address
    • Use "0.0.0.0" to accept connections from any IP
  • port: Number (optional) - the port to listen on (default: 3000)
Important: Call startDB() only once at the end of your server configuration. This function initializes and starts the database server with all configured domains and settings. Multiple calls to startDB() can cause resource conflicts and unexpected behavior. Once you've defined all your domains, middleware, triggers, and functions, finish with a single call to startDB() to launch your server.

Server-Side Data Operations

The same data operations available on the client (get, set, update, remove, query) are also available server-side in your app.js file. However, there is one key difference:

Synchronous vs Asynchronous:

  • Client-side: All operations are async and return Promises (use await or .then())
  • Server-side: All operations are sync and return results directly (no await needed)
Server-side data operations (synchronous)
module.exports = ({ get, set, update, remove, query, addWsFunction, ... }) => {

  // Server-side operations are SYNCHRONOUS - no await needed

  // Get data directly
  const user = get(["users", "john"]);
  console.log(user.data);  // { name: "John", age: 32 }

  // Set data directly
  set(["users", "alice"], { name: "Alice", age: 28 });

  // Update data directly
  update(["users", "john"], { lastLogin: Date.now() });

  // Remove data directly
  remove(["users", "oldUser"]);

  // Query data directly
  const activeUsers = query({
    path: ["users"],
    query: "child.status == 'active'"
  });
  console.log(activeUsers.data);

  // Example: Using in a WebSocket function
  addWsFunction("getUserStats", function(data, admin, sessionId) {
    // All these operations complete immediately (sync)
    const user = get(["users", admin.uid]);
    const orders = query({
      path: ["orders"],
      query: `child.userId == '${admin.uid}'`
    });

    return {
      username: user.data.name,
      orderCount: Object.keys(orders.data || {}).length
    };
  });

  // Example: Using in a database trigger
  addFunction("onSet", ["orders", "$orderId"], function(context) {
    // Sync operations in triggers
    const user = get(["users", context.dataAfter.userId]);
    update(["users", context.dataAfter.userId], {
      lastOrderDate: Date.now()
    });
  });

};

Why synchronous? Server-side operations access the in-memory database directly, eliminating the need for network round-trips. This makes your server code simpler and faster.

Connecting to External Databases

If you need to connect to another NukeBase database from your server (for example, a shared service or microservice architecture), you can use the server/serversdk.js module.

When to use serversdk.js:

  • Connecting to a separate NukeBase instance
  • Building microservices that communicate with each other
  • Aggregating data from multiple database servers
  • Server-to-server real-time synchronization
Using serversdk.js to connect to another database
module.exports = ({ get, set, update, addWsFunction, startDB, addDomain, ... }) => {

  // Import the server SDK for external connections
  const createServerClient = require('./serversdk.js');

  // Connect to an external NukeBase database
  // Note: External connections ARE async (like client-side)
  createServerClient('wss://other-project.nukebase.com').then(externalDb => {
    console.log('Connected to external database');

    // Use the external database with async operations
    addWsFunction("getExternalData", async function(data, admin, sessionId) {
      // Local database (sync)
      const localUser = get(["users", admin.uid]);

      // External database (async - requires await)
      const externalData = await externalDb.get(["sharedData", data.itemId]);

      return {
        local: localUser.data,
        external: externalData.data
      };
    });

    // Subscribe to changes on external database
    externalDb.getSub({
      event: "value@",
      path: ["notifications"]
    }, (event) => {
      // When external data changes, update local database
      set(["cache", "externalNotifications"], event.data);
    });

  }).catch(err => {
    console.error('Failed to connect to external database:', err);
  });

  // Set up local domain
  const nukebase = addDomain({
    authPath: ["users"]
  });

  startDB(nukebase);
};

Important differences:

  • Local operations (via destructured get, set, etc.) are synchronous
  • External operations (via serversdk.js) are asynchronous and require await

This is because external connections go over the network via WebSocket, just like client connections.

Complete Server Example

Here's a minimal but complete server setup:

Complete server configuration example
module.exports = ({
  addFunction,
  addWsFunction,
  get,
  set,
  update,
  remove,
  query,
  generateRequestId,
  data,
  addDomain,
  startDB,
  onConnection,
  onClose
}) => {

// Set up a domain
const nukebase = addDomain({
  authPath: ["users"],  // Path where user authentication data is stored
  host: "127.0.0.1", // optional
  port: 3000 // optional
});

// Configure middleware for serving static files
const path = require('path');
nukebase.app.serveStatic("/*", path.join(__dirname, "../public"),
  (req, res) => { return true; }
);

// Add a database trigger for important changes
addFunction("onValue", ["orders", "$orderId"], async function(context) {
  // Only trigger if data has actually changed
  if (JSON.stringify(context.dataAfter) !== JSON.stringify(context.dataBefore)) {
    await set(["logs", generateRequestId()], {
      path: context.path,
      timestamp: Date.now(),
      oldValue: context.dataBefore,
      newValue: context.dataAfter,
      change: "Important data changed"
    });
  }
});

// Add a WebSocket function for client calculations
addWsFunction("addNumbers", function(data, admin, sessionId) {
  // Extract numbers from the request
  const { num1, num2 } = data;
  // Perform the calculation on the server
  const sum = num1 + num2;
  // Return the result to the client
  return sum;
});

// Track user connections
onConnection(function(admin, sessionId) {
  // Record when user connects
  update(["sessions", admin.uid, sessionId], {
    start: Date.now()
  });

  // Update user status
  update(["users", admin.uid], {
    online: true,
    lastSeen: Date.now()
  });
});

// Handle user disconnections
onClose(function(admin, sessionId) {
  // Record when user disconnects
  update(["sessions", admin.uid, sessionId], {
    end: Date.now()
  });

  // Update user status
  update(["users", admin.uid], {
    online: false,
    lastSeen: Date.now()
  });
});

startDB(nukebase);
console.log("🚀 NukeBase server running on http://127.0.0.1:3000");
};

Note: This example demonstrates best practices including:

  • Domain setup with authPath, host, and port configuration
  • Static file serving with serveStatic
  • Real-time database triggers
  • Custom WebSocket functions
  • Connection tracking
  • Server initialization with startDB(nukebase)

Client-Side API

NukeBase's client library provides a real-time connection to your database through WebSockets. The client handles connection management, request tracking, and event dispatching automatically.

Connection Setup

The client automatically establishes a secure WebSocket connection:

Basic connection
<script type="module">
  import createClient from './sdkmod.js';

  // ============================================
  // PATTERN 1: Full client object
  // ============================================
  const db = await createClient();

  // Use methods with db. prefix
  await db.set(['users', 'john'], { name: 'John', age: 30 });
  const user = await db.get(['users', 'john']);

  // ============================================
  // PATTERN 2: Destructured methods (recommended)
  // All examples below use this pattern
  // ============================================
  const { set, get, update, remove, query, getSub, querySub,
          getSubChanged, querySubChanged, wsFunction,
          login, logout, changePassword } = await createClient();

  console.log("Connected and ready to use NukeBase");

  // Use methods directly without prefix
  await set(['users', 'alice'], { name: 'Alice', age: 28 });
  const userData = await get(['users', 'alice']);
</script>

Important: The example above shows both patterns for demonstration. In practice, choose ONE pattern for your application. Both patterns create their own WebSocket connection, so using both would create two connections.

Key Features:

  • Promise-based initialization: Wait for connection before using the client
  • Automatic Reconnection: Reconnects every 5 seconds after disconnection
  • Subscription Restoration: Automatically restores all active subscriptions after reconnect
  • Tab Focus Recovery: Reconnects when browser tab regains focus
  • Encapsulated State: Multiple client instances can coexist independently
Connection State Indicators

The SDK provides console messages to track connection state:

  • ✅ Connected to [url] - WebSocket connection established
  • ❌ Disconnected from [url] - Connection lost
  • 🔁 Reconnecting... - Attempting to reconnect
  • 🔄 Restoring subscriptions... - Resubscribing after reconnect

Data Operations

Setting Data

The set() function creates or replaces data at a specific path:

Auto-creation: The set() function will automatically create any missing parent objects in the path. You don't need to create intermediate objects manually.

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: Like set(), the update() function will automatically create any missing parent objects in the path if they don't exist.

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);
});

Querying Data

Query allows you to search through collections and find items that match specific conditions. The query string uses JavaScript expressions where child represents each item being evaluated:

How queries work: NukeBase iterates through each child at the specified path and evaluates your condition. Items where the condition returns true are included in the results.

Querying data examples
// Basic equality check
query({
    path: ["users"],
    query: "child.age == 32"
}).then(response => {
    console.log(response.data);  // All users who are exactly 32
});

// Using comparison operators
query({
    path: ["products"],
    query: "child.price < 50"
}).then(response => {
    console.log(response.data);  // All products under $50
});

// Compound conditions with AND (&&)
query({
    path: ["products"],
    query: "child.price < 100 && child.category == 'electronics'"
}).then(response => {
    console.log(response.data);  // Affordable electronics
});

// Compound conditions with OR (||)
query({
    path: ["users"],
    query: "child.role == 'admin' || child.role == 'moderator'"
}).then(response => {
    console.log(response.data);  // All admins and moderators
});

// Text search with includes()
query({
    path: ["posts"],
    query: "child.title.includes('JavaScript')"
}).then(response => {
    console.log(response.data);  // Posts with "JavaScript" in the title
});

// Checking nested properties with childPath
query({
    path: ["users"],
    childPath: ["profile", "location"],
    query: "child == 'New York'"  // child refers to location value
}).then(response => {
    // Returns: { matt123: { profile: { location: "New York" } } }
    console.log(response.data);
});

// Combining multiple conditions
query({
    path: ["orders"],
    query: "child.status == 'pending' && child.total > 100 && child.items.length > 2"
}).then(response => {
    console.log(response.data);  // Large pending orders with multiple items
});

// Checking if a property exists
query({
    path: ["users"],
    query: "child.premiumAccount == true"
}).then(response => {
    console.log(response.data);  // All premium users
});

// Using NOT operator
query({
    path: ["tasks"],
    query: "child.completed != true"
}).then(response => {
    console.log(response.data);  // All incomplete tasks
});

// Date comparisons (assuming timestamps)
query({
    path: ["events"],
    query: "child.date > " + Date.now()
}).then(response => {
    console.log(response.data);  // Future events
});

Query Syntax Reference

Queries support standard JavaScript operators and methods:

Operator/Method Description Example
== Equal to child.status == 'active'
!= Not equal to child.deleted != true
<, >, <=, >= Comparison child.age >= 18
&& Logical AND child.active && child.verified
|| Logical OR child.role == 'admin' || child.role == 'mod'
.includes() String contains child.email.includes('@gmail.com')
.length Array/string length child.tags.length > 3

Important: The child variable represents each item at the path you're querying. For example, when querying "users", child represents each individual user object.

Using childPath to Query Nested Data

The childPath parameter allows you to query and return only specific nested portions of your data. This is especially useful for separating public and private data, improving performance, or working with complex data structures.

How childPath works:

  • Navigation: childPath navigates to a nested position in your data
  • Query context: The child variable in your query refers to the data at that nested position
  • Response structure: Results include the full path with childPath, so you know which parent item matched
childPath examples
// Data structure:
// {
//   users: {
//     matt123: {
//       public: { name: "Matt", age: 25, city: "NYC" },
//       private: { ssn: "123-45-6789", salary: 80000 }
//     },
//     john456: {
//       public: { name: "John", age: 30, city: "LA" },
//       private: { ssn: "987-65-4321", salary: 90000 }
//     }
//   }
// }

// Query WITHOUT childPath - queries full user objects
query({
    path: ["users"],
    query: "child.public.age > 21"
}).then(response => {
    console.log(response.data);
    // Returns: {
    //   matt123: { public: {...}, private: {...} },
    //   john456: { public: {...}, private: {...} }
    // }
    // You get FULL user objects including private data
});

// Query WITH childPath - queries only public portion
query({
    path: ["users"],
    childPath: ["public"],
    query: "child.age > 21"  // child now refers to the "public" object
}).then(response => {
    console.log(response.data);
    // Returns: {
    //   matt123: { public: { name: "Matt", age: 25, city: "NYC" } },
    //   john456: { public: { name: "John", age: 30, city: "LA" } }
    // }
    // You get ONLY public data, private fields are excluded
    // Response includes childPath structure so you know which users matched
});

// Multiple childPath levels
query({
    path: ["users"],
    childPath: ["public", "address"],
    query: "child.city == 'NYC'"  // child refers to the "address" object
}).then(response => {
    console.log(response.data);
    // Returns: {
    //   matt123: { public: { address: { city: "NYC", state: "NY" } } }
    // }
});

childPath Use Cases

Practical childPath use cases
// Use Case 1: Security - Exclude private data
// If users.matt123.private is blocked by read rules, childPath ensures
// you only query the accessible portion
query({
    path: ["users"],
    childPath: ["public"],
    query: "child.verified == true"
}).then(response => {
    // Only returns public data, won't fail if private is restricted
    displayPublicProfiles(response.data);
});

// Use Case 2: Performance - Return only needed data
// When clients only need profile info, not full user objects
query({
    path: ["users"],
    childPath: ["profile"],
    query: "child.country == 'USA'"
}).then(response => {
    // Smaller response payload, faster transmission
    renderUserProfiles(response.data);
});

// Use Case 3: Complex filtering on nested arrays
// Query specific nested collections
query({
    path: ["orders"],
    childPath: ["items"],
    query: "child.quantity > 5"
}).then(response => {
    // Returns: {
    //   order123: { items: { itemA: {quantity: 10, ...}, ... } }
    // }
    console.log("Orders with high-quantity items:", response.data);
});

// Use Case 4: Separating data concerns
// Different parts of your app query different data sections
query({
    path: ["products"],
    childPath: ["inventory"],
    query: "child.stock < 10"
}).then(response => {
    // Warehouse dashboard only needs inventory data
    showLowStockAlert(response.data);
});

When to use childPath:

  • You want to exclude certain fields from results (public vs private data)
  • You need to improve query performance by returning less data
  • Your read rules block certain paths, and childPath ensures you only query accessible data
  • You're querying nested collections or arrays within parent objects

Important: When using childPath, remember that child in your query refers to the data AT the childPath position, not the root object. Adjust your query conditions accordingly.

Real-time Subscriptions

Important: All subscription functions (getSub, getSubChanged, querySub, and querySubChanged) immediately send the current data when the subscription is created. This ensures your UI can display the current state right away, before any changes occur.

Basic Subscriptions

Get real-time updates when data changes. All subscription functions immediately send the current data when the subscription is created, then continue to send updates whenever the data changes:

Basic subscription examples
// Subscribe to changes on a path
const unsubscribe = getSub({
    event: "value@",
    path: ["users", "john"]
}, event => {
    // This fires immediately with current data, then on every change
    console.log("User data:", event.data);
});

// When finished listening
unsubscribe();

Query Subscriptions

Subscribe to data matching specific conditions:

Query subscription examples
// Subscribe to active users
const unsubscribe = querySub({
    event: "value@",
    path: ["users"],
    query: "child.status == 'online'"
}, event => {
    // Receives all currently online users immediately, then updates
    const onlineUsers = event.data;
    updateOnlineUsersList(onlineUsers);
});

Query Subscriptions with childPath

Just like regular queries, subscriptions can use childPath to subscribe only to specific nested portions of your data:

childPath subscription examples
// Subscribe to public profiles only (excludes private data)
const unsubscribe = querySub({
    event: "value@",
    path: ["users"],
    childPath: ["public"],
    query: "child.verified == true"
}, event => {
    // Receives only public data for verified users
    // Response: { matt123: { public: { verified: true, ... } } }
    displayVerifiedUsers(event.data);
});

// Subscribe to inventory changes for low stock items
const unsubscribe2 = querySub({
    event: "value@",
    path: ["products"],
    childPath: ["inventory"],
    query: "child.stock < 10"
}, event => {
    // Only receive inventory data, not full product details
    // Response: { productA: { inventory: { stock: 5, ... } } }
    showLowStockAlert(event.data);
});

// Use with querySubChanged for efficient updates
const unsubscribe3 = querySubChanged({
    event: "value@",
    path: ["users"],
    childPath: ["profile"],
    query: "child.country == 'USA'"
}, event => {
    // Only fires when USA profiles change
    // Only returns the profile portion that changed
    console.log("Updated USA profiles:", event.data);
});

Benefits of childPath with subscriptions:

  • Reduced bandwidth: Only transmit the data portions you need
  • Security: Never receive data that might be blocked by read rules
  • Performance: Smaller payloads mean faster real-time updates
  • Clean data: Clients receive exactly the structure they expect

Changed-Only Subscriptions

Despite the name, these subscriptions ALSO receive the initial data immediately when created, then only fire again when data actually changes:

Important for getSubChanged and querySubChanged: What you receive depends on what path you're watching:

  • If watching "users" and John updates his name, you get John's COMPLETE object (all fields)
  • If watching "users.john" and a field changes, you get ONLY the changed field (e.g., just {name: "New Name"})
  • If watching "users.john.name" and it changes, you get just the new name value
  • The deeper your watch path, the more specific the change data
Changed-only subscription examples
// getSubChanged - watching a collection
const unsubscribe = getSubChanged({
    event: "value@",
    path: ["users"]
}, event => {
    // Initial: all users
    // If John updates his email:
    // event.data = { john: { name: "John", email: "new@email.com", age: 25 } }
    // You get John's COMPLETE object
    updateChangedUsers(event.data);
});

// getSubChanged - watching a specific user
const unsubscribe2 = getSubChanged({
    event: "value@",
    path: ["users", "john"]
}, event => {
    // Initial: John's complete data
    // If John's email changes:
    // event.data = { email: "new@email.com" }
    // You get ONLY the changed field
    Object.assign(currentUser, event.data);  // Merge changes
});

// getSubChanged - watching a specific field
const unsubscribe3 = getSubChanged({
    event: "value@",
    path: ["users", "john", "status"]
}, event => {
    // Initial: "online"
    // If status changes:
    // event.data = "offline"
    // You get just the new value
    updateStatusIndicator(event.data);
});

// With query filtering - returns only the changed items
const unsubscribe4 = querySubChanged({
    event: "value@",
    path: ["users"],
    query: "child.age > 21"
}, event => {
    // If user John (age 25) updates only his name:
    // event.data = { john: { name: "John Doe", age: 25, email: "john@example.com" } }
    // You get John's COMPLETE object, not just the changed name field
    console.log("Users that changed:", event.data);
});

// Example: monitoring low stock products
const unsubscribe5 = querySubChanged({
    event: "value@",
    path: ["products"],
    query: "child.stock < 5"
}, event => {
    // If product ABC updates its price, you get:
    // { ABC: { name: "Widget", stock: 3, price: 29.99 } }
    // The complete product object for ONLY the product that changed
    Object.keys(event.data).forEach(productId => {
        updateSingleProduct(productId, event.data[productId]);
    });
});

Operation-Specific Subscriptions

Listen for specific types of operations by prefixing your path with an operation type:

Available operation types:

  • value@ - Fires on any change (set, update, or remove)
  • set@ - Fires only when data is created or completely replaced
  • update@ - Fires only when existing data is partially updated
  • remove@ - Fires only when data is deleted

Compatibility: Operation prefixes work with all subscription functions: getSub, getSubChanged, querySub, and querySubChanged.

Operation-specific subscription examples
// Listen only for updates to user data
const unsubscribe = getSub({
    event: "update@",
    path: ["users", "john"]
}, event => {
    console.log("User was updated:", event.data);
});

// Listen for new data being set
const unsubscribe2 = getSub({
    event: "set@",
    path: ["orders"]
}, event => {
    console.log("New order created:", event.data);
});

// Listen for data removal
const unsubscribe3 = getSub({
    event: "remove@",
    path: ["users"]
}, event => {
    console.log("A user was deleted:", event.path);
});

// Operation-specific with getSubChanged
const unsubscribe4 = getSubChanged({
    event: "set@",
    path: ["products"]
}, event => {
    // Only fires when NEW products are created (not updates)
    console.log("New products added:", event.data);
});

// Operation-specific with queries
const unsubscribe5 = querySub({
    event: "update@",
    path: ["users"],
    query: "child.status == 'premium'"
}, event => {
    // Only fires when premium users are UPDATED (not created or deleted)
    console.log("Premium users updated:", event.data);
});

// Combining with querySubChanged
const unsubscribe6 = querySubChanged({
    event: "remove@",
    path: ["tasks"],
    query: "child.completed == true"
}, event => {
    // Only fires when completed tasks are DELETED
    console.log("Completed tasks removed:", event.data);
});

// Default behavior without prefix (same as value@)
const unsubscribe7 = getSub({
    path: ["users", "john"]
}, event => {
    // Fires on ANY change: set, update, or remove
    // event parameter defaults to "value@" if not specified
    console.log("Something changed:", event.data);
});

Subscription Bubble-Up Behavior

Understanding how subscription changes propagate is crucial for designing efficient real-time applications. NukeBase subscriptions follow a "bubble-up" pattern:

Key Concept: Changes Bubble UP, Not DOWN

  • Bubble UP ✅: Changes at child paths trigger parent subscriptions
  • No Trickle DOWN ❌: Changes at parent paths do NOT trigger child subscriptions
Bubble-up behavior example
// Set up subscriptions at different levels
getSub({
    event: "value@",
    path: ["calls"]
}, (event) => {
    console.log("1. Calls level:", event.data);
});

getSub({
    event: "value@",
    path: ["calls", "123"]
}, (event) => {
    console.log("2. Specific call:", event.data);
});

getSub({
    event: "value@",
    path: ["calls", "123", "answer"]
}, (event) => {
    console.log("3. Answer level:", event.data);
});

// Scenario 1: Change at deep level (bubbles UP)
await set(["calls", "123", "answer"], { type: "answer", sdp: "..." });
// ✅ Fires: 1. Calls level (bubbled up)
// ✅ Fires: 2. Specific call (bubbled up)
// ✅ Fires: 3. Answer level (direct match)

// Scenario 2: Change at middle level (bubbles UP, not DOWN)
await update(["calls", "123"], { status: "active" });
// ✅ Fires: 1. Calls level (bubbled up)
// ✅ Fires: 2. Specific call (direct match)
// ❌ NOT fired: 3. Answer level (no trickle down)

// Scenario 3: Change at top level (no trickle DOWN)
await set(["calls"], { "456": { offer: {...} } });
// ✅ Fires: 1. Calls level (direct match)
// ❌ NOT fired: 2. Specific call (no trickle down)
// ❌ NOT fired: 3. Answer level (no trickle down)

Practical Implications:

  • Parent subscriptions are "catch-all": Watching users will fire for ANY change in ANY user or their properties
  • Child subscriptions are specific: Watching users.john.email only fires when that exact path or its children change
  • Performance consideration: Higher-level subscriptions fire more frequently due to bubble-up
  • Data replacement warning: If you set() at a parent level, child subscriptions may stop working as their paths no longer exist

Custom Server Functions

Execute custom logic on the server without exposing implementation details:

WebSocket function example
// Call the server function
wsFunction("addNumbers", {
  num1: 5,
  num2: 7
})
.then(response => {
    // Display the result returned by the server
    console.log(`The sum is: ${response.data}`);  // Output: The sum is: 12
});

This straightforward example shows how WebSocket functions allow you to execute code on the server and return results directly to the client, with the return value accessible via the data property of the response.

Ultra-Low Latency Performance

WebSocket functions provide the fastest possible way to communicate with your server. This makes them perfect for real-time games, live collaboration, and any application where milliseconds matter.

WebSocket functions are especially powerful when you need to:

  • Aggregate data from multiple database paths
  • Perform complex calculations server-side
  • Validate game moves or business logic
  • Return processed results without exposing raw data

Example Use Cases: Game state calculations, leaderboard generation, real-time analytics, complex permission checks, or any scenario where you need to fetch multiple database values, process them, and return a calculated result.

Authentication

NukeBase provides a built-in cookie-based authentication system. When you configure authPath: ["users"] in your domain setup, authentication endpoints are automatically available and cookies are handled seamlessly.

How it works:

  1. Configure authPath: ["users"] in your domain setup
  2. Use the built-in authentication endpoints from your client
  3. Server automatically sets HTTP cookies (uid, token)
  4. WebSocket connections automatically use these cookies
  5. User information populates the admin object for security rules

Authentication Endpoints

NukeBase automatically provides these authentication endpoints when authPath is configured:

Available Endpoints:

  • POST /auth - Login, registration, and anonymous user creation
  • POST /logout - Clear authentication cookies
  • POST /changepassword - Change user password (requires authentication)

Login and Registration

Use the /auth endpoint to login existing users or register new ones:

Login and registration
// Import and destructure the methods you need
import createClient from './sdkmod.js';
const { login } = await createClient();

// Login or register a user
const result = await login("username", "password");
if (result && result.success) {
    console.log('Authenticated as:', result.username || 'Anonymous');
}

// Create anonymous user (no username/password)
const result2 = await login();
if (result2 && result2.success) {
    console.log('Anonymous user created:', result2.uid);
}

Logout

Clear authentication cookies to log out the user:

Logout function
import createClient from './sdkmod.js';
const { logout } = await createClient();

const result = await logout();
if (result && result.success) {
    console.log('Logged out successfully');
}

Change Password

Allow authenticated users to change their password:

Change password function
const result = await changePassword("newPassword123");
if (result && result.success) {
    console.log('Password changed successfully');
} else {
    console.log('Failed to change password');
}

Using Authentication in Security Rules

Once authenticated, the admin object is available in your security rules:

Security rules with authentication
// In your rules.js
module.exports = {
  "users": {
    "$userId": {
      // Anyone can read profiles
      "read": "true",
      // Only the user themselves can edit
      "write": "admin.uid == $userId",
      
      "private": {
        // Private data only visible to the user
        "read": "admin.uid == $userId"
      }
    }
  },
  
  "adminPanel": {
    // Only users with admin role can access
    "read": "admin.role == 'admin'",
    "write": "admin.role == 'admin'"
  }
};

Security Notes:

  • Use HTTPS in production to protect cookies
  • Regularly clean up expired tokens to prevent database bloat
  • Consider implementing rate limiting on login attempts
  • The generateRequestId() function creates secure 8-character tokens

Database Structure for Authentication

The authentication system expects user data to be structured like this:

User data structure

  "users": { 
    "ML96SDE5": { // Unique user UID
      "auth": {
        "username": "matt123", // Unique username
        "password": "helloworld", // Password
        "tokens": {
          "WRL75TPY": 1748357368415, // timestamp
          "V1WM3FR2": 1748357670935
        }
      }
    }
  }

Token Management: Tokens are stored as key-value pairs where the key is the token (generated with generateRequestId() and the value is the expiration timestamp. This makes it easy to clean up expired tokens and validate sessions.

Response Format

All NukeBase operations return a standardized response object:

Standard response format
{
  // The operation performed
  action: "get", 
  
  // Data from the operation
  data: {
    "user123": { name: "John", age: 32 },
    "user456": { name: "Jane", age: 28 }
  },
  
  // For tracking the request
  requestId: "RH8HZX9P",
  
  // Success or Failed
  status: "Success"
}

When an error occurs, the response includes:

Error response format
{
  status: "Failed",
  message: "Error description here"
}

Complete Client NukeBase SDK with createClient()

Here's a complete example using the new modular SDK:

Complete client implementation
<script type="module">
  import createClient from './sdkmod.js';

  // Destructure all the methods you need
  const { set, get, update, query, wsFunction, getSub, querySub,
          getSubChanged, querySubChanged } = await createClient();

  console.log('✅ Connected to NukeBase');

  // Set data
  await set(["users", "matt"], {
    name: "Matt",
    color: "red",
    count: 0
  });

  // Get data
  const sessions = await get(["sessions"]);
  console.log('Sessions:', sessions.data);

  // Update data
  await update(["users", "matt"], {
    leadsSent: "Pending"
  });

  await update(["users", "matt", "count"], 5);

  // Query data
  const results = await query({
    path: ["sessions"],
    query: "child.count > 0"
  });
  console.log('Query results:', results.data);

  // Custom WebSocket function
  const functionResult = await wsFunction("custom1", 23);
  console.log('Function result:', functionResult);

  // Subscribe to changes
  const unsubscribe1 = getSub({
    event: "value@",
    path: ["sessions"]
  }, data => {
    console.log('Sessions updated:', data);
  });

  // Query subscription
  const unsubscribe2 = querySub({
    event: "value@",
    path: ["sessions"],
    query: "child.count == 4"
  }, data => {
    console.log('Matching sessions:', data);
  });

  // Changed-only subscription
  const unsubscribe3 = getSubChanged({
    event: "value@",
    path: ["sessions"]
  }, data => {
    console.log('Changed sessions:', data);
  });

  // Query changed subscription
  const unsubscribe4 = querySubChanged({
    event: "value@",
    path: ["sessions"],
    query: "child.count != 4"
  }, data => {
    console.log('Changed query results:', data);
  });

  // Later, to unsubscribe:
  // unsubscribe1();
  // unsubscribe2();
  // unsubscribe3();
  // unsubscribe4();

</script>