Skip to main content

Column Types

Columns define the structure of your tables. SpacetimeDB supports primitive types, composite types for complex data, and special types for database-specific functionality.

Representing Collections

When modeling data that contains multiple items, you have two choices: store the collection as a column (using Vec, List, or Array) or store each item as a row in a separate table. This decision affects how you query, update, and subscribe to that data.

Use a collection column when:

  • The items form an atomic unit that you always read and write together
  • Order is semantically important and frequently accessed by position
  • The collection is small and bounded (e.g., a fixed-size inventory)
  • The items are values without independent identity

Use a separate table when:

  • Items have independent identity and lifecycle
  • You need to query, filter, or index individual items
  • The collection can grow unbounded
  • Clients should receive updates for individual item changes, not the entire collection
  • You want to enforce referential integrity between items and other data

Consider a game inventory with ordered pockets. A Vec<Item> preserves pocket order naturally, but if you need to query "all items owned by player X" across multiple players, a separate inventory_item table with a pocket_index column allows that query efficiently. The right choice depends on your dominant access patterns.

Binary Data and Files

SpacetimeDB includes optimizations for storing binary data as Vec<u8> (Rust), List<byte> (C#), or t.array(t.u8()) (TypeScript). You can store files, images, serialized data, or other binary blobs directly in table columns.

This approach works well when:

  • The binary data is associated with a specific row (e.g., a user's avatar image)
  • You want the data to participate in transactions and subscriptions
  • The data size is reasonable (up to several megabytes per row)

For very large files or data that changes independently of other row fields, consider external storage with a reference stored in the table.

Type Performance

SpacetimeDB optimizes reading and writing by taking advantage of memory layout. Several factors affect performance:

Prefer smaller types. Use the smallest integer type that fits your data range. A u8 storing values 0-255 uses less memory and bandwidth than a u64 storing the same values. This reduces storage, speeds up serialization, and improves cache efficiency.

Prefer fixed-size types. Fixed-size types (u32, f64, fixed-size structs) allow SpacetimeDB to compute memory offsets directly. Variable-size types (String, Vec<T>) require additional indirection. When performance matters, consider fixed-size alternatives:

  • Use [u8; 32] instead of Vec<u8> for fixed-length hashes or identifiers
  • Use an enum with a fixed set of variants instead of a String for categorical data

Consider column ordering. Types require alignment in memory. A u64 aligns to 8-byte boundaries, while a u8 aligns to 1-byte boundaries. When smaller types precede larger ones, the compiler may insert padding bytes to satisfy alignment requirements. Ordering columns from largest to smallest alignment can reduce padding and improve memory density.

For example, a struct with fields (u8, u64, u8) may require 24 bytes due to padding, while (u64, u8, u8) requires only 16 bytes. This optimization is not something to follow religiously, but it can help performance in memory-intensive scenarios.

These optimizations apply across all supported languages.

Type Reference

CategoryTypeTypeScript TypeDescription
Primitivet.bool()booleanBoolean value
Primitivet.string()stringUTF-8 string
Primitivet.f32()number32-bit floating point
Primitivet.f64()number64-bit floating point
Primitivet.i8()numberSigned 8-bit integer
Primitivet.u8()numberUnsigned 8-bit integer
Primitivet.i16()numberSigned 16-bit integer
Primitivet.u16()numberUnsigned 16-bit integer
Primitivet.i32()numberSigned 32-bit integer
Primitivet.u32()numberUnsigned 32-bit integer
Primitivet.i64()bigintSigned 64-bit integer
Primitivet.u64()bigintUnsigned 64-bit integer
Primitivet.i128()bigintSigned 128-bit integer
Primitivet.u128()bigintUnsigned 128-bit integer
Primitivet.i256()bigintSigned 256-bit integer
Primitivet.u256()bigintUnsigned 256-bit integer
Compositet.object(name, obj){ [K in keyof Obj]: T<Obj[K]> }Product/object type for nested data
Compositet.enum(name, variants){ tag: 'variant' } | { tag: 'variant', value: T }Sum/enum type (tagged union)
Compositet.array(element)T<Element>[]Array of elements
Compositet.option(value)Value | undefinedOptional value
Compositet.unit(){}Zero-field product type
Specialt.identity()IdentityUnique identity for authentication
Specialt.connectionId()ConnectionIdClient connection identifier
Specialt.timestamp()TimestampAbsolute point in time (microseconds since Unix epoch)
Specialt.timeDuration()TimeDurationRelative duration in microseconds
Specialt.scheduleAt()ScheduleAtColumn type for scheduling reducer execution

Complete Example

The following example demonstrates a table using primitive, composite, and special types:

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

// Define a nested object type for coordinates
const Coordinates = t.object('Coordinates', {
  x: t.f64(),
  y: t.f64(),
  z: t.f64(),
});

// Define an enum for status
const Status = t.enum('Status', {
  Active: t.unit(),
  Inactive: t.unit(),
  Suspended: t.object('SuspendedInfo', { reason: t.string() }),
});

const player = table(
  { name: 'player', public: true },
  {
    // Primitive types
    id: t.u64().primaryKey().autoInc(),
    name: t.string(),
    level: t.u8(),
    experience: t.u32(),
    health: t.f32(),
    score: t.i64(),
    is_online: t.bool(),

    // Composite types
    position: Coordinates,
    status: Status,
    inventory: t.array(t.u32()),
    guild_id: t.option(t.u64()),

    // Special types
    owner: t.identity(),
    connection: t.option(t.connectionId()),
    created_at: t.timestamp(),
    play_time: t.timeDuration(),
  }
);