SpacetimeDB Remote Procedure Calls (RPC): Architecture and Implementation Guide

SpacetimeDB implements a pluggable RPC layer that enables benchmarking against traditional SQL databases through a unified TypeScript connector interface, allowing identical client code to target PostgreSQL, SQLite, CockroachDB, and cloud backends.

SpacetimeDB remote procedure calls (RPC) provide a modular architecture for executing database operations across heterogeneous backends. This implementation, found in the clockworklabs/SpacetimeDB repository, allows developers to benchmark SpacetimeDB’s performance against classic relational stores using a single client abstraction. The system bridges TypeScript connectors with HTTP-based server implementations that translate JSON RPC calls into native SQL transactions.

Client-Side RPC Architecture

The RpcConnector Interface

All RPC clients implement the RpcConnector interface defined in templates/keynote-2/src/core/connectors.ts. This interface extends BaseConnector and exposes the primary method signature call(name, args), which returns a Promise<unknown> and standardizes how client code invokes remote functions regardless of the underlying database.

// Conceptual usage of the RpcConnector interface
const conn = await postgres_rpc();
await conn.call('transfer', { from_id: 1, to_id: 2, amount: 100 });

Type-Safe Request and Response Types

The file templates/keynote-2/src/connectors/rpc/rpc_common.ts establishes shared type definitions that enforce contract consistency across all transports. These types guarantee that every RPC exchange follows a predictable JSON structure.

export type RpcRequest = { name?: string; args?: Record<string, unknown> };
export type RpcResponse = 
  | { ok: true; result?: unknown } 
  | { ok: false; error: string };

Server-Side RPC Implementation

HTTP Endpoint Structure

Each backend provides an HTTP server implementation under templates/keynote-2/src/rpc-servers/. These servers expose a single POST endpoint at /rpc that accepts an RpcRequest payload, dispatches to the appropriate local handler, and returns a typed RpcResponse.

PostgreSQL RPC Server Example

The file templates/keynote-2/src/rpc-servers/postgres-rpc-server.ts demonstrates the standard handler pattern. It parses incoming requests and routes them to specific database functions such as rpcTransfer, rpcGetAccount, rpcVerify, and rpcSeed.

if (req.method === 'POST' && url.pathname === '/rpc') {
  const { name, args } = (await jsonBody(req)) as RpcRequest;
  let result: RpcResponse;

  switch (name) {
    case 'transfer':
      await rpcTransfer(args ?? {});
      result = { ok: true };
      break;
    case 'get_account':
      result = { ok: true, result: await rpcGetAccount(args ?? {}) };
      break;
    default:
      result = { ok: false, error: `unknown RPC ${name}` };
  }

  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(result));
}

Database-Specific Optimizations

Server implementations leverage database-specific features for performance and correctness. The PostgreSQL server utilizes prepared statements stored in a PREPARED constants object to reduce parsing overhead. For transactional integrity during transfers, it employs row-level locking via SELECT … FOR UPDATE to prevent race conditions between concurrent RPC calls.

RPC Initialization and Configuration

The orchestration script templates/keynote-2/src/init/init_rpc_servers.ts manages server lifecycle based on the ENABLE_RPC_SERVERS environment variable. This design makes the RPC layer entirely optional; developers can activate specific backends (Postgres, SQLite, CockroachDB, Supabase, PlanetScale) by setting the corresponding environment flags (PG_URL, SQLITE_URL, etc.) without modifying application code. Servers launch via pnpm tsx, enabling rapid benchmarking setup.

Benchmarking with RPC Scenarios

Single Transfer with Retry Logic

The reusable scenario rpc_single_call in templates/keynote-2/src/scenario_recipes/rpc_single_call.ts demonstrates production-ready RPC usage with resilient error handling. It automatically retries transient HTTP errors (429, 502, 503, 504) using exponential backoff before failing.

export async function rpc_single_call(
  conn: RpcConnector,
  from: number,
  to: number,
  amount: number,
): Promise<void> {
  if (from === to || amount <= 0) return;

  const fn = conn.name === 'convex' && process.env.CONVEX_USE_SHARDED_COUNTER === '1'
    ? 'transfer:transfer_sharded'
    : 'transfer';

  for (let attempts = 0; attempts < 3; attempts++) {
    try {
      await conn.call(fn, { amount, from_id: from, to_id: to });
      return;
    } catch (e: any) {
      const msg = String(e?.message ?? '');
      if (!/429|502|503|504/.test(msg) || attempts === 2) throw e;
      await new Promise(r => setTimeout(r, 50 * (attempts + 1)));
    }
  }
}

Environment-Based Routing

The benchmark scenario automatically adapts its behavior based on runtime configuration. When CONVEX_USE_SHARDED_COUNTER is set to "1", the connector switches to a sharded transfer method (transfer:transfer_sharded) instead of the standard transfer function, enabling performance comparisons between different data partitioning strategies.

Summary

  • Pluggable Connector Architecture: The RpcConnector interface abstracts PostgreSQL, SQLite, CockroachDB, Supabase, and PlanetScale behind a unified client contract defined in templates/keynote-2/src/core/connectors.ts.
  • Type Safety: rpc_common.ts enforces strict typing for RpcRequest and RpcResponse payloads across all supported transports.
  • HTTP Translation: Server implementations in templates/keynote-2/src/rpc-servers/ handle POST requests to /rpc, dispatching to specific functions like rpcTransfer with database-specific SQL execution.
  • Production Resilience: The rpc_single_call scenario implements automatic retry logic with exponential backoff for transient HTTP errors (429/502/503/504).
  • Modular Deployment: init_rpc_servers.ts enables selective server activation via environment variables, supporting flexible benchmarking configurations without code changes.

Frequently Asked Questions

How does SpacetimeDB handle RPC errors and retries?

The rpc_single_call function in templates/keynote-2/src/scenario_recipes/rpc_single_call.ts implements a retry mechanism that catches HTTP errors matching patterns 429, 502, 503, or 504. It attempts the call up to three times with increasing delays (50ms, 100ms, 150ms) before throwing the final error to the caller.

What databases are supported by the SpacetimeDB RPC layer?

The RPC implementation supports PostgreSQL, SQLite, CockroachDB, Supabase, and PlanetScale. Each backend maintains dedicated client connectors in templates/keynote-2/src/connectors/rpc/ (e.g., postgres_rpc.ts, sqlite_rpc.ts) and corresponding HTTP server implementations in templates/keynote-2/src/rpc-servers/.

How are RPC requests typed in SpacetimeDB?

The RpcRequest type in templates/keynote-2/src/connectors/rpc/rpc_common.ts defines the contract as an object containing optional name (string) and args (Record<string, unknown>) properties. The RpcResponse type uses a discriminated union with an ok boolean to distinguish successful results ({ ok: true; result?: unknown }) from failures ({ ok: false; error: string }).

Can I run multiple RPC servers simultaneously for benchmarking?

Yes. The templates/keynote-2/src/init/init_rpc_servers.ts script reads the ENABLE_RPC_SERVERS environment variable to selectively launch multiple server processes concurrently. This allows head-to-head performance comparisons where the same client code executes against PostgreSQL, SQLite, and CockroachDB instances simultaneously.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →