mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-07-15 06:41:38 -04:00
Single SDK reference: always use the official customer skills (typescript-server/client SKILL.md). Remove focused/fork alternate refs and STDB_SDK_REF switch
This commit is contained in:
@@ -269,8 +269,8 @@ llm-sequential-upgrade/
|
||||
parse-telemetry.mjs # Telemetry → COST_REPORT.md
|
||||
backends/
|
||||
spacetime.md # SpacetimeDB-specific phases
|
||||
spacetime-sdk-rules.md # SpacetimeDB SDK patterns
|
||||
spacetime-templates.md # Code templates
|
||||
# SDK reference = the official skills/typescript-{server,client}/SKILL.md
|
||||
postgres.md # PostgreSQL-specific phases
|
||||
mongodb.md # MongoDB-specific phases
|
||||
test-plans/
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
# SpacetimeDB TypeScript SDK Reference (focused)
|
||||
|
||||
Lean, chat-app-focused SDK reference for the SpacetimeDB **2.x** TypeScript SDK —
|
||||
parity in scope/prescriptiveness with the PostgreSQL/MongoDB backend files. Covers only
|
||||
what this app needs; omits SDK features the app doesn't use.
|
||||
|
||||
## Imports
|
||||
|
||||
```typescript
|
||||
import { schema, table, t } from 'spacetimedb/server';
|
||||
import { SenderError } from 'spacetimedb/server';
|
||||
import { ScheduleAt } from 'spacetimedb'; // scheduled tables only
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
`table(OPTIONS, COLUMNS)` — two arguments. `name` is snake_case.
|
||||
|
||||
```typescript
|
||||
const user = table(
|
||||
{ name: 'user', public: true },
|
||||
{ identity: t.identity().primaryKey(), name: t.string(), online: t.bool() }
|
||||
);
|
||||
```
|
||||
|
||||
Options: `name` (snake_case), `public: true`, `scheduled: (): any => reducerRef`, `indexes: [...]`.
|
||||
`ctx.db` accessors use the **camelCase** form of the table's `name`.
|
||||
|
||||
## Column Types
|
||||
|
||||
`t.u64()`/`t.i64()` → bigint (use `0n` literals) · `t.u32()`/`t.i32()`/`t.f64()` → number ·
|
||||
`t.bool()` · `t.string()` · `t.identity()` → Identity · `t.timestamp()` → Timestamp ·
|
||||
`t.scheduleAt()` → ScheduleAt · optional: `t.option(t.string())`
|
||||
|
||||
Modifiers: `.primaryKey()` `.autoInc()` `.unique()` `.index('btree')`
|
||||
|
||||
## Indexes
|
||||
|
||||
```typescript
|
||||
// single-column inline (preferred):
|
||||
authorId: t.u64().index('btree'), // → ctx.db.post.authorId.filter(authorId)
|
||||
// multi-column (named):
|
||||
indexes: [{ accessor: 'by_room_user', algorithm: 'btree', columns: ['roomId', 'userIdentity'] }]
|
||||
// → ctx.db.draft.by_room_user.filter([roomId, identity])
|
||||
```
|
||||
|
||||
## Schema Export
|
||||
|
||||
```typescript
|
||||
const spacetimedb = schema({ user, room, message }); // ONE object, not spread args
|
||||
export default spacetimedb;
|
||||
```
|
||||
|
||||
## Reducers
|
||||
|
||||
Export name becomes the reducer name.
|
||||
|
||||
```typescript
|
||||
export const sendMessage = spacetimedb.reducer(
|
||||
{ roomId: t.u64(), text: t.string() },
|
||||
(ctx, { roomId, text }) => {
|
||||
ctx.db.message.insert({ id: 0n, roomId, sender: ctx.sender, text, sentAt: ctx.timestamp });
|
||||
}
|
||||
);
|
||||
// no arguments — just the callback:
|
||||
export const reset = spacetimedb.reducer((ctx) => { ... });
|
||||
```
|
||||
|
||||
## DB Operations
|
||||
|
||||
```typescript
|
||||
ctx.db.message.insert({ id: 0n, ... }); // insert (0n for autoInc PK)
|
||||
ctx.db.message.id.find(msgId); // by PK → row | null
|
||||
ctx.db.user.identity.find(ctx.sender); // by unique column
|
||||
[...ctx.db.message.roomId.filter(roomId)]; // filter → spread to Array
|
||||
[...ctx.db.message.iter()]; // all rows → Array
|
||||
ctx.db.message.id.update({ ...existing, text }); // update (spread + override)
|
||||
ctx.db.message.id.delete(msgId);
|
||||
```
|
||||
|
||||
`iter()`/`filter()` return iterators — spread to Array for `.sort()`/`.map()`/`.filter()`.
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
MUST be `export const` (bare calls are silently ignored).
|
||||
|
||||
```typescript
|
||||
export const init = spacetimedb.init((ctx) => { ... });
|
||||
export const onConnect = spacetimedb.clientConnected((ctx) => { ... });
|
||||
export const onDisconnect = spacetimedb.clientDisconnected((ctx) => { ... });
|
||||
```
|
||||
|
||||
## Reducer Context — identity, time, randomness
|
||||
|
||||
Inside a reducer, get sender / time / randomness **only** from `ctx`.
|
||||
**Standard-library clocks and random sources (`Date.now()`, `Math.random()`) are NOT available
|
||||
in modules** — use `ctx` instead.
|
||||
|
||||
```typescript
|
||||
ctx.sender // caller Identity
|
||||
if (!row.owner.equals(ctx.sender)) throw new SenderError('unauthorized');
|
||||
ctx.timestamp // deterministic server time
|
||||
ctx.db.message.insert({ ..., createdAt: ctx.timestamp });
|
||||
ctx.random(); // [0.0, 1.0)
|
||||
ctx.random.integerInRange(1, 6); // inclusive
|
||||
// Client: Timestamp → Date
|
||||
new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));
|
||||
```
|
||||
|
||||
## Scheduled Tables (timers)
|
||||
|
||||
```typescript
|
||||
const tickTimer = table(
|
||||
{ name: 'tick_timer', scheduled: (): any => tick }, // (): any => breaks circular dep
|
||||
{ scheduledId: t.u64().primaryKey().autoInc(), scheduledAt: t.scheduleAt() }
|
||||
);
|
||||
export const tick = spacetimedb.reducer(
|
||||
{ timer: tickTimer.rowType },
|
||||
(ctx, { timer }) => { /* timer row auto-deleted after this runs */ }
|
||||
);
|
||||
// one-shot: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + delayMicros)
|
||||
// repeating: ScheduleAt.interval(60_000_000n)
|
||||
```
|
||||
|
||||
## React Client — main.tsx
|
||||
|
||||
```typescript
|
||||
import { SpacetimeDBProvider } from 'spacetimedb/react';
|
||||
import { DbConnection } from './module_bindings';
|
||||
import { MODULE_NAME, SPACETIMEDB_URI } from './config';
|
||||
|
||||
const connectionBuilder = useMemo(() =>
|
||||
DbConnection.builder()
|
||||
.withUri(SPACETIMEDB_URI)
|
||||
.withDatabaseName(MODULE_NAME)
|
||||
.withToken(localStorage.getItem('auth_token') || undefined),
|
||||
[]);
|
||||
// <SpacetimeDBProvider connectionBuilder={connectionBuilder}><App /></SpacetimeDBProvider>
|
||||
```
|
||||
|
||||
## React Client — App.tsx
|
||||
|
||||
```typescript
|
||||
import { useTable, useSpacetimeDB } from 'spacetimedb/react';
|
||||
import { DbConnection, tables } from './module_bindings';
|
||||
|
||||
const { isActive, identity: myIdentity, token, getConnection } = useSpacetimeDB();
|
||||
const conn = getConnection() as DbConnection | null;
|
||||
|
||||
useEffect(() => { if (token) localStorage.setItem('auth_token', token); }, [token]);
|
||||
|
||||
useEffect(() => { // subscribe once connected
|
||||
if (!conn || !isActive) return;
|
||||
conn.subscriptionBuilder()
|
||||
.onApplied(() => setReady(true))
|
||||
.subscribe([tables.user, tables.message]); // typed tables (raw SQL strings also accepted)
|
||||
}, [conn, isActive]);
|
||||
|
||||
const [users] = useTable(tables.user); // reactive rows; returns [rows, isReady]
|
||||
const [messages] = useTable(tables.message);
|
||||
|
||||
conn?.reducers.sendMessage({ roomId, text }); // call reducers with object args
|
||||
const isMe = row.owner.toHexString() === myIdentity?.toHexString();
|
||||
```
|
||||
@@ -1,258 +0,0 @@
|
||||
# SpacetimeDB TypeScript SDK Reference
|
||||
|
||||
## Imports
|
||||
|
||||
```typescript
|
||||
import { schema, table, t } from 'spacetimedb/server';
|
||||
import { SenderError } from 'spacetimedb/server';
|
||||
import { ScheduleAt } from 'spacetimedb'; // for scheduled tables only
|
||||
```
|
||||
|
||||
## Tables
|
||||
|
||||
`table(OPTIONS, COLUMNS)` — two arguments. The `name` field MUST be snake_case:
|
||||
|
||||
```typescript
|
||||
const entity = table(
|
||||
{ name: 'entity', public: true },
|
||||
{
|
||||
identity: t.identity().primaryKey(),
|
||||
name: t.string(),
|
||||
active: t.bool(),
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Options: `name` (snake_case, required), `public: true`, `event: true`, `scheduled: (): any => reducerRef`, `indexes: [...]`
|
||||
|
||||
`ctx.db` accessors use the JS variable name (camelCase), not the SQL name.
|
||||
|
||||
## Column Types
|
||||
|
||||
| Builder | JS type | Notes |
|
||||
|---------|---------|-------|
|
||||
| `t.u64()` | bigint | Use `0n` literals |
|
||||
| `t.i64()` | bigint | Use `0n` literals |
|
||||
| `t.u32()` / `t.i32()` | number | |
|
||||
| `t.f64()` / `t.f32()` | number | |
|
||||
| `t.bool()` | boolean | |
|
||||
| `t.string()` | string | |
|
||||
| `t.identity()` | Identity | |
|
||||
| `t.timestamp()` | Timestamp | |
|
||||
| `t.scheduleAt()` | ScheduleAt | |
|
||||
|
||||
Modifiers: `.primaryKey()`, `.autoInc()`, `.unique()`, `.index('btree')`
|
||||
|
||||
Optional columns: `nickname: t.option(t.string())`
|
||||
|
||||
## Indexes
|
||||
|
||||
Prefer inline `.index('btree')` for single-column. Use named indexes only for multi-column:
|
||||
|
||||
```typescript
|
||||
// Inline (preferred):
|
||||
authorId: t.u64().index('btree'),
|
||||
// Access: ctx.db.post.authorId.filter(authorId);
|
||||
|
||||
// Multi-column (named):
|
||||
indexes: [{ accessor: 'by_cat_sev', algorithm: 'btree', columns: ['category', 'severity'] }]
|
||||
```
|
||||
|
||||
## Schema Export
|
||||
|
||||
```typescript
|
||||
const spacetimedb = schema({ entity, record }); // ONE object, not spread args
|
||||
export default spacetimedb;
|
||||
```
|
||||
|
||||
## Reducers
|
||||
|
||||
Export name becomes the reducer name:
|
||||
|
||||
```typescript
|
||||
export const createEntity = spacetimedb.reducer(
|
||||
{ name: t.string(), age: t.i32() },
|
||||
(ctx, { name, age }) => {
|
||||
ctx.db.entity.insert({ identity: ctx.sender, name, age, active: true });
|
||||
}
|
||||
);
|
||||
|
||||
// No arguments — just the callback:
|
||||
export const doReset = spacetimedb.reducer((ctx) => { ... });
|
||||
```
|
||||
|
||||
## DB Operations
|
||||
|
||||
```typescript
|
||||
ctx.db.entity.insert({ id: 0n, name: 'Sample' }); // Insert (0n for autoInc)
|
||||
ctx.db.entity.id.find(entityId); // Find by PK → row | null
|
||||
ctx.db.entity.identity.find(ctx.sender); // Find by unique column
|
||||
[...ctx.db.item.authorId.filter(authorId)]; // Filter → spread to Array
|
||||
[...ctx.db.entity.iter()]; // All rows → Array
|
||||
ctx.db.entity.id.update({ ...existing, name: newName }); // Update (spread + override)
|
||||
ctx.db.entity.id.delete(entityId); // Delete by PK
|
||||
```
|
||||
|
||||
Note: `iter()` and `filter()` return iterators. Spread to Array for `.sort()`, `.filter()`, `.map()`.
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
MUST be `export const` — bare calls are silently ignored:
|
||||
|
||||
```typescript
|
||||
export const init = spacetimedb.init((ctx) => { ... });
|
||||
export const onConnect = spacetimedb.clientConnected((ctx) => { ... });
|
||||
export const onDisconnect = spacetimedb.clientDisconnected((ctx) => { ... });
|
||||
```
|
||||
|
||||
## Authentication & Timestamps
|
||||
|
||||
```typescript
|
||||
// Auth: ctx.sender is the caller's Identity
|
||||
if (!row.owner.equals(ctx.sender)) throw new SenderError('unauthorized');
|
||||
|
||||
// Server timestamps
|
||||
ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp });
|
||||
|
||||
// Client: Timestamp → Date
|
||||
new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n));
|
||||
```
|
||||
|
||||
## Scheduled Tables
|
||||
|
||||
```typescript
|
||||
const tickTimer = table({
|
||||
name: 'tick_timer',
|
||||
scheduled: (): any => tick, // (): any => breaks circular dep
|
||||
}, {
|
||||
scheduledId: t.u64().primaryKey().autoInc(),
|
||||
scheduledAt: t.scheduleAt(),
|
||||
});
|
||||
|
||||
export const tick = spacetimedb.reducer(
|
||||
{ timer: tickTimer.rowType },
|
||||
(ctx, { timer }) => { /* timer row auto-deleted after this runs */ }
|
||||
);
|
||||
|
||||
// One-time: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + delayMicros)
|
||||
// Repeating: ScheduleAt.interval(60_000_000n)
|
||||
```
|
||||
|
||||
## React Client
|
||||
|
||||
### main.tsx — SpacetimeDBProvider is required
|
||||
|
||||
```typescript
|
||||
import React, { useMemo } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { SpacetimeDBProvider } from 'spacetimedb/react';
|
||||
import { DbConnection } from './module_bindings';
|
||||
import { MODULE_NAME, SPACETIMEDB_URI } from './config';
|
||||
import App from './App';
|
||||
|
||||
function Root() {
|
||||
const connectionBuilder = useMemo(() =>
|
||||
DbConnection.builder()
|
||||
.withUri(SPACETIMEDB_URI)
|
||||
.withDatabaseName(MODULE_NAME)
|
||||
.withToken(localStorage.getItem('auth_token') || undefined),
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
|
||||
<App />
|
||||
</SpacetimeDBProvider>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(<Root />);
|
||||
```
|
||||
|
||||
### App.tsx patterns
|
||||
|
||||
```typescript
|
||||
import { useTable, useSpacetimeDB } from 'spacetimedb/react';
|
||||
import { DbConnection, tables } from './module_bindings';
|
||||
|
||||
function App() {
|
||||
const { isActive, identity: myIdentity, token, getConnection } = useSpacetimeDB();
|
||||
const conn = getConnection() as DbConnection | null;
|
||||
|
||||
// Save auth token
|
||||
useEffect(() => { if (token) localStorage.setItem('auth_token', token); }, [token]);
|
||||
|
||||
// Subscribe when connected
|
||||
useEffect(() => {
|
||||
if (!conn || !isActive) return;
|
||||
conn.subscriptionBuilder()
|
||||
.onApplied(() => setSubscribed(true))
|
||||
.subscribe(['SELECT * FROM entity', 'SELECT * FROM record']);
|
||||
}, [conn, isActive]);
|
||||
|
||||
// Reactive data
|
||||
const [entities] = useTable(tables.entity);
|
||||
const [records] = useTable(tables.record);
|
||||
|
||||
// Call reducers with object syntax
|
||||
conn?.reducers.addRecord({ data });
|
||||
|
||||
// Compare identities
|
||||
const isMe = row.owner.toHexString() === myIdentity?.toHexString();
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
// schema.ts
|
||||
import { schema, table, t } from 'spacetimedb/server';
|
||||
|
||||
const entity = table({ name: 'entity', public: true }, {
|
||||
identity: t.identity().primaryKey(),
|
||||
name: t.string(),
|
||||
active: t.bool(),
|
||||
});
|
||||
|
||||
const record = table({ name: 'record', public: true }, {
|
||||
id: t.u64().primaryKey().autoInc(),
|
||||
owner: t.identity(),
|
||||
value: t.u32(),
|
||||
createdAt: t.timestamp(),
|
||||
});
|
||||
|
||||
const spacetimedb = schema({ entity, record });
|
||||
export default spacetimedb;
|
||||
```
|
||||
|
||||
```typescript
|
||||
// index.ts
|
||||
import spacetimedb from './schema';
|
||||
import { t, SenderError } from 'spacetimedb/server';
|
||||
export { default } from './schema';
|
||||
|
||||
export const onConnect = spacetimedb.clientConnected((ctx) => {
|
||||
const existing = ctx.db.entity.identity.find(ctx.sender);
|
||||
if (existing) ctx.db.entity.identity.update({ ...existing, active: true });
|
||||
});
|
||||
|
||||
export const onDisconnect = spacetimedb.clientDisconnected((ctx) => {
|
||||
const existing = ctx.db.entity.identity.find(ctx.sender);
|
||||
if (existing) ctx.db.entity.identity.update({ ...existing, active: false });
|
||||
});
|
||||
|
||||
export const createEntity = spacetimedb.reducer(
|
||||
{ name: t.string() },
|
||||
(ctx, { name }) => {
|
||||
if (ctx.db.entity.identity.find(ctx.sender)) throw new SenderError('already exists');
|
||||
ctx.db.entity.insert({ identity: ctx.sender, name, active: true });
|
||||
}
|
||||
);
|
||||
|
||||
export const addRecord = spacetimedb.reducer(
|
||||
{ value: t.u32() },
|
||||
(ctx, { value }) => {
|
||||
if (!ctx.db.entity.identity.find(ctx.sender)) throw new SenderError('not found');
|
||||
ctx.db.record.insert({ id: 0n, owner: ctx.sender, value, createdAt: ctx.timestamp });
|
||||
}
|
||||
);
|
||||
```
|
||||
@@ -481,7 +481,8 @@ snapshot_inputs() {
|
||||
# Backend specs (only relevant backend)
|
||||
cp "$SCRIPT_DIR/backends/$BACKEND.md" "$INPUTS_DIR/backends/" 2>/dev/null || true
|
||||
if [[ "$BACKEND" == "spacetime" ]]; then
|
||||
cp "$SCRIPT_DIR/backends/spacetime-sdk-rules.md" "$INPUTS_DIR/backends/" 2>/dev/null || true
|
||||
cp "$SCRIPT_DIR/../../skills/typescript-server/SKILL.md" "$INPUTS_DIR/backends/typescript-server-SKILL.md" 2>/dev/null || true
|
||||
cp "$SCRIPT_DIR/../../skills/typescript-client/SKILL.md" "$INPUTS_DIR/backends/typescript-client-SKILL.md" 2>/dev/null || true
|
||||
cp "$SCRIPT_DIR/backends/spacetime-templates.md" "$INPUTS_DIR/backends/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
@@ -750,7 +751,11 @@ if [[ -z "$FIX_MODE" && -z "$UPGRADE_MODE" ]]; then
|
||||
elif [[ "$RULES" == "standard" ]]; then
|
||||
case "$BACKEND" in
|
||||
spacetime)
|
||||
cat "$SCRIPT_DIR/backends/spacetime-sdk-rules.md" > "$APP_DIR/CLAUDE.md"
|
||||
_strip='NR==1 && /^---$/ {fm=1; next} fm && /^---$/ {fm=0; next} !fm {print}'
|
||||
{ awk "$_strip" "$SCRIPT_DIR/../../skills/typescript-server/SKILL.md"
|
||||
echo ""; echo "---"; echo ""
|
||||
awk "$_strip" "$SCRIPT_DIR/../../skills/typescript-client/SKILL.md"
|
||||
} > "$APP_DIR/CLAUDE.md"
|
||||
;;
|
||||
mongodb)
|
||||
echo "# MongoDB Backend" > "$APP_DIR/CLAUDE.md"
|
||||
@@ -771,31 +776,19 @@ if [[ -z "$FIX_MODE" && -z "$UPGRADE_MODE" ]]; then
|
||||
esac
|
||||
echo "Assembled standard CLAUDE.md (rules=$RULES)"
|
||||
else
|
||||
# guided (default) — phases + SDK reference + templates.
|
||||
# SDK reference is selectable via STDB_SDK_REF: focused (default) | skills | fork
|
||||
# guided (default) — phases + the official customer SDK skills + templates.
|
||||
if [[ "$BACKEND" == "spacetime" ]]; then
|
||||
_strip_fm() { awk 'NR==1 && /^---$/ {fm=1; next} fm && /^---$/ {fm=0; next} !fm {print}' "$1"; }
|
||||
_sdk_ref="${STDB_SDK_REF:-focused}"
|
||||
{
|
||||
cat "$SCRIPT_DIR/backends/spacetime.md"
|
||||
echo ""; echo "---"; echo ""
|
||||
case "$_sdk_ref" in
|
||||
skills)
|
||||
_strip_fm "$SCRIPT_DIR/../../skills/typescript-server/SKILL.md"
|
||||
echo ""; echo "---"; echo ""
|
||||
_strip_fm "$SCRIPT_DIR/../../skills/typescript-client/SKILL.md"
|
||||
;;
|
||||
fork)
|
||||
cat "$SCRIPT_DIR/backends/spacetime-sdk-rules.md"
|
||||
;;
|
||||
*) # focused (default) — lean reference, parity in scope with mongo/pg backend files
|
||||
cat "$SCRIPT_DIR/backends/spacetime-sdk-focused.md"
|
||||
;;
|
||||
esac
|
||||
_strip_fm "$SCRIPT_DIR/../../skills/typescript-server/SKILL.md"
|
||||
echo ""; echo "---"; echo ""
|
||||
_strip_fm "$SCRIPT_DIR/../../skills/typescript-client/SKILL.md"
|
||||
echo ""; echo "---"; echo ""
|
||||
cat "$SCRIPT_DIR/backends/spacetime-templates.md"
|
||||
} > "$APP_DIR/CLAUDE.md"
|
||||
echo "Assembled guided CLAUDE.md from spacetime.md + SDK ref [$_sdk_ref] + templates"
|
||||
echo "Assembled guided CLAUDE.md from spacetime.md + official skills + templates"
|
||||
else
|
||||
cp "$SCRIPT_DIR/backends/$BACKEND.md" "$APP_DIR/CLAUDE.md"
|
||||
echo "Copied backends/$BACKEND.md → app CLAUDE.md"
|
||||
|
||||
Reference in New Issue
Block a user