Real-time Subscriptions
Looking for get/set/update/remove/query? Those work the same on client and server — see CRUD Operations and Query Operations. The subscriptions below build on those primitives to deliver live updates.
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:
// 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:
// 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:
// Subscribe to public profiles only (excludes private data)
const unsubscribe = querySub({
event: "value@",
path: ["users"],
childPath: ["public"],
query: "child.verified == true"
}, event => {
// Receives the public subtree wrapped under its childPath key,
// mirroring the DB shape. "private" is never traversed or sent.
// Response: { matt123: { public: { verified: true, name: "Matt", ... } } }
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 the inventory subtree is fetched and pushed; product details
// outside "inventory" never enter the payload.
// 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
// 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 replacedupdate@- Fires only when existing data is partially updatedremove@- Fires only when data is deleted
Compatibility: Operation prefixes work with all subscription functions:
getSub, getSubChanged, querySub, and querySubChanged.
// 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
// 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
userswill fire for ANY change in ANY user or their properties - Child subscriptions are specific: Watching
users.john.emailonly 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