# Writing SpacetimeDB Schema Definitions: The Complete Guide for Rust and TypeScript

> Learn to write SpacetimeDB schema definitions with Rust macros or TypeScript APIs. Our guide details tables, columns, indexes, and reducers for efficient data management.

- Repository: [Clockwork Labs/SpacetimeDB](https://github.com/clockworklabs/SpacetimeDB)
- Tags: how-to-guide
- Published: 2026-03-09

---

**SpacetimeDB schema definitions describe tables, columns, indexes, and scheduled reducers using either Rust procedural macros or TypeScript builder APIs, which compile to SATS-JSON schemas stored in the database's system tables.**

In the **clockworklabs/SpacetimeDB** repository, modules declare their data model through schema definitions that bridge server-side storage and client-side type safety. Whether you are building multiplayer games or real-time collaboration tools, understanding how to write these definitions in both Rust and TypeScript is essential for leveraging SpacetimeDB's automatic migration and code generation capabilities.

## Understanding SpacetimeDB Schema Architecture

SpacetimeDB employs a unified schema system where table metadata flows from source code into internal system tables (`st_table`, `st_column`, etc.). This architecture enables **automatic schema migrations** when you republish modules and guarantees type safety across the network boundary.

### Core Schema Components

The implementation spans multiple layers of the codebase:

| Component | Role | Source Location |
|-----------|------|-----------------|
| `#[spacetimedb::table]` attribute | Marks Rust structs as tables and generates WASM ABI metadata with options for `public`, `primary_key`, `auto_inc`, and `index` | [`modules/sdk-test/src/lib.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/modules/sdk-test/src/lib.rs) (lines 56-65) |
| `define_tables!` macro | Expands a compact DSL into multiple `#[spacetimedb::table]` structs plus associated reducer functions | [`modules/sdk-test/src/lib.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/modules/sdk-test/src/lib.rs) (lines 56-78) |
| `table()` builder (TypeScript) | Runtime API that registers table definitions and produces JSON-encoded SATS schemas | [`crates/bindings-typescript/src/server/schema.ts`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/bindings-typescript/src/server/schema.ts) |
| Column type helpers (`t.identity()`, `t.u64()`) | TypeScript utilities defining column constraints including optional fields, primary keys, and schedule timestamps | [`crates/bindings-typescript/src/server/schema.ts`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/bindings-typescript/src/server/schema.ts) (lines 104-119) |
| System tables | Internal metadata storage (`st_table`, `st_column`) that persists schema definitions and enables migration logic | [`docs/versioned_docs/version-1.12.0/00300-resources/00200-reference/00400-sql-reference.md`](https://github.com/clockworklabs/SpacetimeDB/blob/main/docs/versioned_docs/version-1.12.0/00300-resources/00200-reference/00400-sql-reference.md) (line 494) |

### How Schema Definitions Flow to the Database

1. **Authoring**: Developers write Rust structs with `#[spacetimedb::table]` attributes or use the `define_tables!` macro for bulk definitions. Alternatively, TypeScript modules use the `schema()` and `table()` builders.
2. **Compilation**: The Rust compiler expands procedural macros into WASM-compatible metadata. TypeScript builders serialize to JSON at build time.
3. **Publishing**: The `spacetime publish` command transmits the compiled module and schema JSON to the server, which stores definitions in system tables and attempts automatic migration.
4. **Codegen**: The CLI generates client SDKs (`spacetime generate`) that mirror the server schema, exposing `spacetimedb.table(...)` objects for type-checked client operations.

## Defining Tables in Rust

Rust modules use attribute macros to declare tables, offering fine-grained control over visibility, indexing, and scheduling.

### Using the #[spacetimedb::table] Attribute

The primary method for defining tables involves annotating structs with `#[spacetimedb::table(...)]`. In [`modules/sdk-test/src/lib.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/modules/sdk-test/src/lib.rs), this pattern implements a `Users` table with primary keys, secondary indexes, and public visibility:

```rust
#[spacetimedb::table(
    accessor = users,          // Client access path: ctx.db.users()
    public,                    // Rows visible to all clients
    primary_key,               // The identity column serves as primary key
    index(accessor = name_idx, btree(columns = [name]))
)]
pub struct Users {
    #[primary_key]
    identity: spacetimedb::Identity,

    #[spacetimedb::index]
    name: String,
}

```

Key arguments include:
- **accessor**: Defines the client-side API name for the table.
- **public**: Exposes all rows to connected clients without authentication checks.
- **primary_key**: Designates the struct's primary key column (required for unique row identification).
- **index**: Creates B-tree indexes on specified columns for query optimization.

### Implementing Scheduled Tables

Scheduled tables trigger reducers automatically when their `scheduled_at` timestamp elapses. The following definition from the test suite demonstrates the `scheduled` argument linking a table to its handler reducer:

```rust
#[spacetimedb::table(
    accessor = scheduled_message,
    scheduled(send_scheduled_message), // Reducer invoked when schedule fires
    public
)]
pub struct ScheduledMessage {
    #[primary_key]
    #[auto_inc]                  // Auto-generated u64 identifier
    scheduled_id: u64,

    scheduled_at: spacetimedb::ScheduleAt,
    owner_identity: spacetimedb::Identity,
    payload: String,
}

#[spacetime::reducer]
fn send_scheduled_message(ctx: &ReducerContext, msg: ScheduledMessage) {
    // Business logic executes when scheduled_at is reached
    ctx.db.users().insert(Users {
        identity: msg.owner_identity,
        name: msg.payload,
    });
}

```

The `#[auto_inc]` attribute instructs the database to generate monotonically increasing IDs, while `spacetimedb::ScheduleAt` represents the timestamp type for scheduling.

### Bulk Definitions with define_tables!

For modules requiring multiple similar tables, the `define_tables!` macro in [`modules/sdk-test/src/lib.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/modules/sdk-test/src/lib.rs) (lines 77-89) provides a concise DSL that expands into complete table definitions and reducer functions:

```rust
define_tables! {
    // Simple table with auto-generated insert reducer
    OneU8 { insert insert_one_u8 } n u8;

    // Table with unique constraint and custom reducers for update/delete
    UniqueU32 {
        insert_or_panic insert_unique_u32,
        update_non_pk_by update_unique_u32 = update_by_n(n),
        delete_by delete_unique_u32 = delete_by_n(n: u32),
    } #[unique] n u32, data i32;
}

```

This macro generates structs annotated with `#[spacetimedb::table]` and implements the requested reducer signatures, supporting `#[unique]`, `#[primary_key]`, and `auto_inc` modifiers within the DSL syntax.

## Defining Tables in TypeScript

The TypeScript SDK provides a programmatic builder API that mirrors Rust's declarative attributes, producing identical SATS-JSON schemas at runtime.

### The table() Builder API

Located in [`crates/bindings-typescript/src/server/schema.ts`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/bindings-typescript/src/server/schema.ts), the `table()` function registers definitions with the server module. A real-world example from the chat application demo ([`tools/llm-oneshot/apps/chat-app/typescript/.../schema.ts`](https://github.com/clockworklabs/SpacetimeDB/blob/main/tools/llm-oneshot/apps/chat-app/typescript/.../schema.ts)) shows the equivalent Users table:

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

export const Users = table(
  { name: 'users', public: true },
  {
    identity: t.identity().primaryKey(),
    name: t.string().optional(),
    online: t.bool(),
    status: t.string(),
    lastActive: t.timestamp(),
  }
);

```

The first argument accepts metadata including `name`, `public` visibility, and optional `scheduled` reducer bindings. The second argument defines the column schema using type helpers.

### Column Type Helpers and Constraints

The `t` namespace exposes methods for all SpacetimeDB primitive types with chained constraints:

```typescript
export const ScheduledMessage = table(
  {
    name: 'scheduled_message',
    public: true,
    scheduled: 'send_scheduled_message',
  },
  {
    scheduledId: t.u64().primaryKey().autoInc(),
    scheduledAt: t.scheduleAt(),
    ownerIdentity: t.identity(),
    payload: t.string(),
  }
);

```

Available chainable methods include:
- **`.primaryKey()`**: Marks the column as the table's primary key.
- **`.autoInc()`**: Enables automatic increment for integer columns.
- **`.optional()`**: Makes the column nullable.
- **`.scheduleAt()`**: Uses the special timestamp type for scheduled reducers.

## Consuming Schemas in Client Bindings

When you run `spacetime generate`, the CLI reads the compiled module's schema and emits strongly-typed client code. The generated bindings expose table objects that match the server-side definitions exactly:

```typescript
import { spacetimedb } from './module_bindings';

// TypeScript compiler verifies field names and types against the schema
await spacetimedb.users.insert({
  identity: myIdentity,
  name: 'Alice',
  online: true,
  status: 'online',
  lastActive: new Date(),
});

// Schedule operations use helper methods corresponding to t.scheduleAt()
await spacetimedb.scheduled_message.insert({
  scheduledAt: spacetimedb.ScheduleAt.fromNow(10_000),
  ownerIdentity: myIdentity,
  payload: 'Hello future!',
});

```

The client SDK references the same `accessor` names defined in the Rust attributes or TypeScript `table()` calls, ensuring the API surface remains consistent across language boundaries.

## Summary

- **SpacetimeDB schema definitions** are authored in Rust using `#[spacetimedb::table]` attributes or the `define_tables!` macro, and in TypeScript using the `table()` builder API.
- **Core source files** implementing this functionality include [`modules/sdk-test/src/lib.rs`](https://github.com/clockworklabs/SpacetimeDB/blob/main/modules/sdk-test/src/lib.rs) for Rust macro examples and [`crates/bindings-typescript/src/server/schema.ts`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/bindings-typescript/src/server/schema.ts) for the TypeScript runtime.
- **Schema compilation** produces SATS-JSON that the server stores in system tables (`st_table`, `st_column`), enabling automatic migrations when modules are republished.
- **Advanced features** such as `primary_key`, `auto_inc`, `index`, and `scheduled` reducers are available in both languages through consistent APIs.
- **Client code generation** creates type-safe bindings that mirror the server schema, preventing runtime errors through compile-time verification.

## Frequently Asked Questions

### What is the difference between `#[spacetimedb::table]` and `define_tables!` in Rust?

The `#[spacetimedb::table]` attribute marks individual structs as database tables with specific configuration options for visibility, indexing, and keys. In contrast, `define_tables!` is a macro that expands a domain-specific language into multiple table definitions and their associated reducer functions simultaneously, primarily used in the test suite for rapidly creating schema variations.

### How do scheduled tables work in SpacetimeDB schema definitions?

Scheduled tables include a `scheduled` argument specifying a reducer name that executes automatically when the row's `scheduled_at` timestamp is reached. In Rust, use `scheduled(reducer_name)` in the table attribute with a column of type `spacetimedb::ScheduleAt`. In TypeScript, pass `scheduled: 'reducer_name'` to the `table()` options and use `t.scheduleAt()` for the column type. The server invokes the specified reducer with the row data when the schedule triggers.

### Can I modify table schemas after publishing a SpacetimeDB module?

Yes, SpacetimeDB supports **automatic schema migration** when you republish a module with modified table definitions. The server compares the new schema JSON against existing system table metadata and attempts to migrate data to the new structure. However, destructive changes (such as removing columns or changing primary keys) may require manual intervention or data_reset flags depending on the compatibility of the changes.

### Where are TypeScript schema definitions processed at runtime?

TypeScript schema definitions are processed by the `table()` and column helper functions defined in [`crates/bindings-typescript/src/server/schema.ts`](https://github.com/clockworklabs/SpacetimeDB/blob/main/crates/bindings-typescript/src/server/schema.ts). These functions build a JSON representation of the schema that the SpacetimeDB server consumes during module initialization, effectively translating the TypeScript builder pattern into the same SATS format generated by Rust macros.