Documentation
Replication
Read-replica registry, routing, and failover — built on Postgres, not a custom WAL engine
Replication
Overview
Correction
This page previously described a bespoke WAL-streaming leader/follower replication engine (ReplicationMessage, /v1/wal/stream, crate::replica::{Follower, FollowerConfig}, an embedded Stackhouse/MemTable). None of that exists in the current codebase. Replication is implemented as a read-replica registry and failover-management layer built on top of PostgreSQL (stackhouse/src/platform/replicas.rs), not a custom replication engine.
Stackhouse relies on Postgres's own physical/streaming replication to keep replica nodes in sync; this layer tracks which nodes exist, routes reads across them, and orchestrates promoting a replica to primary on failover.
Mounted under /v1/platform/replicas
The read-replica router is nested into the app in stackhouse/src/main.rs under /v1/platform/replicas. The endpoints below are reachable over HTTP in the default server build.
Data Model
pub struct ReplicaNode {
pub id: String,
pub tenant_id: i64,
pub name: String,
pub host: String,
pub port: u16,
pub database: String,
pub region: String,
pub role: NodeRole, // Primary | Replica | Standby
pub status: NodeStatus, // Healthy | Degraded | Unhealthy | Offline | Promoting
pub replication_lag_ms: u64,
pub connections_active: u32,
pub connections_max: u32,
pub last_health_check: Option<String>,
pub created_at: String,
}Routes
All routes require an authenticated user (extract_auth_user); nodes are scoped per-tenant by the caller's user ID.
POST /nodes # Register a replica/standby node
GET /nodes # List nodes for the current tenant
POST /nodes/:id/promote # Promote a replica to primary (failover)
DELETE /nodes/:id # Remove a node
GET /stats # Aggregate replication statsRegister a Node
curl -X POST http://localhost:3000/v1/platform/replicas/nodes \
-H "Authorization: Bearer <jwt_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "replica-us-east-2",
"host": "replica2.db.internal",
"port": 5432,
"database": "postgres",
"region": "us-east-2",
"role": "replica"
}'port defaults to 5432, database to "postgres", region to "us-east-1", role to "replica" (accepted values: primary, replica, standby) if omitted.
Promote to Primary (Failover)
curl -X POST http://localhost:3000/v1/platform/replicas/nodes/<node_id>/promote \
-H "Authorization: Bearer <jwt_token>"Returns a FailoverEvent:
{
"success": true,
"data": {
"id": "...",
"old_primary_id": "...",
"new_primary_id": "...",
"reason": "...",
"duration_ms": 42,
"timestamp": "2026-08-18T12:00:00Z"
}
}Read Routing
ReplicaService::route_read(tenant_id) picks a healthy node to serve a read (round-robin across registered replicas, falling back toward the primary when no healthy replica is available). This is exposed as a library method for callers that want to route reads themselves; it is not currently invoked automatically by the main query path in handlers.rs.
Stats
curl http://localhost:3000/v1/platform/replicas/stats \
-H "Authorization: Bearer <jwt_token>"{
"success": true,
"data": {
"primary_id": "...",
"replica_count": 2,
"avg_replication_lag_ms": 120,
"max_replication_lag_ms": 340,
"total_reads_routed": 1042,
"reads_to_primary": 210,
"reads_to_replicas": 832
}
}Health checks are TCP-only
Nodes are health-checked every 30 seconds via a plain TCP connect to host:port — not a Postgres protocol check or a replication-lag query. replication_lag_ms is set once at registration and is not automatically updated by the health checker; don't rely on it for real lag monitoring today.
Related: Change Data Capture
Trigger + NOTIFY-based CDC (stackhouse/src/platform/cdc.rs) and PITR backup/restore (stackhouse/src/storage/backups/pitr.rs) are separate subsystems documented in their own sections — see Backup & Recovery.
Next: Billing