Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1591763
Fix getting started docs links
clockwork-labs-bot May 5, 2026
d01d936
Document server-issued token reconnect behavior
clockwork-labs-bot May 21, 2026
29444aa
Clarify Unity WebGL token guard
clockwork-labs-bot May 21, 2026
cf4cc3c
Explain Unity WebGL preprocessor guard
clockwork-labs-bot May 21, 2026
ce19fdc
docs: sync C++ skills coverage
May 21, 2026
591ede5
docs: clarify Unreal connection ticking
May 22, 2026
f515bac
docs: use csharp generate language
May 24, 2026
444ca40
docs: clarify Unreal client ticking
May 25, 2026
c2b081a
docs: update C# connection signatures
May 26, 2026
2777b47
docs: sync C++ language coverage
May 27, 2026
3d55733
docs: use deterministic C# schedule time
May 28, 2026
0a5972e
docs: clarify client frame ticking guidance
Jun 2, 2026
0ee1529
docs: fix generate usage for Unreal bindings
Jun 2, 2026
7e3906e
docs: address Unreal generate usage review
Jun 2, 2026
063b566
docs: regenerate CLI reference
Jun 2, 2026
4cbb7d3
docs: fix TypeScript view cheat sheet syntax
Jun 3, 2026
0514f32
docs: fix auth reference details
Jun 3, 2026
41d4e61
docs: update TypeScript framework integration reference
Jun 4, 2026
3a2c802
Merge branch 'master' into docs/consolidated-docs-prs
cloutiertyler Jun 4, 2026
07211db
docs: fix TypeScript identity table builder
Jun 5, 2026
f5da9ee
Merge branch 'master' into docs/consolidated-docs-prs
cloutiertyler Jun 5, 2026
3e26780
docs: fix TypeScript view context examples
Jun 6, 2026
f38031c
docs: add agent discovery metadata
Jun 6, 2026
bdea1f9
docs: align skill snippets with current APIs
Jun 7, 2026
055ed14
docs: remove obsolete scheduled reducer identity checks
Jun 8, 2026
5060f22
Merge branch 'master' into docs/consolidated-docs-prs
cloutiertyler Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# Generated files
.docusaurus
.cache-loader
static/.well-known/agent-skills/

# Misc
.DS_Store
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -495,10 +495,10 @@ A view can be written in a TypeScript module like so:
```typescript
export const my_player = spacetimedb.view(
{ name: 'my_player', public: true },
t.option(players.row()),
t.option(players.rowType),
(ctx) => {
const row = ctx.db.players.identity.find(ctx.sender);
return row ?? null;
return row ?? undefined;
}
);
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,15 +274,15 @@ Never use external random number generators (like `Random` in C# without using t

## Module Identity

The context provides access to the module's own identity, which is useful for distinguishing between user-initiated and system-initiated reducer calls.
The context provides access to the module's own identity, which is useful when a reducer needs to refer to the database itself.

This is particularly important for [scheduled reducers](./00300-reducers.md) that should only be invoked by the system, not by external clients.
Scheduled reducers and procedures are private by default in SpacetimeDB 2.x, so you do not need to compare the sender against the module identity to prevent ordinary clients from calling them directly. If you need both a scheduled function and a client-callable entry point, keep the scheduled function private and define a separate public reducer that wraps the shared logic.

<Tabs groupId="server-language" queryString>
<TabItem value="typescript" label="TypeScript">

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

const scheduledTask = table(
{ name: 'scheduled_task', scheduled: (): any => send_reminder },
Expand All @@ -296,12 +296,7 @@ const scheduledTask = table(
const spacetimedb = schema({ scheduledTask });
export default spacetimedb;

export const send_reminder = spacetimedb.reducer({ arg: scheduledTask.rowType }, (ctx, { arg }) => {
// Only allow the scheduler (module identity) to call this
if (ctx.sender != ctx.identity) {
throw new SenderError('This reducer can only be called by the scheduler');
}

export const send_reminder = spacetimedb.reducer({ arg: scheduledTask.rowType }, (_ctx, { arg }) => {
console.log(`Reminder: ${arg.message}`);
});
```
Expand All @@ -325,14 +320,8 @@ public static partial class Module
}

[SpacetimeDB.Reducer]
public static void SendReminder(ReducerContext ctx, ScheduledTask task)
public static void SendReminder(ReducerContext _ctx, ScheduledTask task)
{
// Only allow the scheduler (module identity) to call this
if (ctx.Sender != ctx.Identity)
{
throw new Exception("This reducer can only be called by the scheduler");
}

Log.Info($"Reminder: {task.message}");
}
}
Expand All @@ -354,12 +343,7 @@ pub struct ScheduledTask {
}

#[reducer]
fn send_reminder(ctx: &ReducerContext, task: ScheduledTask) {
// Only allow the scheduler (module identity) to call this
if ctx.sender() != ctx.identity() {
panic!("This reducer can only be called by the scheduler");
}

fn send_reminder(_ctx: &ReducerContext, task: ScheduledTask) {
spacetimedb::log::info!("Reminder: {}", task.message);
}
```
Expand All @@ -383,12 +367,7 @@ FIELD_PrimaryKeyAutoInc(scheduled_task, task_id);
// Register the table for scheduling (column 1 = scheduled_at field, 0-based index)
SPACETIMEDB_SCHEDULE(scheduled_task, 1, send_reminder);

SPACETIMEDB_REDUCER(send_reminder, ReducerContext ctx, ScheduledTask task) {
// Only allow the scheduler (module identity) to call this
if (ctx.sender() != ctx.identity()) {
return Err("This reducer can only be called by the scheduler");
}

SPACETIMEDB_REDUCER(send_reminder, ReducerContext _ctx, ScheduledTask task) {
LOG_INFO("Reminder: " + task.message);
return Ok();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export const init = spacetimedb.init((ctx) => {
<TabItem value="csharp" label="C#">

```csharp
[SpacetimeDB.Table(Name = "Config")]
[SpacetimeDB.Table(Accessor = "Config")]
public partial struct Config
{
[SpacetimeDB.PrimaryKey]
Expand Down
8 changes: 4 additions & 4 deletions docs/docs/00200-core-concepts/00200-functions/00500-views.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const my_player = spacetimedb.view(
{ name: 'my_player', public: true },
t.option(players.rowType),
(ctx) => {
const row = ctx.db.players.identity.find(ctx.sender());
const row = ctx.db.players.identity.find(ctx.sender);
return row ?? undefined;
}
);
Expand Down Expand Up @@ -245,7 +245,7 @@ struct Player {
};
SPACETIMEDB_STRUCT(Player, id, identity, name)
SPACETIMEDB_TABLE(Player, player, Public)
FIELD_PrimaryKeyAuto(player, id)
FIELD_PrimaryKeyAutoInc(player, id)
FIELD_Unique(player, identity)

struct PlayerLevel {
Expand Down Expand Up @@ -294,7 +294,7 @@ Views can return either `std::optional<T>` for at-most-one row or `std::vector<T

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.
- **`ViewContext`**: Provides access to the caller's `Identity` through the context's sender field or method. 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`.
Expand All @@ -305,7 +305,7 @@ The choice between `ViewContext` and `AnonymousViewContext` has significant perf

**Anonymous views can be shared across all subscribers.** When a view uses `AnonymousViewContext`, SpacetimeDB knows the result is the same for every client. The database can materialize the view once and serve that same result to all subscribers. When the underlying data changes, it recomputes the view once and broadcasts the update to everyone.

**Per-user views require separate computation for each subscriber.** When a view uses `ViewContext` and invokes `ctx.sender()`, each client potentially sees different data. SpacetimeDB must compute and track the view separately for each subscriber. With 1,000 connected users, that's 1,000 separate view computations and 1,000 separate sets of change tracking.
**Per-user views require separate computation for each subscriber.** When a view uses `ViewContext` and reads the caller's sender identity, each client potentially sees different data. SpacetimeDB must compute and track the view separately for each subscriber. With 1,000 connected users, that's 1,000 separate view computations and 1,000 separate sets of change tracking.

**Prefer `AnonymousViewContext` when possible.** Design your views to be caller-independent when the use case allows. For example:

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/00200-core-concepts/00300-tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ Use idiomatic naming conventions for each language:
| Language | Convention | Example Table | Example Accessor |
|----------|------------|---------------|------------------|
| **TypeScript** | snake_case | `'player_score'` | `ctx.db.playerScore` |
| **C#** | PascalCase | `Name = "PlayerScore"` | `ctx.Db.PlayerScore` |
| **C#** | PascalCase | `Accessor = "PlayerScore"` | `ctx.Db.PlayerScore` |
| **Rust** | lower_snake_case | `name = player_score` | `ctx.db.player_score()` |
| **C++** | lower_snake_case | `player_score` | `ctx.db[player_score]` |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The SpacetimeDB client for C# contains all the tools you need to build native cl
If you are **writing a SpacetimeDB module** (tables and reducers), use these patterns:

- **Module class**: `public static partial class Module`
- **Tables**: `[SpacetimeDB.Table(Accessor = "table_name", Public = true)]` on `partial struct` (or `partial class`) — `Accessor` controls generated API names, and canonical SQL names are derived unless `Name` is explicitly set
- **Tables**: `[SpacetimeDB.Table(Accessor = "TableName", Public = true)]` on `partial struct` (or `partial class`) — `Accessor` controls generated API names, and canonical SQL names are derived unless `Name` is explicitly set
- **Primary key**: Define `[SpacetimeDB.PrimaryKey]` on one column when you need key-based lookups or updates
- **Reducers**: `[SpacetimeDB.Reducer]` on static methods with `ReducerContext ctx` as first parameter
- **Required**: `using SpacetimeDB;` and `partial` on all table structs and the Module class
Expand Down
26 changes: 26 additions & 0 deletions docs/docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,32 @@ const config: Config = {
],

headTags: [
{
tagName: 'link',
attributes: {
rel: 'alternate',
type: 'text/markdown',
title: 'SpacetimeDB docs for agents',
href: '/docs/llms.txt',
},
},
{
tagName: 'link',
attributes: {
rel: 'alternate',
type: 'text/markdown',
title: 'Full SpacetimeDB docs for agents',
href: '/docs/llms-full.txt',
},
},
{
tagName: 'link',
attributes: {
rel: 'sitemap',
type: 'application/xml',
href: '/docs/sitemap.xml',
},
},
{
tagName: 'link',
attributes: {
Expand Down
2 changes: 2 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
"scripts": {
"docusaurus": "docusaurus",
"dev": "docusaurus start",
"prebuild": "node scripts/sync-agent-skills.mjs",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"generate-cli-docs": "node scripts/generate-cli-docs.mjs",
"generate-llms": "docusaurus build && node scripts/generate-llms.mjs",
"sync-agent-skills": "node scripts/sync-agent-skills.mjs",
"rewrite-links": "node scripts/rewrite-doc-links.mjs",
"rewrite-links:write": "node scripts/rewrite-doc-links.mjs --write",
"write-translations": "docusaurus write-translations",
Expand Down
75 changes: 75 additions & 0 deletions docs/scripts/sync-agent-skills.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { createHash } from 'node:crypto';
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '../..');
const sourceDir = path.join(repoRoot, 'skills');
const outputDir = path.join(
repoRoot,
'docs/static/.well-known/agent-skills'
);

function readFrontmatter(markdown, sourcePath) {
const match = markdown.match(/^---\n([\s\S]*?)\n---\n/);
if (!match) {
throw new Error(`${sourcePath} is missing YAML frontmatter`);
}

const frontmatter = {};
for (const line of match[1].split('\n')) {
const field = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
if (field) {
frontmatter[field[1]] = field[2].replace(/^"(.*)"$/, '$1');
}
}

if (!frontmatter.name || !frontmatter.description) {
throw new Error(`${sourcePath} must declare name and description`);
}

return frontmatter;
}

await rm(outputDir, { recursive: true, force: true });
await mkdir(outputDir, { recursive: true });

const skills = [];

for (const entry of (await readdir(sourceDir, { withFileTypes: true })).sort(
(a, b) => a.name.localeCompare(b.name)
)) {
if (!entry.isDirectory()) {
continue;
}

const skillPath = path.join(sourceDir, entry.name, 'SKILL.md');
const markdown = await readFile(skillPath, 'utf8');
const metadata = readFrontmatter(markdown, skillPath);

const skillOutputDir = path.join(outputDir, metadata.name);
await mkdir(skillOutputDir, { recursive: true });
await writeFile(path.join(skillOutputDir, 'SKILL.md'), markdown);

skills.push({
name: metadata.name,
type: 'skill-md',
description: metadata.description,
url: `/docs/.well-known/agent-skills/${metadata.name}/SKILL.md`,
digest: `sha256:${createHash('sha256').update(markdown).digest('hex')}`,
});
}

await writeFile(
path.join(outputDir, 'index.json'),
`${JSON.stringify(
{
$schema: 'https://schemas.agentskills.io/discovery/0.2.0/schema.json',
skills,
},
null,
2
)}\n`
);

5 changes: 5 additions & 0 deletions docs/static/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
User-agent: *
Allow: /
Content-Signal: search=yes, ai-input=yes, ai-train=no

Sitemap: https://spacetimedb.com/docs/sitemap.xml
4 changes: 2 additions & 2 deletions skills/csharp-server/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public ulong AuthorId;

// Multi-column (struct-level):
[SpacetimeDB.Table(Accessor = "Membership")]
[SpacetimeDB.Index.BTree(Accessor = "ByGroupUser", Columns = ["GroupId", "UserId"])]
[SpacetimeDB.Index.BTree(Accessor = "ByGroupUser", Columns = new[] { nameof(GroupId), nameof(UserId) })]
public partial struct Membership { public ulong GroupId; public Identity UserId; ... }
```

Expand Down Expand Up @@ -207,7 +207,7 @@ public static void Tick(ReducerContext ctx, TickTimer timer)
}

// One-time: fires once at a specific time
var at = new ScheduleAt.Time(DateTimeOffset.UtcNow.AddSeconds(10));
var at = new ScheduleAt.Time(ctx.Timestamp + new TimeDuration(10_000_000));
// Repeating: fires on an interval
var at = new ScheduleAt.Interval(TimeSpan.FromSeconds(5));

Expand Down
Loading