Files
Genarrative/AGENTS.md
2026-04-19 09:17:15 +00:00

26 KiB
Raw Blame History

AGENTS.md

项目约束

  • 对工程的修改不仅要落地到代码更面还要更改对应文档若没有生成新的文档文档统一存在doc目录中
  • 不要擅自把现有中文文案、注释、剧情、文档改写成英文,除非用户明确要求翻译。
  • 看到中文乱码时,不要直接沿用乱码文本,也不要用英文替换;先确认文件真实编码,再决定是否修改。
  • 在 PowerShell 5.1 中读取或写入文本时,必须显式使用 UTF-8如果终端输出疑似乱码要用 Get-Content -Encoding UTF8、Python 或 Node 再次核对原文。
  • 非必要不要整文件重写,尤其是包含中文的文件;优先做局部补丁,避免把未改动的中文内容重新编码。
  • 修改包含中文的文件后,优先运行仓库里的编码检查,确保没有把文本写坏。
  • UI面板中不要默认写一些规则描述文案清爽一些按照游戏UI设计规范设计即可。
  • UI设计需要兼顾网页端、移动端双端的使用体验确保在不同设备上都能正常显示和操作移动端优先考虑。
  • 不要在gitignore中添加.env.local文件。
  • 严格遵循简洁的代码风格
  • 前端只负责做表现所有的逻辑、数据都放到Express后端进行运算和存储。
  • 请默认保持系统的简洁性,能复用、修改、扩展现有系统、页面就不新建新系统新页面。
  • 禁止将功能说明描述类的文本默认写入UI界面中。
  • prd文档中每个模块的描述要落地设计到可以精准编码到位不能出现需求落地漂移。

文档图谱

docs/
├─ README.md
├─ audits/
│  ├─ README.md
│  ├─ FUNCTION_DESIGN_AUDIT_2026-04-03.md
│  ├─ ITEM_AND_BUILD_PRD_AUDIT_2026-04-05.md
│  ├─ engineering/
│  │  ├─ README.md
│  │  ├─ ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md
│  │  ├─ ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md
│  │  ├─ ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md
│  │  └─ MONSTER_NPC_UNIFICATION_AUDIT_2026-04-06.md
│  └─ text/
│     ├─ README.md
│     ├─ CHINESE_MOJIBAKE_INVENTORY.md
│     ├─ EDITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md
│     ├─ GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md
│     ├─ GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md
│     ├─ GAME_UI_PRESET_EDITOR_NPC_PROMPT_TEXT_AUDIT_2026-04-02_DEEP_SCAN.md
│     ├─ GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md
│     ├─ GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md
│     ├─ GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md
│     └─ GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md
├─ design/
│  ├─ README.md
│  ├─ AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md
│  ├─ COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md
│  ├─ CUSTOM_WORLD_CREATOR_INPUT_AND_AI_BOUNDARY_DESIGN_2026-04-06.md
│  ├─ EQUIPMENT_BUILD_AND_FORGE_LOOP_SYSTEM_DESIGN.md
│  └─ npc-conversation-situation-draft.md
├─ experience/
│  ├─ README.md
│  ├─ ADVENTURE_RUNTIME_DEV_EXPERIENCE.md
│  ├─ AGENT_UI_CHANGELOG.md
│  ├─ CODEX_IMPLEMENTATION_EXPERIENCE_2026-03-24.md
│  ├─ CODEX_PAST_WORK_EXPERIENCE_SUMMARY.md
│  ├─ MOBILE_UI_DEV_EXPERIENCE.md
│  ├─ PROJECT_DEVELOPMENT_EXPERIENCE.md
│  └─ PROJECT_WORK_EXPERIENCE_PLAYBOOK.md
├─ planning/
│  ├─ README.md
│  └─ CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md
├─ prd/
│  ├─ AI_CHARACTER_VISUAL_ANIMATION_MVP_PRD_2026-04-04.md
│  ├─ AI_NATIVE_CLASSIC_RPG_EXPERIENCE_BENCHMARK_PRD_2026-04-06.md
│  ├─ AI_NATIVE_CROSS_GENRE_STORY_ENGINE_PRD_2026-04-06.md
│  ├─ AI_NATIVE_CUSTOM_WORLD_CREATION_FLOW_OPTIMIZATION_PRD_2026-04-06.md
│  ├─ AI_NATIVE_NARRATIVE_THREAD_ITEM_AND_WORLD_NPC_PRD_2026-04-06.md
│  ├─ AI_NATIVE_QUEST_SYSTEM_PRD_2026-04-02.md
│  ├─ AI_NATIVE_RUNTIME_ITEM_GENERATION_DESIGN.md
│  ├─ AI_NATIVE_STORY_ENGINE_PHASE1_IMPLEMENTATION_PLAN_2026-04-06.md
│  ├─ AI_NATIVE_STORY_ENGINE_PHASE2_IMPLEMENTATION_PLAN_2026-04-06.md
│  ├─ AI_NATIVE_STORY_ENGINE_PHASE3_IMPLEMENTATION_PLAN_2026-04-06.md
│  ├─ AI_NATIVE_STORY_ENGINE_PHASE4_IMPLEMENTATION_PLAN_2026-04-06.md
│  ├─ AI_NATIVE_STORY_ENGINE_PHASE5_IMPLEMENTATION_PLAN_2026-04-06.md
│  ├─ AI_NATIVE_STORY_ENGINE_PHASE6_IMPLEMENTATION_PLAN_2026-04-06.md
│  ├─ AI_NATIVE_UNIFIED_ROLE_ATTRIBUTE_SYSTEM_PRD_2026-04-02.md
│  ├─ BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md
│  └─ RUNTIME_ITEM_GENERATION_CURRENT_SYSTEM_DESIGN.md
├─ reference/
│  ├─ README.md
│  └─ FUNCTION_SCRIPT_CATALOG_2026-04-04.md
└─ technical/
   ├─ README.md
   ├─ AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md
   ├─ GO_SERVER_RUNTIME_INTEGRATION_2026-04-07.md
   ├─ GO_SERVER_TASKLIST_2026-04-08.md
   ├─ NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md
   ├─ PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md
   └─ SERVER_DEPLOYMENT_AND_CORS_TECHNICAL_SOLUTION_2026-04-05.md

SpacetimeDB Rules (All Languages)

Migrating from 1.0 to 2.0?

If you are migrating existing SpacetimeDB 1.0 code to 2.0, apply spacetimedb-migration-2.0.mdc first. It documents breaking changes (reducer callbacks → event tables, nameaccessor, sender() method, etc.) and should be considered before other rules.


Language-Specific Rules

Language Rule File
TypeScript/React spacetimedb-typescript.mdc (MANDATORY)
Rust spacetimedb-rust.mdc (MANDATORY)
C# spacetimedb-csharp.mdc (MANDATORY)
Migrating 1.0 → 2.0 spacetimedb-migration-2.0.mdc

Core Concepts

  1. Reducers are transactional — they do not return data to callers
  2. Reducers must be deterministic — no filesystem, network, timers, or random
  3. Read data via tables/subscriptions — not reducer return values
  4. Auto-increment IDs are not sequential — gaps are normal, don't use for ordering
  5. ctx.sender is the authenticated principal — never trust identity args

Feature Implementation Checklist

When implementing a feature that spans backend and client:

  1. Backend: Define table(s) to store the data
  2. Backend: Define reducer(s) to mutate the data
  3. Client: Subscribe to the table(s)
  4. Client: Call the reducer(s) from UI — don't forget this step!
  5. Client: Render the data from the table(s)

Common mistake: Building backend tables/reducers but forgetting to wire up the client to call them.


Index System

SpacetimeDB automatically creates indexes for:

  • Primary key columns
  • Columns marked as unique

You can add explicit indexes on non-unique columns for query performance.

Index names must be unique across your entire module (all tables). If two tables have indexes with the same declared name → conflict error.

Schema ↔ Code coupling:

  • Your query code references indexes by name
  • If you add/remove/rename an index in the schema, update all code that uses it
  • Removing an index without updating queries causes runtime errors

Commands

# Login to allow remote database deployment e.g. to maincloud
spacetime login

# Start local SpacetimeDB
spacetime start

# Publish module
spacetime publish <db-name> --module-path <module-path>

# Clear and republish
spacetime publish <db-name> --clear-database -y --module-path <module-path>

# Generate client bindings
spacetime generate --lang <lang> --out-dir <out> --module-path <module-path>

# View logs
spacetime logs <db-name>

Deployment

  • Maincloud is the spacetimedb hosted cloud and the default location for module publishing
  • The default server marked by *** in spacetime server list should be used when publishing
  • If the default server is maincloud you should publish to maincloud
  • Publishing to maincloud is free of charge
  • When publishing to maincloud the database dashboard will be at the url: https://spacetimedb.com/@/
  • The database owner can view utilization and performance metrics on the dashboard

Debugging Checklist

  1. Is SpacetimeDB server running? (spacetime start)
  2. Is the module published? (spacetime publish)
  3. Are client bindings generated? (spacetime generate)
  4. Check server logs for errors (spacetime logs <db-name>)
  5. Is the reducer actually being called from the client?

Editing Behavior

  • Make the smallest change necessary
  • Do NOT touch unrelated files, configs, or dependencies
  • Do NOT invent new SpacetimeDB APIs — use only what exists in docs or this repo
  • Do NOT add restrictions the prompt didn't ask for — if "users can do X", implement X for all users

SpacetimeDB Rust SDK

COMMON MISTAKES — LLM HALLUCINATIONS

These are actual errors observed when LLMs generate SpacetimeDB Rust code:

1. Wrong Crate for Server vs Client

// ❌ WRONG — using client crate for server module
use spacetimedb_sdk::*;  // This is for CLIENTS only!

// ✅ CORRECT — use spacetimedb for server modules
use spacetimedb::{table, reducer, Table, ReducerContext, Identity, Timestamp};

2. Wrong Table Macro Syntax

// ❌ WRONG — using attribute-style like C#
#[spacetimedb::table]
#[primary_key]
pub struct User { ... }

// ❌ WRONG — SpacetimeType on tables (causes conflicts!)
#[derive(SpacetimeType)]
#[table(accessor = my_table)]
pub struct MyTable { ... }

// ✅ CORRECT — use #[table(...)] macro with options, NO SpacetimeType
#[table(accessor = user, public)]
pub struct User {
    #[primary_key]
    identity: Identity,
    name: Option<String>,
}

3. Wrong Table Access Pattern

// ❌ WRONG — using ctx.Db or ctx.db() method or field access
ctx.Db.user.Insert(...);
ctx.db().user().insert(...);
ctx.db.player;  // Field access

// ✅ CORRECT — ctx.db is a field, table names are methods with parentheses
ctx.db.user().insert(User { ... });
ctx.db.user().identity().find(ctx.sender);
ctx.db.player().id().find(&player_id);

4. Wrong Update Pattern

// ❌ WRONG — partial update or using .update() directly on table
ctx.db.user().update(User { name: Some("new".into()), ..Default::default() });

// ✅ CORRECT — find existing, spread it, update via primary key accessor
if let Some(user) = ctx.db.user().identity().find(ctx.sender) {
    ctx.db.user().identity().update(User { name: Some("new".into()), ..user });
}

5. Wrong Reducer Return Type

// ❌ WRONG — returning data from reducer
#[reducer]
pub fn get_user(ctx: &ReducerContext, id: Identity) -> Option<User> { ... }

// ❌ WRONG — mutable context
pub fn my_reducer(ctx: &mut ReducerContext, ...) { }

// ✅ CORRECT — reducers return Result<(), String> or nothing, immutable context
#[reducer]
pub fn do_something(ctx: &ReducerContext, value: String) -> Result<(), String> {
    if value.is_empty() {
        return Err("Value cannot be empty".to_string());
    }
    Ok(())
}

6. Wrong Client Connection Pattern

// ❌ WRONG — subscribing before connected
let conn = DbConnection::builder().build()?;
conn.subscription_builder().subscribe_to_all_tables();  // NOT CONNECTED YET!

// ✅ CORRECT — subscribe in on_connect callback
DbConnection::builder()
    .on_connect(|conn, identity, token| {
        conn.subscription_builder()
            .on_applied(|ctx| println!("Ready!"))
            .subscribe_to_all_tables();
    })
    .build()?;

7. Forgetting to Advance the Connection

// ❌ WRONG — connection never processes messages
let conn = DbConnection::builder().build()?;
// ... callbacks never fire ...

// ✅ CORRECT — must call one of these to process messages
conn.run_threaded();           // Spawn background thread
// OR
conn.run_async().await;        // Async task
// OR (in game loop)
conn.frame_tick()?;            // Manual polling

8. Missing Table Trait Import

// ❌ WRONG — "no method named `insert` found"
use spacetimedb::{table, reducer, ReducerContext};
ctx.db.user().insert(...);  // ERROR!

// ✅ CORRECT — import Table trait for table methods
use spacetimedb::{table, reducer, Table, ReducerContext};
ctx.db.user().insert(...);  // Works!

9. Wrong ScheduleAt Variant

// ❌ WRONG — At variant doesn't exist
scheduled_at: ScheduleAt::At(future_time),

// ✅ CORRECT — use Time variant
scheduled_at: ScheduleAt::Time(future_time),

10. Identity to String Conversion

// ❌ WRONG — to_hex() returns HexString<32>, not String
let id: String = identity.to_hex();  // Type mismatch!

// ✅ CORRECT — chain .to_string()
let id: String = identity.to_hex().to_string();

11. Timestamp Duration Extraction

// ❌ WRONG — returns Result, not Duration directly
let micros = ctx.timestamp.to_duration_since_unix_epoch().as_micros();

// ✅ CORRECT — unwrap the Result
let micros = ctx.timestamp.to_duration_since_unix_epoch()
    .unwrap_or_default()
    .as_micros();

12. Borrow After Move

// ❌ WRONG — `tool` moved into struct, then borrowed
ctx.db.stroke().insert(Stroke { tool, color, ... });
if tool == "eraser" { ... }  // ERROR: value moved!

// ✅ CORRECT — check before move, or use clone
let is_eraser = tool == "eraser";
ctx.db.stroke().insert(Stroke { tool, color, ... });
if is_eraser { ... }

13. Client SDK Uses Blocking I/O

The SpacetimeDB Rust client SDK uses blocking I/O. If mixing with async runtimes (Tokio, async-std), use spawn_blocking or run the SDK on a dedicated thread to avoid blocking the async executor.

14. Wrong Schedule Syntax

// ❌ WRONG — `schedule` is not a valid table type
#[table(name = tick_timer, schedule(reducer = tick, column = scheduled_at))]

// ✅ CORRECT — `scheduled` is a valid table type
#[table(name = tick_timer, scheduled(reducer = tick, column = scheduled_at))]

1) Common Mistakes Table

Server-side errors

Wrong Right Error
#[derive(SpacetimeType)] on #[table] Remove it — macro handles this Conflicting derive macros
ctx.db.player (field access) ctx.db.player() (method) "no field player on type"
ctx.db.player().find(id) ctx.db.player().id().find(&id) Must access via index
&mut ReducerContext &ReducerContext Wrong context type
Missing use spacetimedb::Table; Add import "no method named insert"
#[table(accessor = "my_table")] #[table(accessor = my_table)] String literals not allowed
Missing public on table Add public flag Clients can't subscribe
#[spacetimedb::reducer] #[reducer] after import Wrong attribute path
Network/filesystem in reducer Use procedures instead Sandbox violation
Panic for expected errors Return Result<(), String> WASM instance destroyed

2) Table Definition (CRITICAL)

Tables use #[table(...)] macro on pub struct. DO NOT derive SpacetimeType on tables!

⚠️ CRITICAL: Always import Table trait — required for .insert(), .iter(), .find(), etc.

use spacetimedb::{table, reducer, Table, ReducerContext, Identity, Timestamp};

// ❌ WRONG — DO NOT derive SpacetimeType on tables!
#[derive(SpacetimeType)]  // REMOVE THIS!
#[table(accessor = task)]
pub struct Task { ... }

// ✅ CORRECT — just the #[table] attribute
#[table(accessor = user, public)]
pub struct User {
    #[primary_key]
    identity: Identity,
    
    #[unique]
    username: Option<String>,
    
    online: bool,
}

#[table(accessor = message, public)]
pub struct Message {
    #[primary_key]
    #[auto_inc]
    id: u64,
    
    sender: Identity,
    text: String,
    sent: Timestamp,
}

// With multi-column index
#[table(accessor = task, public, index(name = by_owner, btree(columns = [owner_id])))]
pub struct Task {
    #[primary_key]
    #[auto_inc]
    pub id: u64,
    pub owner_id: Identity,
    pub title: String,
}

Table Options

#[table(accessor = my_table)]           // Private table (default)
#[table(accessor = my_table, public)]   // Public table - clients can subscribe

Column Attributes

#[primary_key]           // Primary key (auto-indexed, enables .find())
#[auto_inc]              // Auto-increment (use with #[primary_key])
#[unique]                // Unique constraint (auto-indexed)
#[index(btree)]          // B-Tree index for queries

Insert returns ROW, not ID

let row = ctx.db.task().insert(Task {
    id: 0,  // auto-inc placeholder
    owner_id: ctx.sender,
    title: "New task".to_string(),
    created_at: ctx.timestamp,
});
let new_id = row.id;  // Get the actual ID

3) Reducers

Definition Syntax

use spacetimedb::{reducer, ReducerContext, Table};

#[reducer]
pub fn send_message(ctx: &ReducerContext, text: String) -> Result<(), String> {
    // Validate input
    if text.is_empty() {
        return Err("Message cannot be empty".to_string());
    }
    
    // Insert returns the inserted row
    let row = ctx.db.message().insert(Message {
        id: 0,  // auto-inc placeholder
        sender: ctx.sender,
        text,
        sent: ctx.timestamp,
    });
    
    log::info!("Message {} sent by {:?}", row.id, ctx.sender);
    Ok(())
}

Update Pattern (CRITICAL)

#[reducer]
pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> {
    // Find existing row
    let user = ctx.db.user().identity().find(ctx.sender)
        .ok_or("User not found")?;
    
    // ✅ CORRECT — spread existing row, override specific fields
    ctx.db.user().identity().update(User {
        name: Some(name),
        ..user  // Preserves identity, online, etc.
    });
    
    Ok(())
}

// ❌ WRONG — partial update nulls out other fields!
// ctx.db.user().identity().update(User { identity: ctx.sender, name: Some(name), ..Default::default() });

Delete Pattern

#[reducer]
pub fn delete_message(ctx: &ReducerContext, message_id: u64) -> Result<(), String> {
    ctx.db.message().id().delete(&message_id);
    Ok(())
}

Lifecycle Hooks

#[reducer(init)]
pub fn init(ctx: &ReducerContext) {
    // Called when module is first published
}

#[reducer(client_connected)]
pub fn client_connected(ctx: &ReducerContext) {
    // ctx.sender is the connecting identity
    if let Some(user) = ctx.db.user().identity().find(ctx.sender) {
        ctx.db.user().identity().update(User { online: true, ..user });
    } else {
        ctx.db.user().insert(User {
            identity: ctx.sender,
            username: None,
            online: true,
        });
    }
}

#[reducer(client_disconnected)]
pub fn client_disconnected(ctx: &ReducerContext) {
    if let Some(user) = ctx.db.user().identity().find(ctx.sender) {
        ctx.db.user().identity().update(User { online: false, ..user });
    }
}

ReducerContext fields

ctx.sender          // Identity of the caller
ctx.timestamp       // Current timestamp
ctx.db              // Database access
ctx.rng             // Deterministic RNG (use instead of rand)

4) Index Access

Primary Key / Unique — .find() returns Option<Row>

// Primary key lookup
let user = ctx.db.user().identity().find(ctx.sender);

// Unique column lookup  
let user = ctx.db.user().username().find(&"alice".to_string());

if let Some(user) = user {
    // Found
}

BTree Index — .filter() returns iterator

#[table(accessor = message, public)]
pub struct Message {
    #[primary_key]
    #[auto_inc]
    id: u64,
    
    #[index(btree)]
    room_id: u64,
    
    text: String,
}

// Filter by indexed column
for msg in ctx.db.message().room_id().filter(&room_id) {
    // Process each message in room
}

No Index — .iter() + manual filter

// Full table scan
for user in ctx.db.user().iter() {
    if user.online {
        // Process online users
    }
}

5) Custom Types

Use #[derive(SpacetimeType)] ONLY for custom structs/enums used as fields or parameters.

use spacetimedb::SpacetimeType;

// Custom struct for table fields
#[derive(SpacetimeType, Clone, Debug, PartialEq)]
pub struct Position {
    pub x: i32,
    pub y: i32,
}

// Custom enum
#[derive(SpacetimeType, Clone, Debug, PartialEq)]
pub enum PlayerStatus {
    Idle,
    Walking(Position),
    Fighting(Identity),
}

// Use in table (DO NOT derive SpacetimeType on the table!)
#[table(accessor = player, public)]
pub struct Player {
    #[primary_key]
    pub id: Identity,
    pub position: Position,
    pub status: PlayerStatus,
}

6) Scheduled Tables

use spacetimedb::{table, reducer, ReducerContext, ScheduleAt, Timestamp};

#[table(accessor = cleanup_job, scheduled(cleanup_expired))]
pub struct CleanupJob {
    #[primary_key]
    #[auto_inc]
    scheduled_id: u64,
    
    scheduled_at: ScheduleAt,
    target_id: u64,
}

#[reducer]
pub fn cleanup_expired(ctx: &ReducerContext, job: CleanupJob) {
    // Job row is auto-deleted after reducer completes
    log::info!("Cleaning up: {}", job.target_id);
}

// Schedule a job
#[reducer]
pub fn schedule_cleanup(ctx: &ReducerContext, target_id: u64, delay_ms: u64) {
    let future_time = ctx.timestamp + std::time::Duration::from_millis(delay_ms);
    ctx.db.cleanup_job().insert(CleanupJob {
        scheduled_id: 0,  // auto-inc placeholder
        scheduled_at: ScheduleAt::Time(future_time),
        target_id,
    });
}

// Cancel by deleting the row
#[reducer]
pub fn cancel_cleanup(ctx: &ReducerContext, job_id: u64) {
    ctx.db.cleanup_job().scheduled_id().delete(&job_id);
}

7) Client SDK

// Connection pattern
let conn = DbConnection::builder()
    .with_uri("http://localhost:3000")
    .with_module_name("my-module")
    .with_token(load_saved_token())  // None for first connection
    .on_connect(on_connected)
    .build()
    .expect("Failed to connect");

// Subscribe in on_connect callback, NOT before!
fn on_connected(conn: &DbConnection, identity: Identity, token: &str) {
    conn.subscription_builder()
        .on_applied(|ctx| println!("Ready!"))
        .subscribe_to_all_tables();
}

⚠️ CRITICAL: Advance the Connection

You MUST call one of these — without it, no callbacks fire:

conn.run_threaded();           // Background thread (simplest)
conn.run_async().await;        // Async task
conn.frame_tick()?;            // Manual polling (game loops)

Table Access & Callbacks

// Iterate
for user in ctx.db.user().iter() { ... }

// Find by primary key
if let Some(user) = ctx.db.user().identity().find(&identity) { ... }

// Row callbacks
ctx.db.user().on_insert(|ctx, user| { ... });
ctx.db.user().on_update(|ctx, old, new| { ... });
ctx.db.user().on_delete(|ctx, user| { ... });

// Call reducers
ctx.reducers.set_name("Alice".to_string()).unwrap();

8) Procedures (Beta)

Procedures are for side effects (HTTP, filesystem) that reducers can't do.

⚠️ Procedures are currently in beta. API may change.

use spacetimedb::{procedure, ProcedureContext};

// Simple procedure
#[procedure]
fn add_numbers(_ctx: &mut ProcedureContext, a: u32, b: u32) -> u64 {
    a as u64 + b as u64
}

// Procedure with database access
#[procedure]
fn save_external_data(ctx: &mut ProcedureContext, url: String) -> Result<(), String> {
    // HTTP request (allowed in procedures, not reducers)
    let data = fetch_from_url(&url)?;

    // Database access requires explicit transaction
    ctx.try_with_tx(|tx| {
        tx.db.external_data().insert(ExternalData {
            id: 0,
            content: data,
        });
        Ok(())
    })?;

    Ok(())
}

Key differences from reducers

Reducers Procedures
&ReducerContext (immutable) &mut ProcedureContext (mutable)
Direct ctx.db access Must use ctx.with_tx()
No HTTP/network HTTP allowed
No return values Can return data

9) Logging

use spacetimedb::log;

log::trace!("Detailed trace");
log::debug!("Debug info");
log::info!("Information");
log::warn!("Warning");
log::error!("Error occurred");

10) Commands

# Start local server
spacetime start

# Publish module
spacetime publish <module-name> --module-path <backend-dir>

# Clear database and republish
spacetime publish <module-name> --clear-database -y --module-path <backend-dir>

# Generate bindings
spacetime generate --lang rust --out-dir <client>/src/module_bindings --module-path <backend-dir>

# View logs
spacetime logs <module-name>

11) Hard Requirements

Rust-specific:

  1. DO NOT derive SpacetimeType on #[table] structs — the macro handles this
  2. Import Table traituse spacetimedb::Table; required for .insert(), .iter(), etc.
  3. Use &ReducerContext — not &mut ReducerContext
  4. Tables are methodsctx.db.table() not ctx.db.table
  5. Server modules use spacetimedb crate — clients use spacetimedb-sdk
  6. Reducers must be deterministic — no filesystem, network, timers, or external RNG
  7. Use ctx.rng — not rand crate for random numbers
  8. Use ctx.timestamp — never std::time::SystemTime::now() in reducers
  9. Client MUST advance connection — call run_threaded(), run_async(), or frame_tick()
  10. Subscribe in on_connect callback — not before connection is established
  11. Update requires full row — spread existing row with ..existing
  12. DO NOT edit generated bindings — regenerate with spacetime generate
  13. Identity to String needs .to_string()identity.to_hex().to_string()
  14. Client SDK is blocking — use spawn_blocking or dedicated thread if mixing with async runtimes