Skip to content
ashis.dev
Go back

Spacetime DB

Before we dive in, grab your popcorn. This post comes with a side of drama.

The Database Wars: Twitter’s Favorite Bloodsport

If you’ve spent any time on Twitter (sorry, X) in the past couple of years, you’ve probably witnessed it: the Great Database Debates. Someone posts “Just migrated from PostgreSQL to [shiny new DB]” and suddenly everyone’s an expert. Redis vs. Postgres vs. DynamoDB vs. MongoDB — it’s like watching philosophers argue about the number of angels on a pin, except the pins have uptime SLAs.

But every now and then, something different shows up. Something that makes you go “wait, the database runs my code? And it pushes updates to clients? And it’s fast?” Enter SpacetimeDB — the database that had Hacker News in a frenzy, the licensing debate crowd sharpening their pitchforks, and in February 2026… sparked the most entertaining database controversy to date.

The February 2026 “Fastest Database” Drama

Okay, so here’s where things get spicy. On February 24, 2026, SpacetimeDB dropped a benchmark video claiming their 2.0 release was the “FASTEST database in the world” — complete with dramatic music, a keynote demo showing 100,000 message inserts per second, and the now-infamous claim of being 1000x faster than your database.

The internet, being the internet, immediately went into full investigation mode.

Within 24 hours, developer Brandon Pollack published a gist titled “investigation.md” (because of course that’s what it’s called) digging into the benchmark methodology. His takeaway? “Cool Demo but…huh?” — noting that while not having TCP between the DB and app is definitely fast, being over 10x faster than in-process SQLite (which can run in memory?) raised some eyebrows. The investigationgist went semi-viral, with developers RT-ing it with things like “finally someone asked the question.”

Meanwhile, Prasad Pilla on LinkedIn (yes, LinkedIn — we’re reaching new platforms) actually ran his own head-to-head benchmark against Convex, a database he actually uses. His results? SpacetimeDB delivered 47x the throughput and 39x lower latency — still insanely impressive, but notably less than 1000x. The takeaway? “Really impressive” but maybe temper those claims just a tiny bit.

And then came the memes. Oh, the memes. “My SQL query is faster than your benchmark methodology.” “SpacetimeDB — it’s fast, we swear.” “1000x faster than PostgreSQL, if you remove PostgreSQL, the network, and the laws of physics.” Twitter’s database community had a field day.

Tyler Cloutier (SpacetimeDB’s cofounder) responded with grace, engaging with the critiques and pointing out that the benchmark was comparing a hosted DB-to-client direct path versus traditional client-to-server-to-DB architectures. Fair enough! But the drama had already spawned multiple HN threads, a few choice subtweets, and one particularly scathing take titled “Are published ANN-Benchmarks DBMS results trustworthy?” (though that was more about vector DBs, it felt adjacent).

The whole saga was a masterclass in: bold claims generate engagement, the community will verify your numbers, and Database Twitter is absolutely feral for technical drama. Will we ever know the true speed of SpacetimeDB? Probably not. Will we keep arguing about it? Absolutely.


So… What’s the Deal With SpacetimeDB?

SpacetimeDB burst onto the scene with a bold claim: it’s a “real-time backend framework and database for apps and games” that handles persistence, logic, deployment, and real-time sync in one cohesive stack.

The internet, naturally, had thoughts:

Ah yes, the license. SpacetimeDB uses BSL 1.1 (Business Source License), which means it’s source-available but not exactly “open source” in the traditional sense. It converts to AGPL after 4 years, which sent the “but what about FORKERS?” crowd into a tailspin on Hacker News. People on Twitter debated whether this was a clever business move or a slap in the face to the open-source community. (Spoiler: both perspectives are valid, and the flamewars were spicy.)

Then there’s the Bitcraft connection — SpacetimeDB was built to power Bitcraft Online, an MMO that Clockwork Labs has been developing for years. The MMO launched on Steam in early 2026, and suddenly all those “but does it actually work at scale?” questions got answered in real-time. Developer Voices did a whole episode in February 2026 titled “What launching an MMO can teach us about Databases” — because nothing says “database credibility” like running a massively multiplayer online game on your own infrastructure. Some folks were impressed. Others pointed out that building a game to prove your database works is… a bold strategy, Cotton. (It worked, though — the database held up.)

But here’s the thing — regardless of the Twitter drama, the technology is genuinely interesting. And that’s what we’re actually going to talk about today.

Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                         BROWSER (Client)                            │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │  React App (Next.js)                                         │  │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │  │
│  │  │ page.tsx    │  │providers.tsx│  │ SpacetimeDBProvider │  │  │
│  │  │ (UI)        │  │ (Config)    │  │ (Connection Manager)│  │  │
│  │  └──────┬──────┘  └──────┬──────┘  └──────────┬──────────┘  │  │
│  │         │                │                     │               │  │
│  │         └────────────────┴──────────┬──────────┘               │  │
│  │                                    │                          │  │
│  │                           ┌────────▼────────┐                  │  │
│  │                           │ WebSocket       │                  │  │
│  │                           │ Connection      │                  │  │
│  │                           └────────┬────────┘                  │  │
│  └────────────────────────────────────┼────────────────────────────┘  │
└────────────────────────────────────────┼──────────────────────────────┘

                                         │ ws://localhost:3000

┌─────────────────────────────────────────────────────────────────────┐
│                    SPACETIMEDB SERVER                               │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  Database                                                     │   │
│  │  ┌────────────────┐  ┌─────────────────────────────────────┐ │   │
│  │  │ user table     │  │ message table                       │ │   │
│  │  │ - identity PK  │  │ - sender (identity)                 │ │   │
│  │  │ - name         │  │ - sent (timestamp)                  │ │   │
│  │  │ - online       │  │ - text                              │ │   │
│  │  └────────────────┘  └─────────────────────────────────────┘ │   │
│  │                                                                  │   │
│  │  Reducers (server functions)                                    │   │
│  │  ┌────────────────┐  ┌─────────────────────────────────────┐  │   │
│  │  │ send_message   │  │ set_name                            │  │   │
│  │  │ (insert msg)  │  │ (update user name)                  │  │   │
│  │  └────────────────┘  └─────────────────────────────────────┘  │   │
│  │                                                                  │   │
│  │  Lifecycle Hooks                                                │   │
│  │  ┌────────────────┐  ┌─────────────────────────────────────┐  │   │
│  │  │ onConnect      │  │ onDisconnect                        │  │   │
│  │  │ (create user)  │  │ (mark offline)                      │  │   │
│  │  └────────────────┘  └─────────────────────────────────────┘  │   │
│  └──────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

Understanding the Core Concepts

Before diving into the code, let’s unpack what makes SpacetimeDB tick. According to the official documentation, SpacetimeDB is a “real-time backend framework and database” that combines three things traditionally handled separately:

  1. Data storage (tables)
  2. Server logic (reducers)
  3. Real-time sync (subscriptions via WebSocket)

This is different from traditional architectures where you’d have a database (PostgreSQL), a backend API (Express, FastAPI), and then a separate mechanism for pushing updates to clients (WebSockets, polling, or something like Pusher). SpacetimeDB collapses all of this into one system.

Tables: Where Your Data Lives

Tables in SpacetimeDB work similarly to SQL tables — they’re structured collections of rows with defined columns and types. You define tables in your module (the server-side code), and they become part of your database schema.

The key distinction in SpacetimeDB is public vs. private tables:

In our chat app, both user and message are public because clients need to read both to display the chat UI.

Each row gets a special identity column type — this is a unique identifier for a user, similar to a wallet address. It’s generated automatically when a user connects and persists across sessions (backed by a token stored in localStorage).

Reducers: The Only Way to Write Data

This is the most important concept in SpacetimeDB: clients can never write directly to tables. Instead, they call reducers — server-side functions that run inside database transactions. Per the official docs:

“Reducers are functions that modify database state in response to client requests or system events. They are the only way to mutate tables in SpacetimeDB — all database changes must go through reducers.”

Reducers provide ACID transaction guarantees:

Reducers are also deterministic — they can’t make external HTTP requests, access the filesystem, or do anything with side effects. This ensures that the same input always produces the same result, which is critical for distributed systems. If you need to interact with external services, you’d use procedures instead (which are currently in beta and require manual transaction management).

In our chat app:

Lifecycle Hooks: Automatic Events

SpacetimeDB provides special reducers that run automatically on connection events (lifecycle reducers):

These are defined just like regular reducers but with special decorators (spacetimedb.clientConnected, etc.).

The Client-Side: WebSockets + Subscriptions

Clients connect to SpacetimeDB via WebSocket — not HTTP REST endpoints. This is what enables real-time updates. When you call useTable(tables.message), you’re setting up a subscription that:

  1. Sends an initial snapshot of all matching rows
  2. Keeps the WebSocket open
  3. Receives incremental updates whenever the table changes (inserts, updates, deletes)
  4. Automatically re-renders your React components

This is fundamentally different from polling — you get push updates with no extra requests. The subscription query can filter columns, so clients only receive data they need.

The client SDK handles all of this transparently. You just use hooks like useTable() and useReducer(), and the SDK manages the WebSocket connection, subscription updates, and reconnection logic.


File Breakdown

1. Server Module: spacetimedb/src/index.ts

This is the backend — the code that runs inside SpacetimeDB. According to the docs, modules are compiled to WebAssembly (or JavaScript bundles in v2.0) and executed by the SpacetimeDB runtime. This module defines your entire backend: tables, reducers, and lifecycle hooks.

// Tables define what data we store
const user = table(
  { name: "user", public: true },
  {
    identity: t.identity().primaryKey(),
    name: t.string().optional(),
    online: t.bool(),
  }
);

const message = table(
  { name: "message", public: true },
  { sender: t.identity(), sent: t.timestamp(), text: t.string() }
);

Breaking down the table definitions:

For the message table:

Key concepts:

// Reducers = functions that modify data (the ONLY way to write to DB)
// https://spacetimedb.com/docs/functions/reducers/
export const send_message = spacetimedb.reducer(
  { text: t.string() }, // input parameters - defines what clients can pass
  (ctx, { text }) => {
    // function body - runs inside a database transaction
    ctx.db.message.insert({ sender: ctx.sender, text, sent: ctx.timestamp });
  }
);

What’s happening here:

Transaction guarantees: If this reducer throws an error (or if any part fails), the entire operation rolls back. No partial messages get saved. This is automatic — you don’t need to write rollback code.

// Lifecycle hooks - called automatically when clients connect/disconnect
// https://spacetimedb.com/docs/functions/reducers/lifecycle
export const onConnect = spacetimedb.clientConnected(ctx => {
  ctx.db.user.insert({ identity: ctx.sender, name: undefined, online: true });
});

This runs automatically when a new client establishes a WebSocket connection. It creates a user row with:

You could also emit a system message here (like “User joined the chat”) by inserting into the message table.


2. Generated Bindings: src/module_bindings/

Run spacetime generate to auto-create these files from your server code. This is one of SpacetimeDB’s killer features — you write your schema in TypeScript on the server, and the CLI auto-generates strongly-typed client-side code. No more manually keeping client types in sync with your database schema!

module_bindings/
├── index.ts           # Main exports: tables, reducers, DbConnection
├── message_table.ts   # TypeScript type for message rows
├── user_table.ts      # TypeScript type for user rows
├── send_message_reducer.ts  # Type for reducer arguments
├── set_name_reducer.ts      # Type for reducer arguments
└── types/
    └── reducers.ts   # Combined reducer types

What they provide:

This is similar to how GraphQL Code Generator works, but it’s built into SpacetimeDB’s workflow. The generation happens every time you publish (spacetime publish), keeping client and server in sync.


3. Connection Provider: src/app/providers.tsx

This sets up the WebSocket connectionDB. The React to Spacetime SDK uses a provider pattern — wrap your app in SpacetimeDBProvider and all child components can access the connection via hooks.

const builder = DbConnection.builder()
  .withUri('ws://localhost:3000')    // SpacetimeDB server address
  .withDatabaseName('chat-app')       // database name
  .withToken(localStorage.getItem(tokenKey))  // stored auth token
  .onConnect(onConnect)               // callback when connected
  .onConnectError(onConnectError);    // callback on error

return <SpacetimeDBProvider connectionBuilder={builder}>

Understanding the connection flow:

  1. DbConnection.builder(): Creates a builder pattern for configuring the connection. You specify where to connect and what to do on events.

  2. .withUri('ws://localhost:3000'): The WebSocket endpoint of your SpacetimeDB server. In development, this is typically localhost:3000 (where spacetime start runs). In production, this would be your hosted SpacetimeDB instance.

  3. .withDatabaseName('chat-app'): The name of the database you published. When you run spacetime publish chat-app, it creates a database named “chat-app”.

  4. .withToken(localStorage.getItem(tokenKey)): This is key to identity persistence. When a user first connects, SpacetimeDB generates a new identity and returns a token. Store this token in localStorage (or a cookie), and on subsequent visits, pass it back. The user keeps the same identity across sessions — their messages and data persist.

  5. .onConnect() and .onConnectError(): Callbacks for connection events. The onConnect callback receives the connection and identity, which is where you’d typically store the token for the first time.

What SpacetimeDBProvider does:

In Next.js/App Router, you’d typically wrap your root layout or a dedicated provider component with this.


4. Chat UI: src/app/page.tsx

This is the client — runs in the browser. The React SDK gives you hooks that abstract away the WebSocket and subscription machinery. You just use them like local state, but they’re actually backed by real-time data from the server.

// Hooks to interact with SpacetimeDB
const { identity, isActive: connected } = useSpacetimeDB(); // connection status
const [messages] = useTable(tables.message); // subscribe to messages
const [users] = useTable(tables.user); // subscribe to users
const sendMessageReducer = useReducer(reducers.sendMessage); // call reducer

// To send a message:
sendMessageReducer({ text: newMessage });

Breaking down each hook:

How data flows:

  1. useTable() subscribes to table changes via WebSocket
  2. When ANY user sends a message, ALL connected clients get updated automatically
  3. No manual refresh needed - real-time by default

This is fundamentally different from REST APIs where you’d:

With SpacetimeDB, the data flow is inverted — the server pushes changes, and your UI reacts.


How a Message is Sent

Let’s trace through exactly what happens when a user sends a message:

User types message → clicks Send


page.tsx: sendMessageReducer({ text: "Hello" })


useReducer hook sends via WebSocket to SpacetimeDB


SpacetimeDB server validates & runs send_message reducer


Reducer inserts into message table


SpacetimeDB notifies ALL connected clients of new message


All clients' useTable(tables.message) updates


React re-renders with new messages

Step-by-step breakdown:

  1. User clicks Send: The React component calls sendMessageReducer({ text: "Hello" }). This is just a local function call — no network request yet.

  2. SDK serializes and sends: The @spacetimedb/react SDK takes the arguments, serializes them to JSON, and sends a WebSocket message to the SpacetimeDB server at ws://localhost:3000.

  3. Server receives and validates: SpacetimeDB looks up the reducer by name, validates the input against the schema (text: t.string()), and executes the reducer function in a transaction.

  4. Reducer executes: The send_message reducer runs with ctx.sender set to the caller’s identity and ctx.timestamp set to server time. It inserts a new row into the message table.

  5. Transaction commits: If no errors, the insert is committed. The message is now persisted and visible to all subscribers.

  6. Subscription engine fires: SpacetimeDB’s subscription engine detects the table change. It looks at all connected clients who have subscribed to the message table and prepares update messages.

  7. WebSocket push: The server pushes update messages to all connected clients via WebSocket. This includes the sender (so they see their own message) and all other connected clients.

  8. Client SDK updates local state: Each client’s SDK receives the update, applies it to its local cache, and triggers a React state update.

  9. React re-renders: The component using useTable(tables.message) re-renders with the new message array.

The entire round trip — from click to render — typically takes milliseconds. And here’s the key: every client sees the same data. There’s no race condition where Client A sees the message but Client B doesn’t. The transaction provides a consistent ordering, and the subscription system ensures all clients converge to the same state.


Key SpacetimeDB Concepts

ConceptPurposeLearn More
TableStructured data storage with columns and typesTables docs
ReducerServer-side function that runs in a transaction — the ONLY way to write dataReducers docs
Public tableReadable by clients via subscriptions; only reducers can writeTable config public: true
IdentityGlobally unique user identifier, like a wallet addressIdentity docs
ctx.senderThe identity of whoever invoked the current reducerAutomatically populated
ctx.timestampServer timestamp at reducer invocation timeEnsures deterministic ordering
useTable()Subscribe to table changes; returns reactive array that updates in real-timeClient SDK
useReducer()Call a server-side reducer function from the clientClient SDK
Lifecycle hooksonConnect, onDisconnect, init — auto-run on eventsLifecycle docs
ViewRead-only server function that computes derived data (like aggregations)Views docs

How to Extend

Add a new reducer:

  1. Add to spacetimedb/src/index.ts:
export const delete_message = spacetimedb.reducer(
  { id: t.u64() },
  (ctx, { id }) => {
    ctx.db.message.id.delete(id);
  }
);
  1. Run spacetime generate to update bindings
  2. Use in UI:
const deleteMessage = useReducer(reducers.deleteMessage);
deleteMessage({ id: messageId });

Add a new table:

  1. Add to server module
  2. Run spacetime publish to update database
  3. Run spacetime generate for bindings
  4. Use useTable(tables.newTable) in client

Share this post on:

Previous Post
Edge vs Node Runtimes - The Showdown Your Frontend Deserves