Skip to main content

Subscription Reference

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, User, Message } from './module_bindings';

// Connect to the database
const conn = DbConnection.builder()
  .withUri('wss://maincloud.spacetimedb.com')
  .withModuleName('my_module')
  .onConnect((ctx) => {
    // Subscribe to users and messages
    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(['SELECT * FROM user', 'SELECT * FROM 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}`);
});

How Subscriptions Work

  1. Subscribe: Register SQL queries describing 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 more information on subscription SQL syntax see the SQL docs.

API Reference

This section describes the two main interfaces: SubscriptionBuilder and SubscriptionHandle.

SubscriptionBuilder

interface SubscriptionBuilder {
  // Register a callback to run when the subscription is applied.
  onApplied(callback: (ctx: SubscriptionEventContext) => void): SubscriptionBuilder;

  // Register a callback to run when the subscription fails.
  // This callback may run when attempting to apply the subscription,
  // or later during the subscription's lifetime if the module's interface changes.
  onError(callback: (ctx: ErrorContext, error: Error) => void): SubscriptionBuilder;

  // Subscribe to the following SQL queries.
  // Returns immediately; callbacks are invoked when data arrives from the server.
  subscribe(querySqls: string[]): SubscriptionHandle;

  // Subscribe to all rows from all tables.
  // Intended for applications where memory and bandwidth are not concerns.
  subscribeToAllTables(): void;
}

A SubscriptionBuilder provides an interface for registering subscription queries with a database. It allows you to register callbacks that run when the subscription is successfully applied or when an error occurs. Once applied, a client will start receiving row updates to its client cache. A client can react to these updates by registering row callbacks for the appropriate table.

Example Usage

// Establish a database connection
import { DbConnection } from './module_bindings';

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

// Register a subscription with the database
const userSubscription = conn
  .subscriptionBuilder()
  .onApplied((ctx) => { /* handle applied state */ })
  .onError((ctx, error) => { /* handle error */ })
  .subscribe(['SELECT * FROM user', 'SELECT * FROM message']);

SubscriptionHandle

interface SubscriptionHandle {
  // Whether the subscription has ended (unsubscribed or terminated due to error).
  isEnded(): boolean;

  // Whether the subscription is currently active.
  isActive(): boolean;

  // Unsubscribe from the query controlled by this handle.
  // Throws if called more than once.
  unsubscribe(): void;

  // Unsubscribe and call onEnded when rows are removed from the client cache.
  unsubscribeThen(onEnded?: (ctx: SubscriptionEventContext) => void): void;
}

When you register a subscription, you receive a SubscriptionHandle. A SubscriptionHandle manages the lifecycle of each subscription you register. In particular, it provides methods to check the status of the subscription and to unsubscribe if necessary. Because each subscription has its own independently managed lifetime, clients can dynamically subscribe to different subsets of the database as their application requires.

Example Usage

Consider a game client that displays shop items and discounts based on a player's level. You subscribe to shop_items and shop_discounts when a player is at level 5:

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

const shopItemsSubscription = conn
  .subscriptionBuilder()
  .onApplied((ctx) => { /* handle applied state */ })
  .onError((ctx, error) => { /* handle error */ })
  .subscribe([
    'SELECT * FROM shop_items WHERE required_level <= 5',
    'SELECT * FROM shop_discounts WHERE required_level <= 5',
  ]);

Later, when the player reaches level 6 and new items become available, you can subscribe to the new queries and unsubscribe from the old ones:

const newShopItemsSubscription = conn
  .subscriptionBuilder()
  .onApplied((ctx) => { /* handle applied state */ })
  .onError((ctx, error) => { /* handle error */ })
  .subscribe([
    'SELECT * FROM shop_items WHERE required_level <= 6',
    'SELECT * FROM shop_discounts WHERE required_level <= 6',
  ]);

if (shopItemsSubscription.isActive()) {
  shopItemsSubscription.unsubscribe();
}

All other subscriptions continue to remain in effect.

Best Practices for Optimizing Server Compute and Reducing Serialization Overhead

1. Writing Efficient SQL Queries

For writing efficient SQL queries, see our SQL Best Practices Guide.

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

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

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

// May unsubscribe to shop_items as player advances
const shopSubscription = conn
  .subscriptionBuilder()
  .subscribe([
    'SELECT * FROM shop_items WHERE required_level <= 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

const conn = DbConnection.builder()
  .withUri('https://maincloud.spacetimedb.com')
  .withModuleName('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
    'SELECT * FROM exchange_rates',
    'SELECT * FROM shop_items WHERE required_level <= 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
    'SELECT * FROM exchange_rates',
    'SELECT * FROM shop_items WHERE required_level <= 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 queries:

SELECT * FROM User
SELECT * FROM User WHERE id = 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 queries:

SELECT * FROM User
SELECT * FROM User WHERE id != 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.