Constraints
Constraints enforce data integrity rules on your tables. SpacetimeDB supports primary key and unique constraints.
Primary Keys
A primary key uniquely identifies each row in a table. It represents the identity of a row and determines how updates and deletes are handled.
- TypeScript
- C#
- Rust
- C++
import { table, t } from 'spacetimedb/server';
const user = table(
{ name: 'user', public: true },
{
id: t.u64().primaryKey(),
name: t.string(),
email: t.string(),
}
);Use the .primaryKey() method on a column builder to mark it as the primary key.
[SpacetimeDB.Table(Accessor = "User", Public = true)]
public partial struct User
{
[SpacetimeDB.PrimaryKey]
public ulong Id;
public string Name;
public string Email;
}Use the [SpacetimeDB.PrimaryKey] attribute to mark a field as the primary key.
#[spacetimedb::table(accessor = user, public)]
pub struct User {
#[primary_key]
id: u64,
name: String,
email: String,
}Use the #[primary_key] attribute to mark a field as the primary key.
C++ support is currently in beta and subject to change. SpacetimeDB C++ 2.0 is coming soon, but C++ server modules are currently pinned to v1.12.0. If you are following the C++ tab in this guide, use the v1.12.0 release track for now.
struct User {
uint64_t id;
std::string name;
std::string email;
};
SPACETIMEDB_STRUCT(User, id, name, email)
SPACETIMEDB_TABLE(User, user, Public)
FIELD_PrimaryKey(user, id)Use FIELD_PrimaryKey(table, field) after table registration to mark the primary key.
Primary Key Rules
- One per table: A table can have at most one primary key column.
- Immutable identity: The primary key defines the row's identity. Changing a primary key value is treated as deleting the old row and inserting a new one.
- Unique by definition: Primary keys are automatically unique. No two rows can have the same primary key value.
Because of the unique constraint, SpacetimeDB implements primary keys using a unique index. This index is created automatically.
Multi-Column Primary Keys
SpacetimeDB does not yet support multi-column (composite) primary keys. If you need to look up rows by multiple columns, use a multi-column btree index combined with an auto-increment primary key:
- TypeScript
- C#
- Rust
- C++
const inventory = table(
{
name: 'inventory',
public: true,
indexes: [
{ name: 'by_user_item', algorithm: 'btree', columns: ['userId', 'itemId'] },
],
},
{
id: t.u64().primaryKey().autoInc(),
userId: t.u64(),
itemId: t.u64(),
quantity: t.u32(),
}
);[SpacetimeDB.Table(Accessor = "Inventory", Public = true)]
[SpacetimeDB.Index.BTree(Accessor = "by_user_item", Columns = new[] { nameof(UserId), nameof(ItemId) })]
public partial struct Inventory
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public ulong Id;
public ulong UserId;
public ulong ItemId;
public uint Quantity;
}#[spacetimedb::table(accessor = inventory, public, index(accessor = inventory_index, btree(columns = [user_id, item_id])))]
pub struct Inventory {
#[primary_key]
#[auto_inc]
id: u64,
user_id: u64,
item_id: u64,
quantity: u32,
}struct Inventory {
uint64_t id;
uint64_t user_id;
uint64_t item_id;
uint32_t quantity;
};
SPACETIMEDB_STRUCT(Inventory, id, user_id, item_id, quantity)
SPACETIMEDB_TABLE(Inventory, inventory, Public)
FIELD_PrimaryKeyAutoInc(inventory, id)
// Named multi-column btree index on (user_id, item_id)
FIELD_NamedMultiColumnIndex(inventory, by_user_item, user_id, item_id)This gives you efficient lookups by the column combination while using a simple auto-increment value as the primary key.
Updates and Primary Keys
When you update a row, SpacetimeDB uses the primary key to determine whether it's a modification or a replacement:
- Same primary key: The row is updated in place. Subscribers see an update event.
- Different primary key: The old row is deleted and a new row is inserted. Subscribers see a delete event followed by an insert event.
- TypeScript
- C#
- Rust
- C++
export const update_user_name = spacetimedb.reducer({ id: t.u64(), newName: t.string() }, (ctx, { id, newName }) => {
const user = ctx.db.user.id.find(id);
if (user) {
// This is an update — primary key (id) stays the same
ctx.db.user.id.update({ ...user, name: newName });
}
});[SpacetimeDB.Reducer]
public static void UpdateUserName(ReducerContext ctx, ulong id, string newName)
{
var user = ctx.Db.User.Id.Find(id);
if (user != null)
{
// This is an update — primary key (Id) stays the same
user.Name = newName;
ctx.Db.User.Id.Update(user);
}
}#[spacetimedb::reducer]
fn update_user_name(ctx: &ReducerContext, id: u64, new_name: String) -> Result<(), String> {
if let Some(mut user) = ctx.db.user().id().find(id) {
// This is an update — primary key (id) stays the same
user.name = new_name;
ctx.db.user().id().update(user);
}
Ok(())
}SPACETIMEDB_REDUCER(update_user_name, ReducerContext ctx, uint64_t id, std::string new_name) {
auto user_opt = ctx.db[user_id].find(id);
if (user_opt.has_value()) {
User user_update = user_opt.value();
user_update.name = new_name;
ctx.db[user_id].update(user_update);
}
return Ok();
}Tables Without Primary Keys
Tables don't require a primary key. Without one, the entire row acts as the primary key:
- Rows are identified by their complete content
- Updates require matching all fields
- Duplicate rows are not possible. Inserting an identical row has no effect
SpacetimeDB always maintains set semantics regardless of whether you define a primary key. The difference is what defines uniqueness: a primary key column, or the entire row.
Primary keys add indexing overhead. If your table is only accessed by iterating over all rows (no lookups by key), omitting the primary key can improve performance.
Common Primary Key Patterns
Auto-incrementing IDs: Combine primaryKey() with autoInc() for automatically assigned unique identifiers:
#[spacetimedb::table(accessor = post, public)]
pub struct Post {
#[primary_key]
#[auto_inc]
id: u64,
title: String,
content: String,
}
Identity as primary key: Use the caller's identity as the primary key for user-specific data:
#[spacetimedb::table(accessor = user_profile, public)]
pub struct UserProfile {
#[primary_key]
identity: Identity,
display_name: String,
bio: String,
}
This pattern ensures each identity can only have one profile and makes lookups by identity efficient.
Unique Columns
Mark columns as unique to ensure no two rows can have the same value for that column.
- TypeScript
- C#
- Rust
- C++
const user = table(
{ name: 'user', public: true },
{
id: t.u32().primaryKey(),
email: t.string().unique(),
username: t.string().unique(),
}
);Use the .unique() method on a column builder.
[SpacetimeDB.Table(Accessor = "User", Public = true)]
public partial struct User
{
[SpacetimeDB.PrimaryKey]
public uint Id;
[SpacetimeDB.Unique]
public string Email;
[SpacetimeDB.Unique]
public string Username;
}Use the [SpacetimeDB.Unique] attribute.
#[spacetimedb::table(accessor = user, public)]
pub struct User {
#[primary_key]
id: u32,
#[unique]
email: String,
#[unique]
username: String,
}Use the #[unique] attribute.
struct User {
uint32_t id;
std::string email;
std::string username;
};
SPACETIMEDB_STRUCT(User, id, email, username)
SPACETIMEDB_TABLE(User, user, Public)
FIELD_PrimaryKey(user, id)
FIELD_Unique(user, email)
FIELD_Unique(user, username)Use FIELD_Unique(table, field) after table registration to mark columns as unique.
Unlike primary keys, you can have multiple unique columns on a single table. Unique columns also create an index that enables efficient lookups.
Primary Keys vs Unique Columns
Both primary keys and unique columns enforce uniqueness, but they serve different purposes:
| Aspect | Primary Key | Unique Column |
|---|---|---|
| Purpose | Row identity | Data integrity |
| Count per table | One | Multiple allowed |
| Update behavior | Delete + Insert | In-place update |
| Required | No | No |