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
RpcConnectorinterface abstracts PostgreSQL, SQLite, CockroachDB, Supabase, and PlanetScale behind a unified client contract defined intemplates/keynote-2/src/core/connectors.ts. - Type Safety:
rpc_common.tsenforces strict typing forRpcRequestandRpcResponsepayloads across all supported transports. - HTTP Translation: Server implementations in
templates/keynote-2/src/rpc-servers/handle POST requests to/rpc, dispatching to specific functions likerpcTransferwith database-specific SQL execution. - Production Resilience: The
rpc_single_callscenario implements automatic retry logic with exponential backoff for transient HTTP errors (429/502/503/504). - Modular Deployment:
init_rpc_servers.tsenables 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →