Server & CLI
The TopGun server is a Rust binary built on axum and tokio. It manages client WebSocket connections, CRDT synchronization, Merkle delta sync, full-text and vector search, clustering, and durable storage (embedded redb by default, optional PostgreSQL for production).
This page documents the user-facing surface: how to run it, what flags and environment variables it reads, the HTTP and WebSocket endpoints, and the Rust embed API for callers that want to host the server in-process. For operator-oriented setup with Docker Compose, Kubernetes, and tuning advice, see Deploy.
Running the server
The repository entrypoint is pnpm start:server, which wraps cargo run --bin topgun-server --release. The binary is named topgun-server because the build was originally produced for the integration test suite; it is the same binary used in production-style demos like demo.topgun.build.
# From the repo root
pnpm start:server
# Or directly with cargo
cargo run --bin topgun-server --release
# Pick a port
PORT=8080 cargo run --bin topgun-server --release
# Increase log verbosity
RUST_LOG=topgun_server=debug cargo run --bin topgun-server --release
By default the server uses the embedded redb backend (no Postgres, no Docker required). It binds to 0.0.0.0:8080 and opens ./topgun.redb in the working directory. Set STORAGE_BACKEND=postgres and DATABASE_URL=... to use Postgres instead.
CLI flags
The topgun-server binary exposes five clap-derived flags (defined in packages/server-rust/src/bin/topgun_server.rs):
| Flag | Type | Default | Description |
|---|---|---|---|
--node-id <ID> | string | "topgun-server-node" | Unique identifier for this node in a cluster. |
--host <ADDR> | string | "127.0.0.1" | Host name or IP that peers use to reach this node’s cluster port. Also used as MemberInfo.host in join handshakes. |
--port <PORT> | u16 | 8080 (or env PORT) | Client WebSocket port. 0 means OS-assigned. Also reads the PORT environment variable. |
--cluster-port <PORT> | u16 | 0 (OS-assigned) | Inter-node cluster TCP port. |
--seed-nodes <LIST> | string | "" (single-node) | Comma-separated seed node addresses (e.g., "127.0.0.1:11001,127.0.0.1:11002"). Empty string means single-node mode. |
Every other knob (storage backend, auth, search indexes, admin UI, memory limits) is configured via environment variables, listed in the next section.
Environment variables
All variables below correspond to direct std::env::var(...) reads or tracing-subscriber::EnvFilter::try_from_default_env() consumers in packages/server-rust/src/. Variables not in this table are not read by the binary.
Storage backend
| Variable | Type | Default | Consumer | Notes |
|---|---|---|---|---|
STORAGE_BACKEND | string | "redb" | bin/topgun_server.rs:109 | One of redb, postgres, null. null is in-memory ephemeral (test only). |
TOPGUN_REDB_PATH | path | ./topgun.redb | bin/topgun_server.rs:115 | File path for the embedded redb database (used when STORAGE_BACKEND=redb). |
DATABASE_URL | string | none | bin/topgun_server.rs:126, storage/datastores/postgres.rs:693 | Postgres connection string (required when STORAGE_BACKEND=postgres). |
Networking
| Variable | Type | Default | Consumer | Notes |
|---|---|---|---|---|
PORT | u16 | 8080 | bin/topgun_server.rs:168 (via #[arg(env = "PORT")]) | Client WebSocket port. Equivalent to --port. |
TOPGUN_BIND_ADDR | string | "0.0.0.0" | bin/topgun_server.rs:203 | Bind address for the client WebSocket listener. Override to 127.0.0.1 for loopback-only deployments. |
Authentication
| Variable | Type | Default | Consumer | Notes |
|---|---|---|---|---|
JWT_SECRET | string | none | network/module.rs:454 | HMAC secret for signing/verifying JWTs. Required for production auth. |
TOPGUN_NO_AUTH | truthy | unset | bin/topgun_server.rs:477 | If set ("true", "1"), disables auth. Test and demo use only. |
INSECURE_FORWARD_AUTH_ERRORS | truthy | unset | bin/topgun_server.rs:470 | If set, forwards upstream auth errors verbatim in responses. Insecure; do not enable in production. |
TOPGUN_ADMIN_USERNAME | string | "admin" | network/handlers/admin.rs:111 | Username for the admin dashboard login. |
TOPGUN_ADMIN_PASSWORD | string | required if admin enabled | network/handlers/admin.rs:112 | Password for the admin dashboard login. |
TOPGUN_ADMIN_DIR | path | bundled SPA (npm) / ./admin-dashboard/dist (monorepo) | network/module.rs:478 | Static-file directory served at /admin/*. When unset, the npx @topgunbuild/server distribution auto-resolves the admin SPA bundled inside the package (the bin shim injects this path), and the monorepo falls back to ./admin-dashboard/dist. Set an explicit path to override — the shim never overwrites an operator-provided value. |
Search indexes
| Variable | Type | Default | Consumer | Notes |
|---|---|---|---|---|
TOPGUN_INDEX_PATH | path | none | bin/topgun_server.rs:516, network/module.rs:316, network/handlers/admin.rs:1078 | Directory holding the BM25 full-text-search index. |
TOPGUN_VECTOR_INDEX_PATH | path | none | network/module.rs:286, network/handlers/admin.rs:1139 | Directory holding the HNSW vector index. |
Observability
| Variable | Type | Default | Consumer | Notes |
|---|---|---|---|---|
RUST_LOG | filter expression | "info" | bin/topgun_server.rs:191 (via tracing_subscriber::EnvFilter::try_from_default_env()) | Standard tracing filter (e.g., topgun_server=debug,info). Not read via std::env::var directly. |
TOPGUN_LOG_FORMAT | string | text | service/middleware/observability.rs:130 | Set to "json" for structured JSON logs. Anything else uses the text formatter. |
Memory and eviction
| Variable | Type | Default | Consumer | Notes |
|---|---|---|---|---|
TOPGUN_MAX_RAM_MB | u64 | 1024 | storage/eviction_config.rs:85 | RAM ceiling for the in-memory record cache. |
TOPGUN_EVICTION_HIGH_PCT | u8 | 85 | storage/eviction_config.rs:105 | High water mark (% of TOPGUN_MAX_RAM_MB). Eviction engages above this threshold. |
TOPGUN_EVICTION_LOW_PCT | u8 | 70 | storage/eviction_config.rs:125 | Low water mark. Eviction stops once usage falls below this. |
TOPGUN_EVICTION_INTERVAL_MS | u64 | 1000 | storage/eviction_config.rs:145 | Eviction orchestrator tick interval. |
Write-behind buffer
| Variable | Type | Default | Consumer | Notes |
|---|---|---|---|---|
TOPGUN_WRITEBEHIND_FLUSH_INTERVAL_MS | u64 | 1000 | storage/datastores/write_behind.rs:79 | How often the write-behind buffer flushes to the durable backend. |
TOPGUN_WRITEBEHIND_BATCH_SIZE | usize | 100 | storage/datastores/write_behind.rs:99 | Maximum records flushed per write-behind tick. |
TOPGUN_WRITEBEHIND_CAPACITY | usize | 10000 | storage/datastores/write_behind.rs:119 | Bounded buffer size. Producers apply backpressure when full. |
At startup the server emits a single tracing::info! line containing the effective max_ram_mb, water marks, eviction interval, and write_behind_enabled so operators can confirm the active configuration without reading source.
Endpoints
Health and observability
| Endpoint | Method | Description |
|---|---|---|
/health | GET | Detailed health JSON (server state, uptime, connection count). |
/health/live | GET | Kubernetes liveness probe. Returns 200 if the process is alive. |
/health/ready | GET | Kubernetes readiness probe. Returns 200 once the server accepts connections. |
/metrics | GET | Prometheus metrics endpoint (text/plain). |
Client communication
| Endpoint | Method | Description |
|---|---|---|
/ws | GET (Upgrade) | WebSocket upgrade. Primary real-time channel for CRDT sync, queries, and pub/sub. Sends AUTH_REQUIRED on connect when auth is enabled. |
/sync | POST | Stateless HTTP sync endpoint. Accepts MsgPack-encoded batched operations and returns deltas. |
Admin API
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/status | GET | none | Server status (version, uptime, node info). |
/api/auth/login | POST | none | Admin login. Returns JWT for admin API access. |
/api/admin/cluster/status | GET | admin JWT | Cluster membership and partition status. |
/api/admin/maps | GET | admin JWT | List maps with record counts and partition info. |
/api/admin/settings | GET, PUT | admin JWT | Read or update server configuration at runtime. |
/api/docs | GET | none | Swagger UI for the admin API (OpenAPI spec at /api/openapi.json). |
/admin/* | GET | none | SPA admin dashboard. Serves static files from TOPGUN_ADMIN_DIR. |
Rust embed API
For callers that want to host the server in-process (custom binaries, integration tests, embedded scenarios), the building blocks live in topgun_server::network.
NetworkConfig
struct NetworkConfig {
host: String, // default "0.0.0.0"
port: u16, // default 0 (OS-assigned)
tls: Option<TlsConfig>, // optional TLS (cert_path, key_path, ca_cert_path)
connection: ConnectionConfig, // per-connection settings
cors_origins: Vec<String>, // default [] (deny all cross-origin); set TOPGUN_CORS_ORIGINS env to a comma-separated list
request_timeout: Duration, // default 30s
}
ConnectionConfig fields: outbound_channel_capacity (256), send_timeout (5s), idle_timeout (60s), ws_write_buffer_size (128 KB), ws_max_write_buffer_size (512 KB).
ServerConfig
struct ServerConfig {
node_id: String,
default_operation_timeout_ms: u64, // default 30000
max_concurrent_operations: u32, // default 1000
gc_interval_ms: u64, // default 60000
partition_count: u32, // default 271
security: SecurityConfig,
}
Embedding the server
use topgun_server::network::config::NetworkConfig;
use topgun_server::network::module::NetworkModule;
let config = NetworkConfig {
host: "0.0.0.0".to_string(),
port: 8080,
tls: None,
..Default::default()
};
let mut module = NetworkModule::new(config);
let port = module.start().await?;
println!("Server listening on port {port}");
module.serve(shutdown_signal()).await?;
PostgresDataStore
use sqlx::PgPool;
use topgun_server::storage::datastores::PostgresDataStore;
let pool = PgPool::connect("postgres://user:pass@localhost/topgun").await?;
let store = PostgresDataStore::new(pool, None)?; // default table: "topgun_maps"
store.initialize().await?; // CREATE TABLE IF NOT EXISTS
PostgresDataStore::new(pool, table_name) takes an optional custom table name; if supplied, it is validated against ^[a-zA-Z_][a-zA-Z0-9_]*$. Call initialize() after construction to run the schema migration.
POST /sync wire format
The /sync endpoint is the stateless HTTP fallback for environments without WebSocket support (Cloudflare Workers, restrictive proxies, serverless). It accepts a batch of operations and returns deltas in a single request/response cycle.
- URL:
POST /sync - Content-Type:
application/x-msgpack(binary MsgPack) - Response Content-Type:
application/msgpack
HttpSyncRequest
interface HttpSyncRequest {
clientId: string;
clientHlc: Timestamp; // { millis, counter, nodeId }
operations?: ClientOp[]; // mapName, key, record { value, timestamp }
syncMaps?: SyncMapEntry[]; // { mapName, lastSyncTimestamp }
queries?: HttpQueryRequest[]; // { queryId, mapName, filter, limit?, offset? }
searches?: HttpSearchRequest[];
}
HttpSyncResponse
interface HttpSyncResponse {
serverHlc: Timestamp;
ack?: { lastId: string; results?: AckResult[] };
deltas?: MapDelta[]; // { mapName, records: DeltaRecord[], serverSyncTimestamp }
queryResults?: HttpQueryResult[];
searchResults?: HttpSearchResult[];
errors?: HttpSyncError[]; // { code, message, context? }
}
Example request (shown as JSON for readability)
{
"clientId": "client-1",
"clientHlc": { "millis": 1706000000000, "counter": 0, "nodeId": "client-1" },
"operations": [
{
"mapName": "todos",
"key": "t1",
"record": {
"value": { "text": "Buy milk" },
"timestamp": { "millis": 1706000000000, "counter": 1, "nodeId": "client-1" }
}
}
],
"syncMaps": [
{
"mapName": "todos",
"lastSyncTimestamp": { "millis": 1705999000000, "counter": 0, "nodeId": "" }
}
]
}
Example response
{
"serverHlc": { "millis": 1706000001000, "counter": 1, "nodeId": "server-1" },
"ack": {
"lastId": "http-op-0",
"results": [{ "opId": "http-op-0", "success": true, "achievedLevel": "MEMORY" }]
},
"deltas": [
{
"mapName": "todos",
"records": [
{
"key": "t2",
"record": {
"value": { "text": "Walk the dog" },
"timestamp": { "millis": 1706000000500, "counter": 0, "nodeId": "client-2" }
},
"eventType": "PUT"
}
],
"serverSyncTimestamp": { "millis": 1706000001000, "counter": 2, "nodeId": "server-1" }
}
],
"queryResults": [],
"searchResults": [],
"errors": []
}
Graceful shutdown
On SIGTERM or SIGINT the server:
- Transitions health state to Draining.
- Sends a Close frame to every active WebSocket connection.
- Gives in-flight requests up to 30 seconds to complete.
- Transitions health state to Stopped.
The write-behind buffer flushes pending writes during the drain window; crash-safe shutdown drain plus WAL recovery is tracked as a follow-up item (see Concepts → Reliability for the current durability model).