Summary
Introduce a new view context type, ScopedViewContext<K>, that allows view results to be scoped and shared by an arbitrary key rather than being either global (AnonymousViewContext) or per-caller (ViewContext).
Motivation
AnonymousViewContext offers a significant performance advantage — the database materializes the view once and shares it across all subscribers. The only alternative today is ViewContext, which is tied to the individual caller and requires a separate computation per subscriber.
There is a large gap between these two extremes. Many real-world use cases involve groups of users who should all see the same view results. Today, module authors must work around this by either:
- Manually partitioning data into hardcoded anonymous views (e.g., one view per known region) (not acceptable for sensitive data)
- Accepting the per-user cost of
ViewContext even when many users would share identical results
A ScopedViewContext closes this gap by letting SpacetimeDB automatically memoize and share view computations across all callers that resolve to the same scope key.
Proposed API
The API uses two callbacks:
- Key resolver — runs per-caller to determine which scope group they belong to (lightweight lookup)
- View body — runs once per unique key, shared across all callers with that key
TypeScript
// Scope key as a single value
export const team_chat = spacetimedb.scopedView(
{ name: 'team_chat', public: true },
t.array(chatMessages.rowType),
// 1. Resolve the scope key for this caller
(ctx) => {
const player = ctx.db.players.identity.find(ctx.sender());
return player.teamId; // u64 key — all players on the same team share this view
},
// 2. View body — executed once per unique teamId
(ctx, teamId) => {
return Array.from(ctx.db.chatMessages.teamId.filter(teamId));
}
);
// Scope key as a composite struct/tuple
export const regional_entities = spacetimedb.scopedView(
{ name: 'regional_entities', public: true },
t.array(entity.rowType),
(ctx) => {
const player = ctx.db.players.identity.find(ctx.sender());
const chunk = ctx.db.playerChunks.playerId.find(player.id);
return { chunkX: chunk.chunkX, chunkY: chunk.chunkY }; // composite key
},
(ctx, key) => {
return Array.from(ctx.db.entity.chunkX.filter(key.chunkX))
.filter(e => e.chunkY === key.chunkY);
}
);
Rust
// Scope key as a single value
#[view(accessor = team_chat, public)]
fn team_chat(ctx: &ScopedViewContext<u64>) -> Vec<ChatMessage> {
ctx.db.chat_messages().team_id().filter(ctx.scope()).collect()
}
#[view_scope(team_chat)]
fn team_chat_scope(ctx: &ViewContext) -> u64 {
let player = ctx.db.player().identity().find(&ctx.sender()).unwrap();
player.team_id
}
// Scope key as a composite struct
#[derive(ScopeKey)]
struct ChunkCoord {
chunk_x: i32,
chunk_y: i32,
}
#[view(accessor = regional_entities, public)]
fn regional_entities(ctx: &ScopedViewContext<ChunkCoord>) -> Vec<Entity> {
let key = ctx.scope();
ctx.db.entity().chunk_x().filter(&key.chunk_x)
.filter(|e| e.chunk_y == key.chunk_y)
.collect()
}
#[view_scope(regional_entities)]
fn regional_entities_scope(ctx: &ViewContext) -> ChunkCoord {
let player = ctx.db.player().identity().find(&ctx.sender()).unwrap();
let chunk = ctx.db.player_chunk().player_id().find(&player.id).unwrap();
ChunkCoord { chunk_x: chunk.chunk_x, chunk_y: chunk.chunk_y }
}
Valid Scope Key Types
Scope keys must be hashable and comparable. Valid types include:
- Primitives:
u64, string, Identity
- Composite keys: Structs or tuples composed of the above types (e.g.,
{ chunkX: i32, chunkY: i32 })
Example Use Cases
| Use Case |
Scope Key |
Why |
| Team chat |
teamId: u64 |
All players on the same team see the same messages |
| Match leaderboard |
matchId: u64 |
All players in a match share the same leaderboard |
| Regional map data |
{ chunkX, chunkY } |
Players in the same chunk share entity data |
| Guild inventory |
guildId: u64 |
All guild members see the same shared inventory |
| Instance/room state |
instanceId: u64 |
Players in the same dungeon instance share state |
| Party buffs |
partyId: u64 |
Active buffs shared across party members |
Performance Characteristics
| Context Type |
Computation Cost |
Materialized Views |
AnonymousViewContext |
Once total |
1 |
ScopedViewContext<K> |
Once per unique key |
1 per unique key |
ViewContext |
Once per subscriber |
1 per subscriber |
For a game with 1,000 players across 50 teams, team-scoped views would require ~50 materializations instead of 1,000 — a 20x reduction compared to ViewContext.
Summary
Introduce a new view context type,
ScopedViewContext<K>, that allows view results to be scoped and shared by an arbitrary key rather than being either global (AnonymousViewContext) or per-caller (ViewContext).Motivation
AnonymousViewContextoffers a significant performance advantage — the database materializes the view once and shares it across all subscribers. The only alternative today isViewContext, which is tied to the individual caller and requires a separate computation per subscriber.There is a large gap between these two extremes. Many real-world use cases involve groups of users who should all see the same view results. Today, module authors must work around this by either:
ViewContexteven when many users would share identical resultsA
ScopedViewContextcloses this gap by letting SpacetimeDB automatically memoize and share view computations across all callers that resolve to the same scope key.Proposed API
The API uses two callbacks:
TypeScript
Rust
Valid Scope Key Types
Scope keys must be hashable and comparable. Valid types include:
u64,string,Identity{ chunkX: i32, chunkY: i32 })Example Use Cases
teamId: u64matchId: u64{ chunkX, chunkY }guildId: u64instanceId: u64partyId: u64Performance Characteristics
AnonymousViewContextScopedViewContext<K>ViewContextFor a game with 1,000 players across 50 teams, team-scoped views would require ~50 materializations instead of 1,000 — a 20x reduction compared to
ViewContext.