---
description: "When migrating SpacetimeDB 1.0 code to 2.0. Covers breaking changes: reducer callbacks → event tables, name→accessor, sender() method, update-only-via-primary-key."
globs: **/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.rs,**/*.cs
alwaysApply: false
---
# SpacetimeDB 1.0 → 2.0 Migration Rules

Apply this when migrating existing SpacetimeDB 1.0 modules or clients to 2.0.

## Overview of Breaking Changes

1. **Reducer callbacks removed** — use event tables + `on_insert` instead
2. **`light_mode` removed** — no longer necessary
3. **`CallReducerFlags` / `NoSuccessNotify` removed**
4. **Table/index: `name` → `accessor`** — `name` is now for canonical SQL name only
5. **Client: `with_module_name` → `with_database_name`**
6. **Rust: `ctx.sender` → `ctx.sender()`** (method, not field)
7. **Only primary key indexes have `update`** — use primary key accessor for updates, or delete+insert

---

## Reducer Callbacks → Event Tables

### ❌ 1.0 — REMOVED

```rust
// Server: reducer args were broadcast (no longer)
// Client:
conn.reducers.on_deal_damage(|ctx, target, amount| { ... });
```

```csharp
conn.Reducers.OnDealDamage += (ctx, target, amount) => { ... };
```

### ✅ 2.0 — Event Table Pattern

**Server:** Define event table, insert from reducer.

```rust
use spacetimedb::{table, reducer, ReducerContext, Table, Identity};

#[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 });
}
```

```csharp
[SpacetimeDB.Table(Accessor = "DamageEvent", Public = true, Event = true)]
public partial struct DamageEvent { public Identity Target; public uint Amount; }

[SpacetimeDB.Reducer]
public static void DealDamage(ReducerContext ctx, Identity target, uint amount) {
    ctx.Db.DamageEvent.Insert(new DamageEvent { Target = target, Amount = amount });
}
```

**Client:** Subscribe to event table, use `on_insert`.

```rust
conn.db.damage_event().on_insert(|ctx, event| {
    play_damage_animation(event.target, event.amount);
});
```

```csharp
conn.Db.DamageEvent.OnInsert += (ctx, damageEvent) => {
    PlayDamageAnimation(damageEvent.Target, damageEvent.Amount);
};
```

**Event tables must be subscribed explicitly** — they are excluded from `subscribe_to_all_tables()`.

**TypeScript:** Use `reducer(params, fn)` with `export const name = ...` — never `reducer('name', params, fn)`.

```typescript
// ❌ 1.0 — NO LONGER VALID
spacetimedb.reducer('deal_damage', { target: t.identity(), amount: t.u32() }, (ctx, { target, amount }) => { ... });

// ✅ 2.0 — name from export
export const deal_damage = spacetimedb.reducer({ target: t.identity(), amount: t.u32() }, (ctx, { target, amount }) => {
  ctx.db.damage_event.insert({ target, amount });
});
```

---

## TypeScript: schema, reducer, scheduled

### schema() — object only
```typescript
// ❌ 1.0 — NO LONGER VALID
const spacetimedb = schema(myTable);
const spacetimedb = schema(t1, t2, t3);

// ✅ 2.0 — single object argument
const moduleSchema = schema({ myTable });
export default moduleSchema;
// or: const moduleSchema = schema({ t1, t2, t3 });
```

### scheduled tables
```typescript
// ❌ 1.0 — string reducer name
scheduled: 'run_cleanup'

// ✅ 2.0 — function returning exported reducer
export const run_cleanup = spacetimedb.reducer({ arg: CleanupJob.rowType }, (ctx, { arg }) => { ... });
// In table options:
scheduled: () => run_cleanup
```

---

## Table/Index: `name` → `accessor`

### ❌ 1.0 — NO LONGER VALID

```rust
#[table(name = my_table, public, index(name = position, btree(columns = [x, y])))]
```

```csharp
[SpacetimeDB.Table(Name = "MyTable", Public = true)]
[SpacetimeDB.Index.BTree(Name = "Position", Columns = new[] { nameof(X), nameof(Y) })]
```

```typescript
// schema(myTable) or schema(t1, t2) — NO LONGER VALID
```

### ✅ 2.0

```rust
#[table(accessor = my_table, public, index(accessor = position, btree(columns = [x, y])))]
```

```csharp
[SpacetimeDB.Table(Accessor = "MyTable", Public = true)]
[SpacetimeDB.Index.BTree(Accessor = "Position", Columns = new[] { nameof(X), nameof(Y) })]
```

```typescript
// schema({ myTable }) — object keys become accessors
const spacetimedb = schema({ myTable });
export default spacetimedb;
```

- `accessor` = API names in module/client code
- `name` = optional override for canonical SQL name (for existing DB compatibility)

---

## Client Connection: `with_module_name` → `with_database_name`

### ❌ 1.0

```rust
.with_module_name("my-database")
```

```csharp
.WithModuleName("my-database")
```

### ✅ 2.0

```rust
.with_database_name("my-database")
```

```csharp
.WithDatabaseName("my-database")
```

---

## Rust: `ctx.sender` → `ctx.sender()`

### ❌ 1.0

```rust
let sender_identity = ctx.sender;
```

### ✅ 2.0

```rust
let sender_identity = ctx.sender();
```

---

## Only Primary Key Has `update`

### ❌ 1.0 — REMOVED (unique index update)

```rust
ctx.db.user().name().update(User { ... });
```

```csharp
ctx.Db.User.Name.Update(new User { ... });
```

### ✅ 2.0 — Update via primary key accessor

```rust
ctx.db.user().identity().update(User { ... });
```

```csharp
ctx.Db.User.Identity.Update(new User { ... });
```

If the primary key value changes, use **delete + insert** instead of update.

---

## Per-Call Result Callbacks

If you only need the result of a reducer **you** called, use `_then()`:

```rust
ctx.reducers.deal_damage_then(target, amount, |ctx, result| {
    match result {
        Ok(Ok(())) => println!("succeeded"),
        Ok(Err(err)) => println!("failed: {err}"),
        Err(internal) => println!("error: {internal:?}"),
    }
}).unwrap();
```

---

## Existing 1.0 Database Compatibility

To avoid table name changes when republishing to an existing DB, set case conversion to `None`:

```rust
use spacetimedb::CaseConversionPolicy;

#[spacetimedb::settings]
const CASE_CONVERSION_POLICY: CaseConversionPolicy = CaseConversionPolicy::None;
```

```csharp
[SpacetimeDB.Settings]
public const SpacetimeDB.CaseConversionPolicy CASE_CONVERSION_POLICY =
    SpacetimeDB.CaseConversionPolicy.None;
```

Or specify `name = "ExactTableName"` on each table to preserve canonical names.
