diff --git a/docs/.gitignore b/docs/.gitignore index b2d6de30624..545105cda9a 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -7,6 +7,7 @@ # Generated files .docusaurus .cache-loader +static/.well-known/agent-skills/ # Misc .DS_Store diff --git a/docs/docs/00100-intro/00100-getting-started/00400-key-architecture.md b/docs/docs/00100-intro/00100-getting-started/00400-key-architecture.md index 61a70b681e7..487b962f4ad 100644 --- a/docs/docs/00100-intro/00100-getting-started/00400-key-architecture.md +++ b/docs/docs/00100-intro/00100-getting-started/00400-key-architecture.md @@ -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; } ); ``` diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md index 8df888acb24..4180d28ba6d 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md +++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md @@ -30,14 +30,14 @@ export const create_user = spacetimedb.reducer({ name: t.string(), email: t.stri // Modify tables ctx.db.user.insert({ - id: 0, // auto-increment will assign + id: 0n, // auto-increment will assign name, email }); }); ``` -The first argument is the reducer name, the second defines argument types, and the third is the handler function taking `(ctx, args)`. +The exported const name becomes the reducer name. Pass an argument type object followed by a handler function taking `(ctx, args)`. For reducers with no arguments, pass only the handler function. @@ -127,8 +127,8 @@ SPACETIMEDB_REDUCER(create_user, ReducerContext ctx, std::string name, std::stri } // Modify tables - User user{0, name, email}; // 0 for id - auto-increment will assign - ctx.db[user].insert(user); + User new_user{0, name, email}; // 0 for id - auto-increment will assign + ctx.db[user].insert(new_user); return Ok(); } @@ -160,7 +160,7 @@ Reducers have full read-write access to all tables (both public and private) thr ```typescript ctx.db.user.insert({ - id: 0, // auto-increment will assign + id: 0n, // auto-increment will assign name: 'Alice', email: 'alice@example.com' }); diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md index 4338f0083a2..ac5374516e5 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md +++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md @@ -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. ```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 }, @@ -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}`); }); ``` @@ -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}"); } } @@ -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); } ``` @@ -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(); } diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md index 5a9b58e9adf..b679d90ef82 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md +++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md @@ -22,7 +22,7 @@ export const init = spacetimedb.init((ctx) => { console.log('Database initializing...'); // Set up default data - if (ctx.db.settings.count === 0) { + if (ctx.db.settings.count() === 0n) { ctx.db.settings.insert({ key: 'welcome_message', value: 'Hello, SpacetimeDB!' @@ -133,7 +133,7 @@ export const init = spacetimedb.init((ctx) => { ```csharp -[SpacetimeDB.Table(Name = "Config")] +[SpacetimeDB.Table(Accessor = "Config")] public partial struct Config { [SpacetimeDB.PrimaryKey] diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md index 4fb03251e06..9d7a4cae7af 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md +++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md @@ -25,9 +25,9 @@ Throw a `SenderError`: import { SenderError } from 'spacetimedb/server'; export const transfer_credits = spacetimedb.reducer( - { to_user: t.u64(), amount: t.u32() }, + { to_user: t.identity(), amount: t.u32() }, (ctx, { to_user, amount }) => { - const fromUser = ctx.db.users.id.find(ctx.sender); + const fromUser = ctx.db.users.identity.find(ctx.sender); if (!fromUser) { throw new SenderError('User not found'); } @@ -60,9 +60,9 @@ Throw an exception: ```csharp [SpacetimeDB.Reducer] -public static void TransferCredits(ReducerContext ctx, ulong toUser, uint amount) +public static void TransferCredits(ReducerContext ctx, Identity toUser, uint amount) { - var fromUser = ctx.Db.User.Id.Find(ctx.Sender); + var fromUser = ctx.Db.User.Identity.Find(ctx.Sender); if (fromUser == null) { throw new InvalidOperationException("User not found"); @@ -86,13 +86,13 @@ Return an error: #[reducer] pub fn transfer_credits( ctx: &ReducerContext, - to_user: u64, + to_user: Identity, amount: u32 ) -> Result<(), String> { - let from_balance = ctx.db.users().id().find(ctx.sender.identity) - .ok_or("User not found"); + let from_user = ctx.db.users().identity().find(ctx.sender()) + .ok_or("User not found")?; - if from_balance.credits < amount { + if from_user.credits < amount { return Err("Insufficient credits".to_string()); } diff --git a/docs/docs/00200-core-concepts/00200-functions/00500-views.md b/docs/docs/00200-core-concepts/00200-functions/00500-views.md index a33bb74863b..1bd194d4548 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00500-views.md +++ b/docs/docs/00200-core-concepts/00200-functions/00500-views.md @@ -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; } ); @@ -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 { @@ -294,7 +294,7 @@ Views can return `std::optional` for at-most-one row, `std::vector` for mu 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`. @@ -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: diff --git a/docs/docs/00200-core-concepts/00300-tables.md b/docs/docs/00200-core-concepts/00300-tables.md index e725a0ff2d8..dd8ec6c6b0e 100644 --- a/docs/docs/00200-core-concepts/00300-tables.md +++ b/docs/docs/00200-core-concepts/00300-tables.md @@ -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]` | diff --git a/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md b/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md index 67a04bec608..86c1589ea46 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md +++ b/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md @@ -211,7 +211,7 @@ export const register_document = spacetimedb.reducer({ storageUrl: t.string(), }, (ctx, { filename, mimeType, sizeBytes, storageUrl }) => { ctx.db.document.insert({ - id: 0, // auto-increment + id: 0n, // auto-increment ownerId: ctx.sender, filename, mimeType, diff --git a/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md b/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md index 93cb8db3675..36005fa6c3f 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md +++ b/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md @@ -136,9 +136,9 @@ Reducers receive a `ReducerContext` which provides full read-write access to all ```typescript -export const example = spacetimedb.reducer({}, (ctx) => { +export const example = spacetimedb.reducer((ctx) => { // Insert - ctx.db.user.insert({ id: 0, name: 'Alice', email: 'alice@example.com' }); + ctx.db.user.insert({ id: 0n, name: 'Alice', email: 'alice@example.com' }); // Read: iterate all rows for (const user of ctx.db.user.iter()) { diff --git a/docs/docs/00200-core-concepts/00300-tables/00600-performance.md b/docs/docs/00200-core-concepts/00300-tables/00600-performance.md index 189a54c20fb..4bc49a08546 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00600-performance.md +++ b/docs/docs/00200-core-concepts/00300-tables/00600-performance.md @@ -518,7 +518,7 @@ When inserting or updating multiple rows, batch them in a single reducer call ra export const spawn_enemies = spacetimedb.reducer({ count: t.u32() }, (ctx, { count }) => { for (let i = 0; i < count; i++) { ctx.db.enemy.insert({ - id: 0, // auto_inc + id: 0n, // auto_inc health: 100, }); } diff --git a/docs/docs/00200-core-concepts/00600-clients/00600-csharp-reference.md b/docs/docs/00200-core-concepts/00600-clients/00600-csharp-reference.md index a6a456d007a..bbde96063c2 100644 --- a/docs/docs/00200-core-concepts/00600-clients/00600-csharp-reference.md +++ b/docs/docs/00200-core-concepts/00600-clients/00600-csharp-reference.md @@ -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 diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 0a499284d96..b9c3cd025f1 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -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: { diff --git a/docs/package.json b/docs/package.json index 64e5d951180..94b71bcb1f0 100644 --- a/docs/package.json +++ b/docs/package.json @@ -5,6 +5,7 @@ "scripts": { "docusaurus": "docusaurus", "dev": "docusaurus start", + "prebuild": "node scripts/sync-agent-skills.mjs", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", @@ -12,6 +13,7 @@ "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", diff --git a/docs/scripts/sync-agent-skills.mjs b/docs/scripts/sync-agent-skills.mjs new file mode 100644 index 00000000000..8de4f839815 --- /dev/null +++ b/docs/scripts/sync-agent-skills.mjs @@ -0,0 +1,73 @@ +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` +); diff --git a/docs/static/robots.txt b/docs/static/robots.txt new file mode 100644 index 00000000000..5e46fde337c --- /dev/null +++ b/docs/static/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Allow: / +Content-Signal: search=yes, ai-input=yes, ai-train=no + +Sitemap: https://spacetimedb.com/docs/sitemap.xml diff --git a/skills/csharp-server/SKILL.md b/skills/csharp-server/SKILL.md index f758a98c58b..05f353d5802 100644 --- a/skills/csharp-server/SKILL.md +++ b/skills/csharp-server/SKILL.md @@ -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; ... } ``` @@ -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));