Skip to main content

Views

Views are read-only functions that compute and return results from your tables. Unlike reducers, views do not modify database state - they only query and return data. Views are useful for computing derived data, aggregations, or joining multiple tables before sending results to clients.

Why Use Views?

Views provide several benefits:

  • Performance: Views compute results server-side, reducing the amount of data sent to clients
  • Encapsulation: Views can hide complex queries behind simple interfaces
  • Consistency: Views ensure clients receive consistently formatted data
  • Real-time updates: Like tables, views can be subscribed to and automatically update when underlying data changes

Defining Views

Views must be declared as public with an explicit name, and they accept only a context parameter - no user-defined arguments beyond the context type.

Use the spacetimedb.view or spacetimedb.anonymousView function:

import { schema, table, t } from 'spacetimedb/server';

const players = table(
  { name: 'players', public: true },
  {
    id: t.u64().primaryKey().autoInc(),
    identity: t.identity().unique(),
    name: t.string(),
  }
);

const playerLevels = table(
  { name: 'player_levels', public: true },
  {
    player_id: t.u64().unique(),
    level: t.u64().index('btree'),
  }
);

const spacetimedb = schema(players, playerLevels);

// At-most-one row: return Option<row> via t.option(...)
// Your function may return the row or null
spacetimedb.view(
    { name: 'my_player', public: true },
    t.option(players.rowType),
    (ctx) => {
        const row = ctx.db.players.identity.find(ctx.sender);
        return row ?? undefined;
    }
);

// Define a custom row type for the joined result
const playerAndLevelRow = t.row('PlayerAndLevel', {
    id: t.u64(),
    name: t.string(),
    level: t.u64(),
});

// Multiple rows: return an array of rows via t.array(...)
spacetimedb.anonymousView(
    { name: 'players_for_level', public: true },
    t.array(playerAndLevelRow),
    (ctx) => {
        const out: Array<{ id: bigint; name: string; level: bigint }> = [];
        for (const playerLevel of ctx.db.playerLevels.level.filter(2n)) {
            const p = ctx.db.players.id.find(playerLevel.player_id);
            if (p) out.push({ id: p.id, name: p.name, level: playerLevel.level });
        }
        return out;
    }
);

The handler signature is (ctx) => rows, where rows must be either an array or option of product values.

ViewContext and AnonymousViewContext

Views use one of two context types:

  • ViewContext: Provides access to the caller's Identity through ctx.sender. Use this when the view depends on who is querying it.
  • AnonymousViewContext: Does not provide caller information. Use this when the view produces the same results regardless of who queries it.

Both contexts provide read-only access to tables and indexes through ctx.db.

// View that depends on caller identity
spacetimedb.view(
  { name: 'my_player', public: true },
  t.option(players.rowType),
  (ctx) => {
    const row = ctx.db.players.identity.find(ctx.sender);
    return row ?? undefined;
  }
);

// View that is the same for all callers
spacetimedb.anonymousView(
  { name: 'players_for_level', public: true },
  t.array(playerAndLevelRow),
  (ctx) => {
    const out: Array<{ id: bigint; name: string; level: bigint }> = [];
    for (const playerLevel of ctx.db.playerLevels.level.filter(2n)) {
      const p = ctx.db.players.id.find(playerLevel.player_id);
      if (p) out.push({ id: p.id, name: p.name, level: playerLevel.level });
    }
    return out;
  }
);

Querying Views

Views can be queried and subscribed to just like normal tables using SQL:

SELECT * FROM my_player;
SELECT * FROM players_for_level;

When subscribed to, views automatically update when their underlying tables change, providing real-time updates to clients.

Performance Considerations

Views compute results on the server side, which can improve performance by:

  • Reducing network traffic by filtering/aggregating before sending data
  • Avoiding redundant computation on multiple clients
  • Leveraging server-side indexes and query optimization

However, keep in mind that:

  • Complex views with multiple joins or aggregations can be expensive to compute
  • Views are recomputed whenever their underlying tables change
  • Subscriptions to views will receive updates even if the final result doesn't change

Next Steps