spacetimedb-concepts について
spacetimedb-concepts は、SpacetimeDB を使ったアプリケーションの構築とデバッグを支援するリファレンススキルです。SpacetimeDB は WebAssembly モジュール内でアプリケーションロジックを実行するリレーショナルデータベースで、テーブル、トランザクショナルな Reducer、サブスクリプション、認証、クライアント連携を解説します。
主な機能
- 01テーブルと Reducer の設計を解説
- 02クライアントのリアルタイムサブスクリプションを説明
- 03モジュールのライフサイクルと公開を解説
- 04認証と認可のガイダンスを提供
- 05実装とデバッグのチェックリストを収録
活用シーン
- 01データテーブルと更新用 Reducer の設計
- 02クライアントの表示を購読データに接続
- 03未公開または未呼び出しのモジュールの調査
- 04リアルタイムゲームや共同作業アプリの構築
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ clockworklabs/ spacetimedb --skill spacetimedb-conceptsインストール後、Claude Code で/spacetimedb-conceptsを入力して読み込まれたか確認します。
詳細
SpacetimeDB の基本概念
SpacetimeDB はサーバーでもあるリレーショナルデータベースです。WebAssembly モジュールを介してアプリケーションロジックをデータベースに直接アップロードできるため、従来の Web/ゲームサーバー層を完全になくせます。
重要なルール(最初にお読みください)
次の 5 つのルールを守ることで、SpacetimeDB でよくある間違いを防げます。
- リデューサーはトランザクション処理されます — 呼び出し元にデータを返しません。データの読み取りにはサブスクリプションを使用してください。
- リデューサーは決定的でなければなりません — ファイルシステム、ネットワーク、タイマー、乱数は使用できません。すべての状態はテーブルから取得する必要があります。
- データはテーブル/サブスクリプションを介して読み取ります — リデューサーの戻り値は使用しません。クライアントはサブスクライブしたクエリを通じてデータを取得します。
- 自動インクリメント ID は連番ではありません — 欠番は正常です。順序付けには使用せず、タイムスタンプまたは明示的なシーケンス列を使用してください。
ctx.sender()は認証済みのプリンシパルです — 引数として渡された ID を決して信用しないでください。認可には必ずctx.sender()を使用してください。
機能実装チェックリスト
バックエンドとクライアントにまたがる機能を実装する場合:
- バックエンド: データを保存するテーブルを定義する
- バックエンド: データを変更するリデューサーを定義する
- クライアント: テーブルをサブスクライブする
- クライアント: UI からリデューサーを呼び出す — この手順を省略しないでください
- クライアント: テーブルのデータを表示する
よくある間違い: バックエンドのテーブルやリデューサーを作成したものの、クライアントから呼び出せるように接続するのを忘れること。
デバッグチェックリスト
問題が発生した場合:
- SpacetimeDB サーバーは起動していますか?(
spacetime start) - モジュールは公開されていますか?(
spacetime publish) - クライアントバインディングは生成されていますか?(
spacetime generate) - サーバーログにエラーがないか確認してください(
spacetime logs <db-name>) - クライアントからリデューサーが実際に呼び出されていますか?
CLI コマンド
spacetime start
spacetime publish <db-name> --module-path <module-path>
spacetime publish <db-name> --clear-database -y --module-path <module-path>
spacetime generate --lang <lang> --out-dir <out> --module-path <module-path>
spacetime logs <db-name>
SpacetimeDB とは
SpacetimeDB はデータベースとアプリケーションサーバーを、デプロイ可能な単一のユニットに統合します。クライアントはデータベースに直接接続し、その内部でアプリケーションロジックを実行します。このシステムは、最高の速度と最低のレイテンシーが求められるリアルタイムアプリケーション向けに最適化されています。
主な特徴:
- インメモリ実行:非常に低レイテンシーでアクセスできるよう、アプリケーションの状態をメモリから提供します
- 永続ストレージ:データは耐久性を確保するため、ライトアヘッドログ(WAL)に自動的に永続化されます
- リアルタイム同期:変更はサブスクライブしているクライアントに自動的にプッシュされます
- 単一デプロイ:別個のサーバー、コンテナ、インフラストラクチャを管理する必要がありません
5 つの禅の原則
- すべてがテーブルです:アプリケーションの状態はすべてテーブルに保存されます。別個のキャッシュ層も、Redis も、同期が必要なインメモリ状態もありません。
- すべてが永続化されます:SpacetimeDB はデフォルトで状態を永続化します(たとえば、WAL による耐久性を通じて)。
- すべてがリアルタイムです:クライアントはサーバーの状態のレプリカです。データをサブスクライブすると、自動的に配信されます。
- すべてがトランザクション処理されます:すべてのリデューサーはアトミックに実行されます。変更はすべて成功するか、すべてロールバックされます。
- すべてがプログラム可能です:モジュールは、データベース内で実行される実際のコード(Rust、C#、TypeScript)です。
テーブル
SpacetimeDB のすべてのデータはテーブルに保存されます。テーブルはリレーショナルモデルを使用し、サブスクリプション向けの SQL クエリをサポートします。
テーブルの定義
テーブルは言語固有の属性を使って定義します。2.0 では、API 名には name ではなく accessor を使用してください。
Rust:
#[spacetimedb::table(accessor = player, public)]
pub struct Player {
#[primary_key]
#[auto_inc]
id: u32,
#[index(btree)]
name: String,
#[unique]
email: String,
}
C#:
[SpacetimeDB.Table(Accessor = "Player", Public = true)]
public partial struct Player
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public uint Id;
[SpacetimeDB.Index.BTree]
public string Name;
[SpacetimeDB.Unique]
public string Email;
}
TypeScript:
const players = table(
{ name: 'players', public: true },
{
id: t.u32().primaryKey().autoInc(),
name: t.string().index('btree'),
email: t.string().unique(),
}
);
テーブルの公開範囲
- プライベートテーブル(デフォルト):リデューサーとデータベース所有者のみがアクセスできます
- パブリックテーブル:サブスクリプションを通じてクライアントに読み取りアクセスを公開します。書き込みには引き続きリデューサーが必要です。
テーブル設計の原則
エンティティではなく、アクセスパターンに基づいてデータを整理してください。
分解するアプローチ(推奨):
Player PlayerState PlayerStats
id <-- player_id player_id
name position_x total_kills
position_y total_deaths
velocity_x play_time
メリット:帯域幅の削減、キャッシュ効率の向上、スキーマの進化、意味の明確化。
リデューサー
リデューサーは、データベースの状態を変更するトランザクション関数です。クライアントから呼び出す主な変更処理の手段であり、プロシージャも明示的なトランザクションを実行してテーブルを変更できます。
主な特性
- トランザクション性:分離されたデータベーストランザクション内で実行されます
- 原子性:すべての変更が成功するか、すべてロールバックされます
- 分離性:外部の世界とはやり取りできません(ネットワークやファイルシステムにはアクセスできません)
- 呼び出し可能:クライアントはリモートプロシージャコールとしてリデューサーを呼び出します
リデューサーの重要なルール
- グローバル状態を使用しない:静的変数への依存は未定義の動作になります
- 副作用を発生させない:リデューサーはネットワークリクエストを送信したり、ファイルにアクセスしたりできません
- 状態はテーブルに保存する:永続化する状態はすべてテーブルに保存する必要があります
- データを返さない:リデューサーは呼び出し元にデータを返しません。サブスクリプションを使用してください
- 決定的であること:乱数、タイマー、外部 I/O は使用できません
リデューサーの定義
Rust:
#[spacetimedb::reducer]
pub fn create_user(ctx: &ReducerContext, name: String, email: String) -> Result<(), String> {
if name.is_empty() {
return Err("Name cannot be empty".to_string());
}
ctx.db.user().insert(User { id: 0, name, email });
Ok(())
}
C#:
[SpacetimeDB.Reducer]
public static void CreateUser(ReducerContext ctx, string name, string email)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException("Name cannot be empty");
ctx.Db.User.Insert(new User { Id = 0, Name = name, Email = email });
}
ReducerContext
すべてのリデューサーには ReducerContext が渡され、次の情報を利用できます。
- データベース:
ctx.db(Rust のフィールド、TS のプロパティ)/ctx.Db(C# のプロパティ) - 送信者:
ctx.sender()(Rust のメソッド)/ctx.Sender(C# のプロパティ)/ctx.sender(TS のプロパティ) - 接続 ID:
ctx.connection_id()(Rust のメソッド)/ctx.ConnectionId(C# のプロパティ)/ctx.connectionId(TS のプロパティ) - タイムスタンプ:
ctx.timestamp(Rust のフィールド、TS のプロパティ)/ctx.Timestamp(C# のプロパティ)
イベントテーブル(2.0)
イベントテーブルは、リデューサー固有のデータをクライアントにブロードキャストするための推奨手段です。
#[table(accessor = damage_event, public, event)]
pub struct DamageEvent {
pub target: Identity,
pub amount: u32,
}
#[reducer]
fn deal_damage(ctx: &ReducerContext, target: Identity, amount: u32) {
ctx.db.damage_event().insert(DamageEvent { target, amount });
}
クライアントはイベントテーブルをサブスクライブし、on_insert コールバックを使用します。イベントテーブルは明示的にサブスクライブする必要があり、subscribe_to_all_tables() の対象から除外されます。
サブスクリプション
サブスクリプションは、データベースの行をリアルタイムでクライアントに複製します。
サブスクリプションの仕組み
- サブスクライブ:必要なデータを記述する SQL クエリを登録します
- 初期データを受信:一致する行がすべて直ちに送信されます
- 更新を受信:サブスクライブ中の行が変更されると、リアルタイムで更新が送信されます
- 変更に対応:コールバック(
onInsert、onDelete、onUpdate)を使用します
サブスクリプションのベストプラクティス
- 有効期間ごとにサブスクリプションをグループ化する:常に必要なデータと一時的なサブスクリプションを分けます
- サブスクライブしてから解除する:サブスクリプションを更新するときは、新しいデータを先にサブスクライブします
- クエリの重複を避ける:重複するデータを返す異なるクエリは、処理の重複を引き起こします
- インデックスを使用する:インデックス付き列へのクエリは効率的ですが、テーブル全体のスキャンにはコストがかかります
モジュール
モジュールは、データベース内で実行されるアプリケーションロジックを含む WebAssembly バンドルです。
モジュールの構成要素
- テーブル:データスキーマを定義します
- リデューサー:状態を変更する呼び出し可能な関数を定義します
- ビュー:読み取り専用の計算クエリを定義します
- イベントテーブル:リデューサー固有のデータをクライアントにブロードキャストします(2.0)
- プロシージャ:(ベータ版)副作用(HTTP リクエストなど)を発生させる関数です
モジュールの言語
サーバー側のモジュールは、Rust、C#、TypeScript(ベータ版)で記述できます。
モジュールのライフサイクル
- 記述:選択した言語でテーブルとリデューサーを定義します
- コンパイル:SpacetimeDB CLI を使用して WebAssembly にビルドします
- 公開:
spacetime publishを使用して SpacetimeDB ホストにアップロードします - ホットスワップ:クライアントを切断せずに再公開してコードを更新します
アイデンティティ
アイデンティティは、OpenID Connect(OIDC)に基づく SpacetimeDB の認証システムです。
- アイデンティティ:ユーザーを識別する、長期間有効なグローバルに一意の識別子です。
- ConnectionId:特定のクライアント接続を識別します。
#[spacetimedb::reducer]
pub fn do_something(ctx: &ReducerContext) {
let caller_identity = ctx.sender(); // Who is calling?
// NEVER trust identity passed as a reducer argument
}
認証プロバイダー
SpacetimeDB は、SpacetimeAuth(組み込み)、Auth0、Clerk、Keycloak、Google、GitHub など、多くの OIDC プロバイダーに対応しています。
SpacetimeDB を使用する場面
最適なユースケース
- リアルタイムゲーム:MMO、マルチプレイヤーゲーム、ターン制ゲーム
- コラボレーションアプリケーション:ドキュメント編集、ホワイトボード、デザインツール
- チャットとメッセージング:プレゼンス機能を備えたリアルタイム通信
- ライブダッシュボード:ストリーミング分析とモニタリング
主な判断要因
次の条件が必要な場合は、SpacetimeDB を選択してください。
- 読み取りと書き込みのレイテンシーが 10ms 未満
- リアルタイムの自動同期
- すべての操作に対するトランザクション保証
- シンプルなアーキテクチャ(個別のキャッシュ、キュー、サーバーが不要)
あまり適していない用途
- バッチ分析:OLAP ではなく、OLTP 向けに最適化されています
- 大容量の BLOB ストレージ:構造化されたリレーショナルデータに適しています
- ステートレス API:従来の REST API ではリアルタイム同期は不要です
一般的なパターン
リデューサーでの認証チェック:
#[spacetimedb::reducer]
fn admin_action(ctx: &ReducerContext) -> Result<(), String> {
let admin = ctx.db.admin().identity().find(&ctx.sender())
.ok_or("Not an admin")?;
Ok(())
}
スケジュールされたリデューサー:
#[spacetimedb::table(accessor = reminder, scheduled(send_reminder))]
pub struct Reminder {
#[primary_key]
#[auto_inc]
id: u64,
scheduled_at: ScheduleAt,
message: String,
}
#[spacetimedb::reducer]
fn send_reminder(ctx: &ReducerContext, reminder: Reminder) {
log::info!("Reminder: {}", reminder.message);
}
編集時の動作
SpacetimeDB のコードを変更する場合:
- 必要最小限の変更にとどめます
- 無関係なファイル、設定、依存関係には手を加えないでください
- SpacetimeDB API を新たに作り出さず、ドキュメントまたはこのリポジトリに存在するものだけを使用してください