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 src="NukeBaseSDK.js"></script>

// Connect to the server (returns a Promise)
connectWebSocket().then(() => {
    console.log("Connected and ready to use NukeBase");
    // Start using NukeBase methods here
});

The connection is automatically maintained:

  • Reconnects when browser tabs regain focus
  • Uses WSS for HTTPS sites, WS for HTTP sites
  • Dispatches events for server notifications

Data Operations

Setting Data

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

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

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:

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

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

Find data that matches specific conditions:

Querying data examples
// Basic equality query
query("users", "child.age == 32").then(response => {
    console.log(response.data);  // All users age 32
});

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

// Text search with includes()
query("users", "child.bio.includes('developer')").then(response => {
    console.log(response.data);  // Users with "developer" in their bio
});

Real-time Subscriptions

Basic Subscriptions

Get real-time updates when data changes:

Basic subscription examples
// Subscribe to changes on a path
const unsubscribe = getSub("value@users.john", event => {
    console.log("User data changed:", event.data);
});

// When finished listening
unsubscribe().then(() => {
    console.log("Unsubscribed successfully");
});

Query Subscriptions

Subscribe to data matching specific conditions:

Query subscription examples
// Subscribe to active users
const unsubscribe = querySub("value@users", "child.status == 'online'", event => {
    const onlineUsers = event.data;
    updateOnlineUsersList(onlineUsers);
});

Changed-Only Subscriptions

Only receive updates when data changes, not on the initial subscription:

Changed-only subscription examples
// Only get notified of changes
const unsubscribe = getSubChanged("value@activeOrders", event => {
    notifyNewOrder(event.data);
});

// With query filtering
const unsubscribe = querySubChanged("value@products", 
    "child.stock < 5", event => {
        alertLowStock(event.data);
    });

Operation-Specific Subscriptions

Listen for specific types of operations:

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

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

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

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.

File Operations

Upload files to your server:

File upload example
// Upload from a file input
const fileInput = document.getElementById('profilePicture');
const file = fileInput.files[0];

setFile("users.john.profile.jpg", file).then(response => {
    showSuccess("Profile picture uploaded!");
    updateProfileImage(response.data.url);
});

The file upload process:

  • Reads the file as an ArrayBuffer
  • Adds path and filename metadata
  • Sends the binary data over WebSocket
  • Returns the server response (typically with a URL to access the file)

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"
}

Server-Side API

NukeBase is designed for simplicity. To get up and running, you only need:

  • server/database.exe: The core database engine
  • server/data.json: Your database file
  • server/rules.js: JSON security rules (coming soon)
  • server/app.js: Your configuration file
  • public/(index.html, ...): For serving HTML files

That's it! No complex configuration or additional services required.

Setup and Initialization

NukeBase server runs as a Node.js application with a simple setup process. The key components are:

  • database.exe: Core database engine that must be in your project's root directory
  • app.js: Configuration file that sets up domains, middleware, and event handlers

Basic Server Structure

The server configuration is defined using a module export function that receives core dependencies:

Server configuration structure
module.exports = (fs, express, addFunction, functionMatch, addWsFunction, get, set, update, remove, query, generateRequestId, data, addDomain, startDB, onConnection, onClose) => {
 
  // Server configuration goes here

}

Domain Configuration

NukeBase supports multiple domains with custom SSL certificates using a single server instance:

Multiple domain configuration
const productionDomain = addDomain({
  domain: "example.com",
  https: {
    key: '/etc/letsencrypt/live/example.com/privkey.pem',
    cert: '/etc/letsencrypt/live/example.com/fullchain.pem'
  }
});

// You can add as many domains as needed
const anotherDomain = addDomain({
  domain: "another-domain.com",
  https: {
    key: '/etc/letsencrypt/live/another-domain.com/privkey.pem',
    cert: '/etc/letsencrypt/live/another-domain.com/fullchain.pem'
  }
});

For development environments, you can use empty strings for the SSL credentials:

Development domain configuration
const devDomain = addDomain({
  domain: "exampledomain.com",
  https: {
    key: '',
    cert: ''
  }
});

Express Middleware

Each domain has its own Express app instance that you can configure:

Express middleware configuration
// Serve static files
myDomain.app.use(express.static(path.join(process.cwd(), `../public`)));

// Serve files with long cache time
myDomain.app.use('/files', express.static(path.join(process.cwd(), `../files`), { 
    maxAge: 60 * 60 * 24 * 1000 * 365  // 1 year in milliseconds
}));

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, pathPattern, callbackFunction)

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 a path string 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 that was changed
  • 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
    const orderId = context.path.split('.')[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("addNumbers", function (data, admin, sessionId, userId) {
    // 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;
});

WebSocket functions receive:

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

Connection Events

Track client connections and disconnections:

Connection event handlers
// When a client connects
onConnection(function (userId, sessionId) {
    // Record session start time
    update(`sessions.${userId}.${sessionId}`, { 
        start: Date.now()
    });
});

// When a client disconnects
onClose(function (userId, sessionId) {
    // Record session end time
    update(`sessions.${userId}.${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
// Local IP Address mode (no SSL) 
startDB({ local: true, http: "127.0.0.1", port: 3000 });

// Server IP Address mode (no SSL) 
startDB({ local: true, http: "126.23.45.1", port: 3000 });

// Production mode (with SSL)
startDB({ local: false, http: "0.0.0.0", port: 3000 });

Configuration options:

  • local: Boolean - use IP address provided
    • When true: Use HTTP without SSL, binding to the specified IP address
    • When false: Use HTTPS with the configured SSL certificates
  • http: String - the IP address to bind to
    • Use "127.0.0.1" to accept connections only from the local machine
    • 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 - the starting port address (Increments on multiple domains)
    • For a single domain: The server will listen on this port
    • For multiple domains: Each domain gets its own port, starting with this number and incrementing (e.g., 3000, 3001, 3002...)
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.

Complete Server Example

Here's a minimal but complete server setup:

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

// Set up a domain
const myApp = addDomain({
domain: "myapp.com",
  https: {
  key: '/etc/letsencrypt/live/myapp.com/privkey.pem',
  cert: '/etc/letsencrypt/live/myapp.com/fullchain.pem'
}
});
// Configure middleware for serving static files
const path = require('path');
myApp.app.use(express.static(path.join(process.cwd(), 'public')));

// 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, userId) {
  // 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(userId, sessionId) {
  // Record when user connects
  update(sessions.${userId}.${sessionId}, {
    start: Date.now(),
    userAgent: data.request?.headers?.["user-agent"] || "Unknown"
  });

  // Update user status
  update(`users.${userId}`, { 
    online: true,
    lastSeen: Date.now()
  });
});

// Handle user disconnections
onClose(function(userId, sessionId) {
  // Record when user disconnects
  update(sessions.${userId}.${sessionId}, {
    end: Date.now(),
    duration: function(current) {
    return current.end - current.start;
  }
  });

  // Update user status
  update(`users.${userId}`, { 
    online: false,
    lastSeen: Date.now()
  });
});

// Start the server (local mode on port 3000)
startDB({ local: true, http: "127.0.0.1", port: 3000 });
console.log(🚀 NukeBase server running on Node.js "http://127.0.0.1:3000");
};

Note: This example demonstrates best practices including:

  • Domain setup with SSL configuration
  • Static file serving
  • Real-time database triggers
  • Custom WebSocket functions
  • Connection tracking
  • Server initialization with proper port configuration

Complete Client Example

Here's a minimal but complete client setup:

Complete client implementation
const urlParam = new URLSearchParams(location.search);

function generateRequestId() {
    const chars = '0123456789ABCDEFGHJKLMNPQRSTUVWXYZ';
    let result = '';
    for (let i = 0; i < 8; i++) {
        result += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return result;
}

function sendRequest(action, path, socket, data) {
    return new Promise((resolve, reject) => {
        admin = urlParam.get("admin") || "";
        const requestId = generateRequestId();
        pendingRequests[requestId] = { resolve, reject };
        socket.send(JSON.stringify({ action, path, requestId, data, admin }));
    });
}


function sendSubscribe(action, path, socket, data) {
    admin = urlParam.get("admin") || "";
    socket.send(JSON.stringify({ action, path, data, admin }));
}

const sub = {
    events: {},
    on(action, path, callback, data) {
        this.events[action + path] = callback;
        sendSubscribe(action, path, socket, data);
    },
    emit(action, path, data) {
        if (this.events[action + path]) {
            this.events[action + path](data);
        }
    },
    off(action, path, callback, data) {
        sendSubscribe(action + 'Stop', path, socket, data)
        delete this.events[action + path];
    }
};

var pendingRequests = {};
var socket;

function connectWebSocket() {
    return new Promise((resolve, reject) => {
        const wsProtocol = window.location.protocol === "https:" ? "wss://" : "ws://";
        socket = new WebSocket(`${wsProtocol}${window.location.host}`);

        socket.addEventListener('message', function (event) {
            try {
                const response = JSON.parse(event.data);
                if (response.requestId) {
                    pendingRequests[response.requestId].resolve(response);
                    delete pendingRequests[response.requestId];
                } else {
                    sub.emit(response.action, response.path, response);
                }
            } catch (error) {
                console.error('Error parsing message:', error);
            }
        });

        socket.addEventListener('open', async () => {
            console.log('WebSocket connection opened');
            resolve();
        });

        socket.addEventListener('close', () => {
            console.log('WebSocket connection closed');
        });

        socket.addEventListener('error', (error) => {
            console.error('WebSocket error:', error);
        });
    });
}

//Reconnect WebSocket when the browser window gains focus
document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') {
        setTimeout(function () {
            if (socket.readyState === WebSocket.CLOSED) {
                connectWebSocket();
            }
        }, 500);
    }
});

function set(path, data) {
    return sendRequest("set", path, socket, data).then(data => {
        return data;
    })
}

function get(path) {
    return sendRequest('get', path, socket).then(data => {
        return data;
    })
}

function update(path, data) {
    return sendRequest("update", path, socket, data).then(data => {
        return data;
    })
}

function remove(path) {
    return sendRequest("remove", path, socket).then(data => {
        return data;
    })
}

function query(path, query) {
    return sendRequest('query', path, socket, query).then(data => {
        return data;
    })
}

function wsFunction(name, data) {
    return sendRequest(name, null, socket, data).then(data => {
        return data;
    })
}

function getSub(path, handler) {
    sub.on("getSub", path, handler);
    return function () {
        sub.off('getSub', path);
    };
}

function querySub(path, query, handler) {
    sub.on("querySub", path, handler, query);
    return function () {
        sub.off("querySub", path, handler, query);
    }
}

function getSubChanged(path, handler) {
    sub.on("getSubChanged", path, handler);
    return function () {
        sub.off('getSubChanged', path);
    };
}

function querySubChanged(path, query, handler) {
    sub.on("querySubChanged", path, handler, query);
    return function () {
        sub.off("querySubChanged", path, handler, query);
    }
}

function setFile(path, file) {
    return new Promise((resolve, reject) => {
        const requestId = generateRequestId();
        pendingRequests[requestId] = { resolve, reject };
        const separator = "--myUniqueSeparator--";
        const reader = new FileReader();
        reader.onload = function (e) {
            const arrayBuffer = e.target.result;
            const textEncoder = new TextEncoder();

            const encodedPath = textEncoder.encode(path + separator);
            const encodedFileName = textEncoder.encode(file.name + separator);
            const encodedRequestId = textEncoder.encode(requestId + separator);
            const combinedArrayBuffer = new Uint8Array(
                encodedPath.length + encodedFileName.length + encodedRequestId.length + arrayBuffer.byteLength
            );

            combinedArrayBuffer.set(encodedPath, 0);
            combinedArrayBuffer.set(encodedFileName, encodedPath.length);
            combinedArrayBuffer.set(encodedRequestId, encodedPath.length + encodedFileName.length);
            combinedArrayBuffer.set(
                new Uint8Array(arrayBuffer), encodedPath.length + encodedFileName.length + encodedRequestId.length
            );
            socket.send(combinedArrayBuffer.buffer);
        };
        reader.readAsArrayBuffer(file);
    });
}

connectWebSocket().then(() => {
    // Example usage
    set("users.matt.color", "red").then(data => {
        console.log(data);
    })

    get("sessions").then(data => {
        console.log(data);
    })

    update("users.matt", { leadsSent: "Pending" }).then(data => {
        console.log(data);
    })

    update("users.matt.count", 5).then(data => {
        console.log(data);
    })

    remove("users.matt").then(data => {
        console.log(data);
    })

    query("sessions", `child.count > 0`).then(data => {
        console.log(data);
    })

    wsFunction("custom1", 23).then(data => {
        console.log(data);
    })

    //all subscriptions use value, get, update, remove. This subscribes to that action

    getSub("value@sessions", data => {
        console.log(data);
    });

    querySub("value@sessions", "child.count == 4", data => {
        console.log(data);
    });

    getSubChanged("value@sessions", data => {
        console.log(data);
    });

    querySubChanged("value@sessions", "child.count != 4", data => {
        console.log(data);
    });

    setFile(undefined, blob).then(data => {
        console.log(data);
    })
});