---
title: Migrating from 1.0 to 2.0
slug: /upgrade
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Migrating from SpacetimeDB 1.0 to 2.0

This guide covers the breaking changes between SpacetimeDB 1.0 and 2.0 and how to update your code.

## Overview of Changes

SpacetimeDB 2.0 introduces a new WebSocket protocol (v2) and SDK with several breaking changes aimed at simplifying the programming model and improving security:

1. **Reducer callbacks removed** -- replaced by event tables and per-call `_then()` callbacks
2. **`light_mode` removed** -- no longer necessary since reducer events are no longer broadcast
3. **`CallReducerFlags` removed** -- `NoSuccessNotify` and `set_reducer_flags()` are gone
4. **Event tables introduced** -- a new table type for publishing transient events to subscribers
5. **Confirmed reads enabled by default** -- subscription updates and SQL results are only sent after the transaction is confirmed durable

## Reducer Callbacks

### What changed

In 1.0, you could register global callbacks on reducers that would fire whenever *any* client called that reducer and you were subscribed to affected rows:

<Tabs groupId="client-language" queryString>
<TabItem value="typescript" label="TypeScript">

```typescript
// 1.0 -- REMOVED in 2.0
conn.reducers.onDealDamage((ctx, { target, amount }) => {
  console.log(`Someone called dealDamage with args: (${target}, ${amount})`);
});
```

</TabItem>
<TabItem value="csharp" label="C#">

```csharp
// 1.0-style global reducer callback semantics (no longer true in 2.0)
conn.Reducers.OnDealDamage += (ctx, target, amount) =>
{
    Console.WriteLine($"Someone called DealDamage with args: ({target}, {amount})");
};
```

</TabItem>
<TabItem value="rust" label="Rust">

```rust
// 1.0 -- REMOVED in 2.0
conn.reducers.on_deal_damage(|ctx, target, amount| {
    println!("Someone called deal_damage with args: ({target}, {amount})");
});
```

</TabItem>
<TabItem value="cpp-unreal" label="Unreal C++">

```cpp
// 1.0-style global reducer callback semantics (no longer true in 2.0)
UFUNCTION()
void OnDealDamage(const FReducerEventContext& Context, const FSpacetimeDBIdentity& Target, int32 Amount)
{
    UE_LOG(LogTemp, Log, TEXT("Someone called DealDamage with args: (%s, %d)"), *Target.ToString(), Amount);
}

Conn->Reducers->OnDealDamage.AddDynamic(this, &AMyActor::OnDealDamage);
```

</TabItem>
</Tabs>

In 2.0, global reducer callbacks no longer exist. The server does not broadcast reducer argument data to other clients. Instead, you have two options:

### Option A: Per-call result callbacks (`_then()`)

If you only need to know the result of a reducer *you* called, you can await the result or use the `_then()` variant:

<Tabs groupId="client-language" queryString>
<TabItem value="typescript" label="TypeScript">

```typescript
try {
  await ctx.reducers.dealDamage({ target, amount });
  console.log(`You called dealDamage with args: (${target}, ${amount})`);
} catch (err) {
  if (err instanceof SenderError) {
    console.log(`You made an error: ${err}`)
  } else if (err instanceof InternalError) {
    console.log(`The server had an error: ${err}`);
  }
}
```

</TabItem>
<TabItem value="csharp" label="C#">

```csharp
// 2.0 -- per-call callback on the calling connection
conn.Reducers.OnDealDamage += (ctx, _, _) =>
{
    if (ctx.Event.Status is Status.Committed)
    {
        Console.WriteLine("Reducer succeeded");
    }
    else if (ctx.Event.Status is Status.Failed failed)
    {
        Console.WriteLine($"Reducer failed: {failed}");
    }
    else if (ctx.Event.Status is Status.OutOfEnergy)
    {
        Console.WriteLine("Reducer failed: out of energy");
    }
};

conn.Reducers.DealDamage(target, amount);
```

</TabItem>
<TabItem value="rust" label="Rust">

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

The fire-and-forget form still works:

```rust
// 2.0 -- fire and forget (unchanged)
ctx.reducers.deal_damage(target, amount).unwrap();
```

</TabItem>
<TabItem value="cpp-unreal" label="Unreal C++">

```cpp
// 2.0 -- per-call callback on the calling connection
UFUNCTION()
void OnDealDamage(const FReducerEventContext& Context, const FSpacetimeDBIdentity& Target, int32 Amount)
{
    if (Context.Event.Status.IsCommitted())
    {
        UE_LOG(LogTemp, Log, TEXT("Reducer succeeded"));
    }
    else if (Context.Event.Status.IsFailed())
    {
        UE_LOG(LogTemp, Error, TEXT("Reducer failed: %s"), *Context.Event.Status.GetAsFailed());
    }
    else if (Context.Event.Status.IsOutOfEnergy())
    {
        UE_LOG(LogTemp, Error, TEXT("Reducer failed: out of energy"));
    }
}

Conn->Reducers->OnDealDamage.AddDynamic(this, &AMyActor::OnDealDamage);
Conn->Reducers->DealDamage(Target, Amount);
```

In Unreal, there is no `_then()` method. The generated `On<Reducer>` delegate is correlated by `request_id` and only fires on the connection that called the reducer.

</TabItem>
</Tabs>

### Option B: Event tables (recommended for most use cases)

If you need *other* clients to observe that something happened (the primary use case for 1.0 reducer callbacks), create an event table and insert into it from your reducer.

<Tabs groupId="server-language" queryString>
<TabItem value="typescript" label="TypeScript">

**Server (module) -- before:**
```typescript
// 1.0 -- NO LONGER VALID in 2.0 (reducer args were automatically broadcast)
spacetimedb.reducer('deal_damage', { target: t.identity(), amount: t.u32() }, (ctx, { target, amount }) => {
  // update game state
});
```

**Server (module) -- after:**
```typescript
// 2.0 server -- explicitly publish events via an event table
const damageEvent = table({ event: true }, {
    target: t.identity(),
    amount: t.u32(),
})
// schema() takes an object: schema({ damageEvent }), never schema(damageEvent)
const spacetimedb = schema({ damageEvent });

export const dealDamage = spacetimedb.reducer({ target: t.identity(), amount: t.u32() }, (ctx, { target, amount }) => {
  ctx.db.damageEvent.insert({ target, amount });
});
```

</TabItem>
<TabItem value="csharp" label="C#">

**Server (module) -- before:**
```csharp
// 1.0 -- NO LONGER VALID in 2.0 (reducer args were automatically broadcast)
[SpacetimeDB.Reducer]
public static void DealDamage(ReducerContext ctx, Identity target, uint amount)
{
    // update game state...
}
```

**Server (module) -- after:**
```csharp
// 2.0 server -- explicitly publish events via an event table
[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)
{
    // update game state...

    ctx.Db.DamageEvent.Insert(new DamageEvent
    {
        Target = target,
        Amount = amount,
    });
}
```

</TabItem>
<TabItem value="rust" label="Rust">

**Server (module) -- before:**
```rust
// 1.0 -- NO LONGER VALID in 2.0 (reducer args were automatically broadcast)
#[spacetimedb::reducer]
fn deal_damage(ctx: &ReducerContext, target: Identity, amount: u32) {
    // update game state...
}
```

**Server (module) -- after:**
```rust
// 2.0 server -- explicitly publish events via an event table
use spacetimedb::{table, reducer, ReducerContext, Table, Identity};

#[spacetimedb::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) {
    // update game state...

    ctx.db.damage_event().insert(DamageEvent { target, amount });
}
```

</TabItem>
<TabItem value="cpp" label="C++">

**Server (module) -- before:**
```cpp
// 1.0-style reducer arguments were effectively observable through reducer callbacks
SPACETIMEDB_REDUCER(deal_damage, ReducerContext ctx, Identity target, uint32_t amount) {
    // update game state...
    return Ok();
}
```

**Server (module) -- after:**
```cpp
// 2.0 server -- explicitly publish events via an event table
#include <spacetimedb.h>
using namespace SpacetimeDB;

struct DamageEvent {
    Identity target;
    uint32_t amount;
};
SPACETIMEDB_STRUCT(DamageEvent, target, amount);
SPACETIMEDB_TABLE(DamageEvent, damage_event, Public);

SPACETIMEDB_REDUCER(deal_damage, ReducerContext ctx, Identity target, uint32_t amount) {
    // update game state...

    ctx.db[damage_event].insert(DamageEvent{target, amount});
    return Ok();
}
```

</TabItem>
</Tabs>

<Tabs groupId="client-language" queryString>
<TabItem value="typescript" label="TypeScript">

**Client -- before:**
```typescript
// 1.0 -- NO LONGER VALID in 2.0 (global reducer callback)
conn.reducers.onDealDamage((ctx, { target, amount }) => {
    playDamageAnimation(target, amount);
});
```

**Client -- after:**
```typescript
// 2.0 client -- event table callback
// Note that although this callback fires, the `damageEvent`
// table will never have any rows present in the client cache
conn.db.damageEvent().onInsert((ctx, { target, amount }) => {
    playDamageAnimation(target, amount);
});
```

</TabItem>
<TabItem value="csharp" label="C#">

**Client -- before:**
```csharp
// 1.0 -- NO LONGER VALID in 2.0 (global reducer callback)
conn.Reducers.OnDealDamage += (ctx, target, amount) =>
{
    PlayDamageAnimation(target, amount);
};
```

**Client -- after:**
```csharp
// 2.0 client -- event table callback
conn.Db.DamageEvent.OnInsert += (ctx, damageEvent) =>
{
    PlayDamageAnimation(damageEvent.Target, damageEvent.Amount);
};
```

</TabItem>
<TabItem value="rust" label="Rust">

**Client -- before:**
```rust
// 1.0 -- NO LONGER VALID in 2.0 (global reducer callback)
conn.reducers.on_deal_damage(|ctx, target, amount| {
    play_damage_animation(target, amount);
});
```

**Client -- after:**
```rust
// 2.0 client -- event table callback
// Note that although this callback fires, the `damage_event`
// table will never have any rows present in the client cache
conn.db.damage_event().on_insert(|ctx, event| {
    play_damage_animation(event.target, event.amount);
});
```

</TabItem>
<TabItem value="cpp-unreal" label="Unreal C++">

**Client -- before:**
```cpp
// 1.0 -- NO LONGER VALID in 2.0 (global reducer callback)
UFUNCTION()
void OnDealDamage(const FReducerEventContext& Context, const FSpacetimeDBIdentity& Target, int32 Amount)
{
    PlayDamageAnimation(Target, Amount);
}

Conn->Reducers->OnDealDamage.AddDynamic(this, &AMyActor::OnDealDamage);
```

**Client -- after:**
```cpp
// 2.0 client -- event table callback
// Note that although this callback fires, the `DamageEvent`
// table will never have any rows present in the client cache
UFUNCTION()
void OnDamageEvent(const FEventContext& Context, const FDamageEventType& DamageEvent)
{
    PlayDamageAnimation(DamageEvent.Target, DamageEvent.Amount);
}

Conn->Db->DamageEvent->OnInsert.AddDynamic(this, &AMyActor::OnDamageEvent);

Conn->SubscriptionBuilder()
    ->OnApplied(OnAppliedDelegate)
    ->OnError(OnErrorDelegate)
    ->Subscribe({ TEXT("SELECT * FROM damage_event") });
```

</TabItem>
</Tabs>


### Why event tables are better

- **Security**: You control exactly what data is published. In 1.0, reducer arguments were broadcast to any subscriber of affected rows, which could accidentally leak sensitive data.
- **Flexibility**: Multiple reducers can insert the same event type. In 1.0, events were tied 1:1 to a specific reducer.
- **Transactional**: Events are only published if the transaction commits. In 1.0, workarounds using scheduled reducers were not transactional.
- **Row-level security**: RLS rules apply to event tables, so you can control which clients see which events.
- **Queryable**: Event tables can be subscribed to with query builders (or SQL), and can be filtered per-client.

### Event table details

- Event tables are always empty outside of a transaction. They don't accumulate rows.
- On the client, `count()` always returns 0 and `iter()` is always empty.
- Only `on_insert` callbacks are generated (no `on_delete` or `on_update`).
- The `event` keyword in `#[table(..., event)]` marks the table as transient.
- Event tables must be subscribed to explicitly (they are excluded from `subscribeToAllTables` / `SubscribeToAllTables` / `subscribe_to_all_tables`).

## Event Type Changes

### What changed

<Tabs groupId="client-language" queryString>
<TabItem value="typescript" label="TypeScript">

In 1.0, table callbacks received `{ tag: 'Reducer'; value: ReducerEvent<Reducer> }` with full reducer information when a reducer caused a table change. Non-callers could also receive `{ tag: 'UnknownTransaction' }`.

In 2.0, the event model is simplified:

- **The caller** sees `{ tag: 'Reducer'; value: ReducerEvent<Reducer> }` with `type ReducerEvent = { timestamp, status, reducer }` in response to their own reducer calls.
- **Other clients** see `{ tag: 'Transaction' }` (no reducer details).
- `{ tag: 'UnknownTransaction' }` is removed.

```typescript
// 2.0 -- checking who caused a table change
conn.db.myTable().onInsert((ctx, row) => {
  if (ctx.event.tag === 'Reducer') {
    // This client called the reducer that caused this insert.
    console.log(`Our reducer: ${ctx.event.value.reducer}`);
  }
  if (ctx.event.tag === 'Transaction') {
    // Another client's action caused this insert.
  }
});
```

</TabItem>
<TabItem value="csharp" label="C#">

In 1.0, table callbacks could receive `Event<Reducer>.Reducer` with reducer information when a reducer caused a table change. Non-callers could also receive `Event<Reducer>.UnknownTransaction`.

In 2.0, for known reducer updates:

- **The caller** sees `Event<Reducer>.Reducer` with `ReducerEvent { Timestamp, Status, Reducer }`.
- **Other clients** see `Event<Reducer>.Transaction` (no reducer details).

```csharp
// 2.0 -- checking who caused a table change
conn.Db.Person.OnInsert += (ctx, row) =>
{
    if (ctx.Event is Event<Reducer>.Reducer(var reducerEvent))
    {
        // This client called the reducer that caused this insert.
        Console.WriteLine($"Our reducer: {reducerEvent.Reducer}");
    }
    else if (ctx.Event is Event<Reducer>.Transaction)
    {
        // Another client's action caused this insert.
    }
};
```

</TabItem>
<TabItem value="rust" label="Rust">

In 1.0, table callbacks received `Event::Reducer` with full reducer information when a reducer caused a table change. Non-callers could also receive `Event::UnknownTransaction`.

In 2.0, the event model is simplified:

- **The caller** sees `Event::Reducer` with `ReducerEvent { timestamp, status, reducer }` in response to their own reducer calls.
- **Other clients** see `Event::Transaction` (no reducer details).
- `Event::UnknownTransaction` is removed.

```rust
// 2.0 -- checking who caused a table change
conn.db.my_table().on_insert(|ctx, row| {
    match &ctx.event {
        Event::Reducer(reducer_event) => {
            // This client called the reducer that caused this insert.
            println!("Our reducer: {:?}", reducer_event.reducer);
        }
        Event::Transaction => {
            // Another client's action caused this insert.
        }
        _ => {}
    }
});
```

</TabItem>
<TabItem value="cpp-unreal" label="Unreal C++">

In 1.0, table callbacks could receive a reducer event with full reducer information when a reducer caused a table change. In 2.0:

- **The caller** sees `Context.Event.IsReducer()` with `FReducerEvent { Timestamp, Status, Reducer }`.
- **Other clients** see `Context.Event.IsTransaction()` with no reducer details.

```cpp
// 2.0 -- checking who caused a table change
UFUNCTION()
void OnPersonInsert(const FEventContext& Context, const FPersonType& Row)
{
    if (Context.Event.IsReducer())
    {
        const auto ReducerEvent = Context.Event.GetAsReducer();
        UE_LOG(LogTemp, Log, TEXT("Our reducer: %s"), *ReducerEvent.ReducerName);
    }
    else if (Context.Event.IsTransaction())
    {
        // Another client's action caused this insert.
    }
}
```

</TabItem>
</Tabs>

If you need metadata about reducers invoked by other clients, update your reducer code to emit an event using an event table.

## Subscription API

In 2.0, the subscription API is largely the same, but you can now subscribe to the database with a typed query builder:

<Tabs groupId="client-language" queryString>
<TabItem value="typescript" label="TypeScript">

```typescript
// 1.0 -- NO LONGER VALID in 2.0
ctx.subscriptionBuilder()
  .onApplied(ctx => { /* ... */ })
  .onError((ctx, err) => { /* ... */ })
  .subscribe(["SELECT * FROM my_table"]);
```

```typescript
// 2.0 -- Typed query builder
import { tables } from './module_bindings';
ctx.subscriptionBuilder()
  .onApplied(ctx => { /* ... */ })
  .onError((ctx, err) => { /* ... */ })
  .subscribe([tables.myTable]);
```

</TabItem>
<TabItem value="csharp" label="C#">

```csharp
// 2.0 -- same as 1.0
conn.SubscriptionBuilder()
    .OnApplied(_ => { /* ... */ })
    .OnError((_, error) => { /* ... */ })
    .AddQuery(q => q.From.Person())
    .Subscribe();
```

</TabItem>
<TabItem value="rust" label="Rust">

```rust
// 2.0 -- same as 1.0
ctx.subscription_builder()
    .on_applied(|ctx| { /* ... */ })
    .on_error(|ctx, error| { /* ... */ })
    .add_query(|q| q.from.my_table())
    .subscribe();
```

</TabItem>
<TabItem value="cpp-unreal" label="Unreal C++">

```cpp
// 2.0 -- same as 1.0 today
Conn->SubscriptionBuilder()
    ->OnApplied(OnAppliedDelegate)
    ->OnError(OnErrorDelegate)
    ->Subscribe({ TEXT("SELECT * FROM person") });
```

The Unreal SDK does not expose typed query builders yet. For now, use SQL strings. Typed query builder support is planned.

</TabItem>
</Tabs>

Note that subscribing to event tables requires an explicit query:

<Tabs groupId="client-language" queryString>
<TabItem value="typescript" label="TypeScript">

```typescript
// Event tables are excluded from subscribe_to_all_tables(), so subscribe explicitly:
import { tables } from "./module_bindings";
ctx.subscriptionBuilder()
    .onApplied((ctx) => { /* ... */ })
    .subscribe([tables.damageEvent]);
```

</TabItem>
<TabItem value="csharp" label="C#">

```csharp
// Subscribe explicitly to an event table:
conn.SubscriptionBuilder()
    .OnApplied(_ => { /* ... */ })
    .AddQuery(q => q.From.DamageEvent())
    .Subscribe();
```

</TabItem>
<TabItem value="rust" label="Rust">

```rust
// Event tables are excluded from subscribe_to_all_tables(), so subscribe explicitly:
ctx.subscription_builder()
    .on_applied(|ctx| { /* ... */ })
    .add_query(|q| q.from.damage_event())
    .subscribe();
```

</TabItem>
<TabItem value="cpp-unreal" label="Unreal C++">

```cpp
// Event tables are excluded from SubscribeToAllTables(), so subscribe explicitly:
Conn->SubscriptionBuilder()
    ->OnApplied(OnAppliedDelegate)
    ->OnError(OnErrorDelegate)
    ->Subscribe({ TEXT("SELECT * FROM damage_event") });
```

</TabItem>
</Tabs>

## Table Name Canonicalization

### What changed

SpacetimeDB 2.0 no longer equates the canonical name of your tables and indexes with the accessor method you use in module or client code. The canonical name is largely an internal detail, but you may encounter it when making SQL queries, or in the migration plans printed by `spacetime publish`.

### Updating source code: Change `name` to `accessor` in table definitions

<Tabs groupId="server-language" queryString>
<TabItem value="typescript" label="TypeScript">

The `name` option for table definitions is now used to overwrite the canonical name, and is optional. The name of the key passed to the `schema` function controls the method names you write in your module and client source code.

By default, the canonical name is derived from the accessor by converting it to snake case.

To migrate a 1.0 table definition to 2.0, pass an object to the `schema` function. Always use `schema({ table1 })` or `schema({ t1, t2 })` — never pass a single table directly.

:::warning TypeScript: `schema()` takes exactly one argument — an object
Use `schema({ table })` or `schema({ t1, t2 })`. **Never** use `schema(table)` or `schema(t1, t2, t3)`.
:::

```typescript
// 1.0 -- NO LONGER VALID in 2.0
const myTable = table({ name: "my_table", public: true });
const spacetimedb = schema(myTable); // NO LONGER VALID in 2.0
```

```typescript
// 2.0
const myTable = table({ public: true });
const spacetimedb = schema({ myTable }); // NOTE! We are passing `{ myTable }`, not `myTable`
export default spacetimedb; // You must now also export the schema from your module.
```

</TabItem>
<TabItem value="csharp" label="C#">

The `Name` argument on table and index attributes is now used to override the canonical SQL name and is optional. The `Accessor` argument controls the generated API names you use in module and client code.

By default, the canonical name is derived from the accessor using the module's case-conversion policy.

To migrate a 1.0 table definition to 2.0, replace `Name =` with `Accessor =` in table and index definitions. Always use `SpacetimeDB.Index.BTree` (never bare `Index` — it conflicts with `System.Index`):

```csharp
// 1.0 style -- NO LONGER VALID in 2.0
[SpacetimeDB.Table(Name = "MyTable", Public = true)]
[SpacetimeDB.Index.BTree(Name = "Position", Columns = new[] { nameof(X), nameof(Y) })]
public partial struct MyTable
{
    [SpacetimeDB.PrimaryKey]
    [SpacetimeDB.AutoInc]
    public uint Id;
    public uint X;
    public uint Y;
}

// 2.0
[SpacetimeDB.Table(Accessor = "MyTable", Public = true)]
[SpacetimeDB.Index.BTree(Accessor = "Position", Columns = new[] { nameof(X), nameof(Y) })]
public partial struct MyTable
{
    [SpacetimeDB.PrimaryKey]
    [SpacetimeDB.AutoInc]
    public uint Id;
    public uint X;
    public uint Y;
}
```

</TabItem>
<TabItem value="rust" label="Rust">

The `name` argument to table definitions is now used to overwrite the canonical name, and is optional. The `accessor` argument controls the method names you write in your module and client source code.

By default, the canonical name is derived from the accessor by converting it to snake case.

To migrate a 1.0 table definition to 2.0, replace `name =` with `accessor =` in the table and index definitions:

```rust
// 1.0 -- NO LONGER VALID in 2.0
#[spacetimedb::table(
    name = my_table,
    public,
    index(
        name = position,
        btree(columns = [x, y]),
    )
)]
struct MyTable {
    #[primary_key]
    #[auto_inc]
    id: u32,
    x: u32,
    y: u32,
}

// 2.0
#[spacetimedb::table(
    accessor = my_table,
    public,
    index(
        accessor = position,
        btree(columns = [x, y]),
    )
)]
struct MyTable {
    #[primary_key]
    #[auto_inc]
    id: u32,
    x: u32,
    y: u32,
}
```

</TabItem>
<TabItem value="cpp" label="C++">

The C++ module API does not have a direct `name -> accessor` rename like C# and Rust.

In C++, the second argument to `SPACETIMEDB_TABLE(...)` is already the table accessor you use in module code, and the second argument to `FIELD_NamedMultiColumnIndex(...)` is already the index accessor.

So for this migration step, keep using the accessor names you want in code:

```cpp
struct MyTable {
    uint32_t id;
    uint32_t x;
    uint32_t y;
};
SPACETIMEDB_STRUCT(MyTable, id, x, y);

// 2.0
SPACETIMEDB_TABLE(MyTable, my_table, Public);
FIELD_PrimaryKeyAutoInc(my_table, id);
FIELD_NamedMultiColumnIndex(my_table, position, x, y);
```

If you also need to preserve or override the canonical SQL names that appear in migrations or SQL, use the explicit-name forms described in Option 2 below.

</TabItem>
</Tabs>

### Auto-migrating existing databases

The new default behavior for canonicalizing names may not be compatible with existing 1.0 databases, as it may change the casing of table names, which would require a manual migration.

#### Option 1: Disable case conversion

To avoid this manual migration, configure the case conversion policy in your module to not convert, which will result in the same table names as a 1.0 module:

<Tabs groupId="server-language" queryString>
<TabItem value="typescript" label="TypeScript">

```typescript
export const moduleSettings: ModuleSettings = {
  caseConversionPolicy: CaseConversionPolicy.None,
};
```

</TabItem>
<TabItem value="csharp" label="C#">

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

</TabItem>
<TabItem value="rust" label="Rust">

```rust
use spacetimedb::CaseConversionPolicy;

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

</TabItem>
<TabItem value="cpp" label="C++">

```cpp
SPACETIMEDB_SETTING_CASE_CONVERSION(SpacetimeDB::CaseConversionPolicy::None)
```

Use `SPACETIMEDB_SETTING_CASE_CONVERSION(...)` to preserve 1.0-style canonical names when migrating a C++ module.

</TabItem>
</Tabs>

#### Option 2: overwrite the name of individual tables

Alternatively, manually specify the correct canonical name of each table:

<Tabs groupId="server-language" queryString>
<TabItem value="typescript" label="TypeScript">

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

const myTable = table(
  {
    name: 'MyTable',
    public: true,
    indexes: [{ accessor: 'position', columns: ['x', 'y'] }],
  },
  {
    id: t.u32().primaryKey().autoInc(),
    x: t.u32(),
    y: t.u32(),
  }
);
```

</TabItem>
<TabItem value="csharp" label="C#">

Always use `SpacetimeDB.Index.BTree` (never bare `Index` — it conflicts with `System.Index`):

```csharp
[SpacetimeDB.Table(Accessor = "MyTable", Name = "MyTable", Public = true)]
[SpacetimeDB.Index.BTree(Accessor = "Position", Columns = new[] { nameof(X), nameof(Y) })]
public partial struct MyTable
{
    [SpacetimeDB.PrimaryKey]
    [SpacetimeDB.AutoInc]
    public uint Id;
    public uint X;
    public uint Y;
}
```

</TabItem>
<TabItem value="rust" label="Rust">
```rust
#[spacetimedb::table(
    accessor = my_table,
    name = "MyTable",
    public,
    index(
        accessor = position,
        btree(columns = [x, y]),
    )
)]
struct MyTable {
    #[primary_key]
    #[auto_inc]
    id: u32,
    x: u32,
    y: u32,
}
```

</TabItem>
<TabItem value="cpp" label="C++">

If you need to preserve an existing canonical SQL name during migration, pass it directly to `SPACETIMEDB_TABLE(...)`. Use the `_NAMED` field/index macros only for index canonical names.

```cpp
struct MyTable {
    uint32_t id;
    uint32_t x;
    uint32_t y;
};
SPACETIMEDB_STRUCT(MyTable, id, x, y);
SPACETIMEDB_TABLE(MyTable, my_table, Public, "MyTable");
FIELD_PrimaryKeyAutoInc(my_table, id);
FIELD_MultiColumnIndex_NAMED(my_table, position, "Position", x, y);
```

</TabItem>
</Tabs>

## Clients connect with database name

<Tabs groupId="client-language" queryString>
<TabItem value="typescript" label="TypeScript">

When constructing a `DbConnection` to a remote database, you now use `withDatabaseName` to provide the database name, rather than `withModuleName`. This is a more accurate terminology.

```typescript
// 1.0 -- NO LONGER CORRECT
const conn = DbConnection.builder()
    .withUri("https://maincloud.spacetimedb.com")
    .withModuleName("my-database")
    // other options...
    .build();

// 2.0
const conn = DbConnection.builder()
    .withUri("https://maincloud.spacetimedb.com")
    .withDatabaseName("my-database")
    // other options...
    .build()
```

</TabItem>
<TabItem value="csharp" label="C#">

When constructing a `DbConnection` to a remote database, you now use `WithDatabaseName` to provide the database name, rather than `WithModuleName`. This is a more accurate terminology.

```csharp
// 1.0 -- NO LONGER CORRECT
var conn = DbConnection.Builder()
    .WithUri("https://maincloud.spacetimedb.com")
    .WithModuleName("my-database")
    // other options...
    .Build();

// 2.0
var conn = DbConnection.Builder()
    .WithUri("https://maincloud.spacetimedb.com")
    .WithDatabaseName("my-database")
    // other options...
    .Build();
```

</TabItem>
<TabItem value="rust" label="Rust">

When constructing a `DbConnection` to a remote database, you now use `with_database_name` to provide the database name, rather than `with_module_name`. This is a more accurate terminology.

```rust
// 1.0 -- NO LONGER CORRECT
let conn = DbConnection::builder()
    .with_uri("https://maincloud.spacetimedb.com")
    .with_module_name("my-database")
    // other options...
    .build()
    .expect("Failed to connect");

// 2.0
let conn = DbConnection::builder()
    .with_uri("https://maincloud.spacetimedb.com")
    .with_database_name("my-database")
    // other options...
    .build()
    .expect("Failed to connect");
```

</TabItem>
<TabItem value="cpp-unreal" label="Unreal C++">

When constructing a `UDbConnection` to a remote database, use `WithDatabaseName` in 2.0. The Unreal 2.0 builder does not expose `WithModuleName`.

```cpp
// 1.0 terminology in other SDKs / older docs
// UDbConnection::Builder()
//     ->WithUri(TEXT("https://maincloud.spacetimedb.com"))
//     ->WithModuleName(TEXT("my-database"))
//     ->Build();

// 2.0
UDbConnection* Conn = UDbConnection::Builder()
    ->WithUri(TEXT("https://maincloud.spacetimedb.com"))
    ->WithDatabaseName(TEXT("my-database"))
    // other options...
    ->Build();
```

</TabItem>
</Tabs>

## `sender` Is Now A Method, Not A Field

<Tabs groupId="server-language" queryString>
<TabItem value="typescript" label="TypeScript">

This change does not apply to TypeScript, where properties are indistinguishable from fields.

</TabItem>
<TabItem value="csharp" label="C#">

This change does not apply to C#, where properties are indistinguishable from fields.

</TabItem>
<TabItem value="rust" label="Rust">

In Rust modules, the sender of a request is no longer exposed via a field `ctx.sender` on `ReducerContext`, `ProcedureContext`, `ViewContext` or `AnonymousViewContext`. Instead, each of these types has a method `ctx.sender()` which returns the sender's identity.

```rust
// 1.0 -- NO LONGER CORRECT
#[spacetimedb::reducer]
fn my_reducer(ctx: &ReducerContext) {
    let sender_identity = ctx.sender;
    // Do stuff with `sender_identity`...
}

// 2.0
#[spacetimedb::reducer]
fn my_reducer(ctx: &ReducerContext) {
    let sender_identity = ctx.sender();
    // Do stuff with `sender_identity`...
}
```
</TabItem>
<TabItem value="cpp" label="C++">

In C++ modules, the sender is now accessed with `ctx.sender()` rather than a `ctx.sender` field.

```cpp
// 1.0 -- NO LONGER CORRECT
SPACETIMEDB_REDUCER(my_reducer, ReducerContext ctx) {
    auto sender_identity = ctx.sender;
    // Do stuff with `sender_identity`...
    return Ok();
}

// 2.0
SPACETIMEDB_REDUCER(my_reducer, ReducerContext ctx) {
    auto sender_identity = ctx.sender();
    // Do stuff with `sender_identity`...
    return Ok();
}
```
</TabItem>
</Tabs>

## Only Primary Keys Have Update Methods

<Tabs groupId="server-language" queryString>
<TabItem value="typescript" label="TypeScript">

In 2.0 modules, only columns with a `.primaryKey()` constraint expose an `update` method, whereas previously, `.unique()` constraints also provided that method. The previous behavior led to confusion, as only updates which preserved the value in the primary key column resulted in `onUpdate` callbacks being invoked on the client.

```typescript
const myTable = table({ name: 'my_table' }, {
    id: t.u32().unique(),
    name: t.string(),
}) 

// 1.0 -- REMOVED in 2.0 
spacetimedb.reducer('my_reducer', ctx => {
    ctx.db.myTable.id.update({
        id: 1,
        name: "Foobar",
    });
})

// 2.0 -- Perform a delete followed by an insert
// OR change the `.unique()` constraint into `.primaryKey()` constraint
spacetimedb.reducer(ctx => {
    ctx.db.myTable.id.delete(1);
    ctx.db.myTable.insert({
        id: 1,
        name: "Foobar"
    });
})
```

</TabItem>
<TabItem value="csharp" label="C#">

In 2.0 modules, only `[SpacetimeDB.PrimaryKey]` indexes expose an `Update` method, whereas previously, `[SpacetimeDB.Unique]` indexes also provided that method. The previous behavior led to confusion, as only updates which preserved the primary key value resulted in `OnUpdate` callbacks being invoked on the client.

### Updates which preserve the primary key - update with the primary key index

```csharp
[SpacetimeDB.Table(Accessor = "User")]
public partial struct User
{
    [SpacetimeDB.PrimaryKey]
    public Identity Identity;

    [SpacetimeDB.Unique]
    public string Name;

    public uint ApplesOwned;
}

// 1.0 -- REMOVED in 2.0
[SpacetimeDB.Reducer]
public static void AddAppleOld(ReducerContext ctx, string name)
{
    var user = ctx.Db.User.Name.Find(name).Value;
    ctx.Db.User.Name.Update(new User
    {
        ApplesOwned = user.ApplesOwned + 1,
        Identity = user.Identity,
        Name = user.Name,
    });
}

// 2.0
[SpacetimeDB.Reducer]
public static void AddApple(ReducerContext ctx, string name)
{
    var user = ctx.Db.User.Name.Find(name).Value;
    ctx.Db.User.Identity.Update(new User
    {
        ApplesOwned = user.ApplesOwned + 1,
        Identity = user.Identity,
        Name = user.Name,
    });
}
```

### Updates which change the primary key - explicitly delete and insert

```csharp
[SpacetimeDB.Table(Accessor = "User")]
public partial struct User
{
    [SpacetimeDB.PrimaryKey]
    public Identity Identity;

    [SpacetimeDB.Unique]
    public string Name;

    public uint ApplesOwned;
}

// 1.0 -- REMOVED in 2.0
[SpacetimeDB.Reducer]
public static void ChangeUserIdentityOld(ReducerContext ctx, string name, Identity identity)
{
    var user = ctx.Db.User.Name.Find(name).Value;
    ctx.Db.User.Name.Update(new User
    {
        Identity = identity,
        Name = user.Name,
        ApplesOwned = user.ApplesOwned,
    });
}

// 2.0
[SpacetimeDB.Reducer]
public static void ChangeUserIdentity(ReducerContext ctx, string name, Identity identity)
{
    var user = ctx.Db.User.Name.Find(name).Value;
    ctx.Db.User.Delete(user);
    ctx.Db.User.Insert(new User
    {
        Identity = identity,
        Name = user.Name,
        ApplesOwned = user.ApplesOwned,
    });
}
```

</TabItem>
<TabItem value="rust" label="Rust">

In 2.0 modules, only `#[primary_key]` constraints expose an `update` method, whereas previously, `#[unique]` constraints also provided that method. The previous behavior led to confusion, as only updates which preserved the value in the primary key column resulted in `on_update` callbacks being invoked on the client.

### Updates which preserve the primary key - update with the primary key index

```rust
#[spacetimedb::table(accessor = user)]
struct User {
    #[primary_key]
    identity: Identity,

    #[unique]
    name: String,

    apples_owned: u32,
}

// 1.0 -- REMOVED in 2.0
#[spacetimedb::reducer]
fn add_apple(ctx: &ReducerContext, name: String) {
    let user = ctx.db.user().name().find(&name).unwrap();
    ctx.db.user().name().update(User {
        apples_owned: user.apples_owned + 1,
        ..user
    });
}

// 2.0
#[spacetimedb::reducer]
fn add_apple(ctx: &ReducerContext, name: String) {
    let user = ctx.db.user().name().find(&name).unwrap();
    ctx.db.user().identity().update(User {
        apples_owned: user.apples_owned + 1,
        ..user
    });
}
```

### Updates which change the primary key - explicitly delete and insert

```rust
#[spacetimedb::table(accessor = user)]
#[derive(Clone)]
struct User {
    #[primary_key]
    identity: Identity,

    #[unique]
    name: String,

    apples_owned: u32,
}

// 1.0 -- REMOVED in 2.0
#[spacetimedb::reducer]
fn change_user_identity(ctx: &ReducerContext, name: String, identity: Identity) {
    let user = ctx.db.user().name().find(&name).unwrap();
    ctx.db.user().name().update(User {
        identity,
        ..user
    });
}

// 2.0
#[spacetimedb::reducer]
fn change_user_identity(ctx: &ReducerContext, name: String, identity: Identity) {
    let user = ctx.db.user().name().find(&name).unwrap();
    ctx.db.user().delete(user.clone());
    ctx.db.user().insert(User {
        identity,
        ..user
    });
}
```

</TabItem>
<TabItem value="cpp" label="C++">

In 2.0 modules, updates should go through the primary key index. If you were previously treating another unique index like an update handle, migrate to primary-key updates or an explicit delete-plus-insert.

### Updates which preserve the primary key - update with the primary key index

```cpp
struct User {
    Identity identity;
    std::string name;
    uint32_t apples_owned;
};
SPACETIMEDB_STRUCT(User, identity, name, apples_owned);
SPACETIMEDB_TABLE(User, user, Public);
FIELD_PrimaryKey(user, identity);
FIELD_Unique(user, name);

// 2.0
SPACETIMEDB_REDUCER(add_apple, ReducerContext ctx, std::string name) {
    auto user_row = ctx.db[user_name].find(name);
    if (!user_row) {
        return Err("User not found");
    }

    user_row->apples_owned += 1;
    ctx.db[user_identity].update(*user_row);
    return Ok();
}
```

### Updates which change the primary key - explicitly delete and insert

```cpp
struct User {
    Identity identity;
    std::string name;
    uint32_t apples_owned;
};
SPACETIMEDB_STRUCT(User, identity, name, apples_owned);
SPACETIMEDB_TABLE(User, user, Public);
FIELD_PrimaryKey(user, identity);
FIELD_Unique(user, name);

// 2.0
SPACETIMEDB_REDUCER(change_user_identity, ReducerContext ctx, std::string name, Identity identity) {
    auto user_row = ctx.db[user_name].find(name);
    if (!user_row) {
        return Err("User not found");
    }

    User updated = *user_row;
    ctx.db[user_identity].delete_by_key(updated.identity);
    updated.identity = identity;
    ctx.db[user].insert(updated);
    return Ok();
}
```

</TabItem>
</Tabs>

## Scheduled Functions Are Now Private

Scheduled reducers and procedures are now private by default, meaning that only the database owner and team collaborators can bypass the schedule table to invoke them manually.

### Remove authorization logic from scheduled functions

Because scheduled reducers and procedures are now private, it's no longer necessary to explicitly check that the sender is the database itself.

<Tabs groupId="server-language" queryString>
<TabItem value="typescript" label="TypeScript">

```typescript
// 1.0 -- NO LONGER VALID in 2.0
const myTimer = table({ name: "my_timer", scheduled: 'runMyTimer' }, {
  scheduledId: t.u64(),
  scheduledAt: t.scheduleAt(),
});
const spacetimedb = schema(myTimer);

// 1.0 - SUPERFLUOUS IN 2.0
spacetimedb.reducer('runMyTimer', myTimer.rowType, (ctx, timer) => {
  if (ctx.sender != ctx.identity) {
    throw SenderError(`'runMyTimer' should only be invoked by the database!`);
  }
  // Do stuff
})
```

```typescript
const myTimer = table({ scheduled: () => runMyTimer }, {
  scheduledId: t.u64().primaryKey().autoInc(),
  scheduledAt: t.scheduleAt(),
});
const spacetimedb = schema({ myTimer }); // schema({ table }), never schema(table)

// 2.0 -- Can only be called by the database
export const runMyTimer = spacetimedb.reducer({ arg: myTimer.rowType }, (ctx, { arg }) => {
  // Do stuff
})
```

</TabItem>
<TabItem value="csharp" label="C#">

```csharp
[SpacetimeDB.Table(Accessor = "MyTimer", Scheduled = nameof(RunMyTimer))]
public partial struct MyTimer
{
    [SpacetimeDB.PrimaryKey]
    [SpacetimeDB.AutoInc]
    public ulong ScheduledId;
    public ScheduleAt ScheduledAt;
}

// 1.0 - SUPERFLUOUS
[SpacetimeDB.Reducer]
public static void RunMyTimer(ReducerContext ctx, MyTimer timer)
{
    if (ctx.Sender != ctx.Identity)
    {
        throw new Exception("`RunMyTimer` should only be invoked by the database!");
    }
    // Do stuff...
}

// 2.0
[SpacetimeDB.Reducer]
public static void RunMyTimer(ReducerContext ctx, MyTimer timer)
{
    // Do stuff...
}
```

</TabItem>
<TabItem value="rust" label="Rust">

```rust
#[spacetimedb::table(accessor = my_timer, scheduled(run_my_timer))]
struct MyTimer {
    #[primary_key]
    #[auto_inc]
    scheduled_id: u64,
    scheduled_at: spacetimedb::ScheduleAt,
}

// 1.0 - SUPERFLUOUS IN 2.0
#[spacetimedb::reducer]
fn run_my_timer(ctx: &ReducerContext, timer: MyTimer) -> Result<(), String> {
    if ctx.sender() != ctx.identity() {
        return Err("`run_my_timer` should only be invoked by the database!".to_string());
    }
    // Do stuff...
    Ok(())
}

// 2.0 -- Can only be called by the database
#[spacetimedb::reducer]
fn run_my_timer(ctx: &ReducerContext, timer: MyTimer) {
    // Do stuff...
}
```

</TabItem>
<TabItem value="cpp" label="C++">

```cpp
struct MyTimer {
    uint64_t scheduled_id;
    ScheduleAt scheduled_at;
};
SPACETIMEDB_STRUCT(MyTimer, scheduled_id, scheduled_at);
SPACETIMEDB_TABLE(MyTimer, my_timer, Private);
FIELD_PrimaryKeyAutoInc(my_timer, scheduled_id);
SPACETIMEDB_SCHEDULE(my_timer, 1, run_my_timer);

// 1.0 - SUPERFLUOUS IN 2.0
SPACETIMEDB_REDUCER(run_my_timer, ReducerContext ctx, MyTimer timer) {
    if (ctx.sender() != ctx.identity()) {
        return Err("`run_my_timer` should only be invoked by the database!");
    }
    // Do stuff...
    return Ok();
}

// 2.0
SPACETIMEDB_REDUCER(run_my_timer, ReducerContext ctx, MyTimer timer) {
    // Do stuff...
    return Ok();
}
```

</TabItem>
</Tabs>

### Define wrappers for functions that are both scheduled and invoked by clients

In the rare event that you have a reducer or procedure which is intended to be invoked by both clients and a schedule table, define a new public reducer or procedure which wraps the scheduled function.

<Tabs groupId="server-language" queryString>
<TabItem value="typescript" label="TypeScript">

```typescript
const myTimer = table({ scheduled: () => runMyTimerPrivate }, {
  scheduledId: t.u64().primaryKey().autoInc(),
  scheduledAt: t.scheduleAt(),
});
const spacetimedb = schema({ myTimer }); // schema({ table }), never schema(table)

export const runMyTimerPrivate = spacetimedb.reducer({ arg: myTimer.rowType }, (ctx, { arg }) => {
  // Do stuff...
});

export const runMyTimer = spacetimedb.reducer({ arg: myTimer.rowType }, (ctx, { arg }) => {
  // Same logic as runMyTimerPrivate — extract to a helper if needed
});
```

</TabItem>
<TabItem value="csharp" label="C#">

```csharp
[SpacetimeDB.Table(Accessor = "MyTimer", Scheduled = nameof(RunMyTimerPrivate))]
public partial struct MyTimer
{
    [SpacetimeDB.PrimaryKey]
    [SpacetimeDB.AutoInc]
    public ulong ScheduledId;
    public ScheduleAt ScheduledAt;
}

[SpacetimeDB.Reducer]
public static void RunMyTimerPrivate(ReducerContext ctx, MyTimer timer)
{
    // Do stuff...
}

[SpacetimeDB.Reducer]
public static void RunMyTimer(ReducerContext ctx, MyTimer timer)
{
    RunMyTimerPrivate(ctx, timer);
}
```

</TabItem>
<TabItem value="rust" label="Rust">

```rust
#[spacetimedb::table(accessor = my_timer, scheduled(run_my_timer_private))]
struct MyTimer {
    #[primary_key]
    #[auto_inc]
    scheduled_id: u64,
    scheduled_at: spacetimedb::ScheduleAt,
}

#[spacetimedb::reducer]
fn run_my_timer_private(ctx: &ReducerContext, timer: MyTimer) {
    // Do stuff...
}

#[spacetimedb::reducer]
fn run_my_timer(ctx: &ReducerContext, timer: MyTimer) {
    run_my_timer_private(ctx, timer)
}
```

</TabItem>
<TabItem value="cpp" label="C++">

```cpp
struct MyTimer {
    uint64_t scheduled_id;
    ScheduleAt scheduled_at;
};
SPACETIMEDB_STRUCT(MyTimer, scheduled_id, scheduled_at);
SPACETIMEDB_TABLE(MyTimer, my_timer, Private);
FIELD_PrimaryKeyAutoInc(my_timer, scheduled_id);
SPACETIMEDB_SCHEDULE(my_timer, 1, run_my_timer_private);

SPACETIMEDB_REDUCER(run_my_timer_private, ReducerContext ctx, MyTimer timer) {
    // Do stuff...
    return Ok();
}

SPACETIMEDB_REDUCER(run_my_timer, ReducerContext ctx, MyTimer timer) {
    return run_my_timer_private(ctx, timer);
}
```

</TabItem>
</Tabs>

## Private Items Are Not Code-Generated By Default

Starting in SpacetimeDB 2.0, `spacetime generate` will not generate bindings for private tables or functions by default. These bindings were confusing, as only clients authenticated as the database owner or a collaborator could access them, with most clients seeing an error when trying to subscribe to a private table or invoke a private function.

### Pass `--include-private` to `spacetime generate`

For clients which rely on generated bindings to private tables or functions, pass the `--include-private` flag to the `spacetime generate` CLI command.

## Light Mode

### What changed

In 1.0, `light_mode` prevented the server from sending reducer event data to a client (unless that client was the caller):

<Tabs groupId="client-language" queryString>
<TabItem value="typescript" label="TypeScript">

```typescript
// 1.0 -- REMOVED in 2.0
DbConnection.builder()
    .withLightMode(true)
    // ...
```

</TabItem>
<TabItem value="csharp" label="C#">

```csharp
// 1.0 -- REMOVED in 2.0
DbConnection.Builder()
    .WithLightMode(true)
    // ...
```

</TabItem>
<TabItem value="rust" label="Rust">

```rust
// 1.0 -- REMOVED in 2.0
DbConnection::builder()
    .with_light_mode(true)
    // ...
```

</TabItem>
<TabItem value="cpp-unreal" label="Unreal C++">

```cpp
// 1.0 equivalent removed in 2.0
// The Unreal 2.0 builder does not expose WithLightMode(...)
```

</TabItem>
</Tabs>

In 2.0, the server never broadcasts reducer argument data to any client, so `light_mode` is no longer necessary. Simply remove the call:

<Tabs groupId="client-language" queryString>
<TabItem value="typescript" label="TypeScript">

```typescript
// 2.0
DbConnection.builder()
    .withUri(uri)
    .withDatabaseName(name)
    // no withLightMode needed
    .build()
```

</TabItem>
<TabItem value="csharp" label="C#">

```csharp
// 2.0
DbConnection.Builder()
    .WithUri(uri)
    .WithDatabaseName(name)
    // no WithLightMode needed
    .Build();
```

</TabItem>
<TabItem value="rust" label="Rust">

```rust
// 2.0
DbConnection::builder()
    .with_uri(uri)
    .with_database_name(name)
    // no with_light_mode needed
    .build()
```

</TabItem>
<TabItem value="cpp-unreal" label="Unreal C++">

```cpp
// 2.0
UDbConnection* Conn = UDbConnection::Builder()
    ->WithUri(Uri)
    ->WithDatabaseName(DatabaseName)
    // no WithLightMode needed
    ->Build();
```

This migration item does not apply directly to Unreal. The 2.0 Unreal builder has no public `WithLightMode(...)` API.

</TabItem>
</Tabs>

## CallReducerFlags

### What changed

In 1.0, you could suppress success notifications for individual reducer calls:

<Tabs groupId="client-language" queryString>
<TabItem value="typescript" label="TypeScript">

```typescript
// 1.0 -- REMOVED in 2.0
ctx.setReducerFlags(CallReducerFlags.NoSuccessNotify);
ctx.reducers.myReducer(args);
```

In 2.0, the success notification is lightweight (just `requestId` and `timestamp`, no reducer args or full event data), so there is no need to suppress it. Remove any `setReducerFlags` calls and `CallReducerFlags` imports.

</TabItem>
<TabItem value="csharp" label="C#">

This migration item does not apply to C#. Before recent SpacetimeDB changes, C# had no public `CallReducerFlags` or `set_reducer_flags` equivalent.

</TabItem>
<TabItem value="rust" label="Rust">

```rust
// 1.0 -- REMOVED in 2.0
ctx.set_reducer_flags(CallReducerFlags::NoSuccessNotify);
ctx.reducers.my_reducer(args).unwrap();
```

In 2.0, the success notification is lightweight (just `request_id` and `timestamp`, no reducer args or full event data), so there is no need to suppress it. Remove any `set_reducer_flags` calls and `CallReducerFlags` imports.

</TabItem>
<TabItem value="cpp-unreal" label="Unreal C++">

This migration item does not apply to Unreal. The 2.0 Unreal SDK has no public `CallReducerFlags` or `setReducerFlags` equivalent.

</TabItem>
</Tabs>

## Confirmed Reads Enabled by Default

### What changed

In 1.0, subscription updates and SQL query results were sent to the client immediately, before the underlying transaction was confirmed to be durable. This meant a client could observe a row that was later lost if the server crashed before persisting it.

In 2.0, **confirmed reads are enabled by default**. The server waits until a transaction is confirmed durable before sending updates to clients. This ensures that any data a client receives will survive a server restart.

### Impact

- **Slightly higher latency**: Subscription updates and SQL results may arrive a few milliseconds later, as the server waits for durability confirmation before sending them.
- **Stronger consistency**: Clients will never observe data that could be lost due to a crash.
- **No code changes required**: This is a server-side default change. Existing client code works without modification.

### Opting out

If your application prioritizes low latency over durability guarantees (for example, a real-time game where occasional data loss on crash is acceptable), you can opt out by passing `confirmed=false` in the connection URL:

<Tabs groupId="client-language" queryString>
<TabItem value="typescript" label="TypeScript">

```typescript
DbConnection.builder()
    .withUri("https://maincloud.spacetimedb.com")
    .withDatabaseName("my-database")
    .withConfirmedReads(false) // opt out of confirmed reads
    .build()
```

</TabItem>
<TabItem value="csharp" label="C#">

```csharp
DbConnection.Builder()
    .WithUri("https://maincloud.spacetimedb.com")
    .WithDatabaseName("my-database")
    .WithConfirmedReads(false) // opt out of confirmed reads
    .Build();
```

</TabItem>
<TabItem value="rust" label="Rust">

```rust
DbConnection::builder()
    .with_uri("https://maincloud.spacetimedb.com")
    .with_database_name("my-database")
    .with_confirmed_reads(false) // opt out of confirmed reads
    .build()
    .expect("Failed to connect");
```

</TabItem>
<TabItem value="cpp-unreal" label="Unreal C++">

The Unreal SDK gets confirmed reads by default in 2.0. There is currently no Unreal builder method for opting out.

</TabItem>
</Tabs>

For the CLI:

```bash
# SQL without confirmed reads
spacetime sql <database> "SELECT * FROM my_table"
# The --confirmed flag is no longer needed (it is the default)
```

## Quick Migration Checklist

- [ ] Remove all `ctx.reducers.on_<reducer>()` calls
  - Replace with `_then()` callbacks for your own reducer calls
  - Unreal: replace with generated `On<Reducer>` delegates on the calling connection
  - Replace with event tables + `on_insert` for cross-client notifications
- [ ] Update `Event::UnknownTransaction` matches to `Event::Transaction`
- [ ] For each reducer whose args you were observing from other clients:
  1. Create an `#[table(..., event)]` on the server
  2. Insert into it from the reducer
  3. Subscribe to it on the client
  4. Use `on_insert` instead of the old reducer callback
- [ ] Replace `name =` with `accessor =` in table and index definitions
- [ ] **TypeScript:** Use `schema({ table })` or `schema({ t1, t2 })` — never `schema(table)` or `schema(t1, t2, t3)`
- [ ] Set your module's case conversion policy to `None`
- [ ] Change `with_module_name` to `with_database_name`
- [ ] Change `ctx.sender` to `ctx.sender()`
  - Only necessary in Rust and C++ modules.
- [ ] Remove `update` calls on non-primary key unique indexes
  - When leaving the primary key value unchanged, update using the primary key index
  - When altering the primary key value, delete and insert
- [ ] Remove superfluous auth logic from scheduled functions which are not called by clients
- [ ] Define wrappers around scheduled functions which are called by clients
- [ ] Use `spacetime generate --include-private` if you rely on bindings for private tables or functions
- [ ] Remove `with_light_mode()` from `DbConnectionBuilder`
  - Unreal: no action if you are already on the 2.0 Unreal SDK; there is no public `WithLightMode(...)`
- [ ] Remove `set_reducer_flags()` calls and `CallReducerFlags` imports
  - Unreal: no action if you are already on the 2.0 Unreal SDK; there is no public reducer-flags API
- [ ] Remove `unstable::CallReducerFlags` from imports
- [ ] Note that confirmed reads are now enabled by default (no action needed unless you want to opt out with `.withConfirmedReads(false)`)
