Skip to main content
Version: 2.0.0-rc1

Subscriptions

Subscriptions replicate database rows to your client in real-time. When you subscribe to a query, SpacetimeDB sends you the matching rows immediately and then pushes updates whenever those rows change.

Quick Start

Here's a complete example showing how to subscribe to data and react to changes:

import { DbConnection, tables } from './module_bindings';

// Connect to the database
const conn = DbConnection.builder()
  .withUri('wss://maincloud.spacetimedb.com')
  .withDatabaseName('my_module')
  .onConnect((ctx) => {
    // Subscribe to users and messages using query builders
    ctx.subscriptionBuilder()
      .onApplied(() => {
        console.log('Subscription ready!');
        // Initial data is now in the client cache
        for (const user of ctx.db.user.iter()) {
          console.log(`User: ${user.name}`);
        }
      })
      .subscribe([tables.user, tables.message]);
  })
  .build();

// React to new rows being inserted
conn.db.user.onInsert((ctx, user) => {
  console.log(`New user joined: ${user.name}`);
});

// React to rows being deleted
conn.db.user.onDelete((ctx, user) => {
  console.log(`User left: ${user.name}`);
});

// React to rows being updated
conn.db.user.onUpdate((ctx, oldUser, newUser) => {
  console.log(`${oldUser.name} changed name to ${newUser.name}`);
});
Typed Query Builders

Type-safe query builders are available in TypeScript, C#, and Rust and are the recommended default. They provide auto-completion and compile-time type checking. For complete API details, see TypeScript, C#, and Rust references.

How Subscriptions Work

  1. Subscribe: Subscribe with queries to the data you need
  2. Receive initial data: SpacetimeDB sends all matching rows immediately
  3. Receive updates: When subscribed rows change, you get real-time updates
  4. React to changes: Use row callbacks (onInsert, onDelete, onUpdate) to handle changes

The client maintains a local cache of subscribed data. Reading from the cache is instant since it's local memory.

For advanced raw SQL subscription syntax, see the SQL docs.

Common API Concepts

This page focuses on subscription behavior and usage patterns that apply across SDKs. For exact method signatures and SDK-specific overloads, use the language references.

Builder and Lifecycle Callbacks

All SDKs expose a builder API for creating subscriptions:

  • Register an applied callback: runs once initial matching rows are present in the local cache.
  • Register an error callback: runs if subscription registration fails or a subscription later terminates with an error.
  • Subscribe with one or more queries.

Query Forms

All SDKs support subscriptions. TypeScript, C#, and Rust support query builders (recommended), while Unreal uses query strings:

SDKTyped Query Builder SupportEntry Point
TypeScriptYestables.<table>.where(...) passed to subscribe(...)
C#YesSubscriptionBuilder.AddQuery(...).Subscribe()
RustYessubscription_builder().add_query(...).subscribe()
UnrealNoQuery strings passed to Subscribe(...)

Subscription Handles

Subscribing returns a handle that manages an individual subscription lifecycle.

  • isActive / IsActive / is_active indicates that matching rows are currently active in the cache.
  • isEnded / IsEnded / is_ended indicates a subscription has ended, either from unsubscribe or error.
  • Unsubscribe is asynchronous: rows are removed after the unsubscribe operation is applied.
  • subscribeToAllTables / SubscribeToAllTables / subscribe_to_all_tables is a convenience entry point intended for simple clients and is not individually cancelable.

API References

Best Practices for Optimizing Server Compute and Reducing Serialization Overhead

1. Writing Efficient Subscription Queries

Use the typed query builder to express precise filters and keep subscriptions small. If you use raw SQL subscriptions, see SQL Best Practices.

2. Group Subscriptions with the Same Lifetime Together

Subscriptions with the same lifetime should be grouped together.

For example, you may have certain data that is required for the lifetime of your application, but you may have other data that is only sometimes required by your application.

By managing these sets as two independent subscriptions, your application can subscribe and unsubscribe from the latter, without needlessly unsubscribing and resubscribing to the former.

This will improve throughput by reducing the amount of data transferred from the database to your application.

Example

import { DbConnection, tables } from './module_bindings';

const conn = DbConnection.builder()
  .withUri('https://maincloud.spacetimedb.com')
  .withDatabaseName('my_module')
  .build();

// Never need to unsubscribe from global subscriptions
const globalSubscriptions = conn
  .subscriptionBuilder()
  .subscribe([
    // Global messages the client should always display
    tables.announcements,
    // A description of rewards for in-game achievements
    tables.badges,
  ]);

// May unsubscribe to shop_items as player advances
const shopSubscription = conn
  .subscriptionBuilder()
  .subscribe([
    tables.shopItems.where(r => r.requiredLevel.lte(5)),
  ]);

3. Subscribe Before Unsubscribing

If you want to update or modify a subscription by dropping it and subscribing to a new set, you should subscribe to the new set before unsubscribing from the old one.

This is because SpacetimeDB subscriptions are zero-copy. Subscribing to the same query more than once doesn't incur additional processing or serialization overhead. Likewise, if a query is subscribed to more than once, unsubscribing from it does not result in any server processing or data serializtion.

Example

import { DbConnection, tables } from './module_bindings';

const conn = DbConnection.builder()
  .withUri('https://maincloud.spacetimedb.com')
  .withDatabaseName('my_module')
  .build();

// Initial subscription: player at level 5.
const shopSubscription = conn
  .subscriptionBuilder()
  .subscribe([
    // For displaying the price of shop items in the player's currency of choice
    tables.exchangeRates,
    tables.shopItems.where(r => r.requiredLevel.lte(5)),
  ]);

// New subscription: player now at level 6, which overlaps with the previous query.
const newShopSubscription = conn
  .subscriptionBuilder()
  .subscribe([
    // For displaying the price of shop items in the player's currency of choice
    tables.exchangeRates,
    tables.shopItems.where(r => r.requiredLevel.lte(6)),
  ]);

// Unsubscribe from the old subscription once the new one is in place.
if (shopSubscription.isActive()) {
  shopSubscription.unsubscribe();
}

4. Avoid Overlapping Queries

This refers to distinct queries that return intersecting data sets, which can result in the server processing and serializing the same row multiple times. While SpacetimeDB can manage this redundancy, it may lead to unnecessary inefficiencies.

Consider the following two query builder subscriptions:

tables.user
tables.user.where(r => r.id.eq(5))

If User.id is a unique or primary key column, the cost of subscribing to both queries is minimal. This is because the server will use an index when processing the 2nd query, and it will only serialize a single row for the 2nd query.

In contrast, consider these two query builder subscriptions:

tables.user
tables.user.where(r => r.id.ne(5))

The server must now process each row of the User table twice, since the 2nd query cannot be processed using an index. It must also serialize all but one row of the User table twice, due to the significant overlap between the two queries.

By following these best practices, you can optimize your data replication strategy and ensure your application remains efficient and responsive.