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 (the embedded redb store).
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 database to run, no Docker required). It binds to 0.0.0.0:8080 and keeps its store in the data directory ./topgun-data in the working directory, with its write-ahead log in ./topgun-wal.
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. Must not contain :, | or # — the server refuses to start if it does. The default is always valid. |
--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 or null; any other value is refused at startup. null is in-memory ephemeral (test only). |
TOPGUN_DATA_DIR | path | topgun-data beside TOPGUN_REDB_PATH when that is set, else ./topgun-data | storage/datastores/redb.rs | The store directory (used when STORAGE_BACKEND=redb); it holds engine.redb. A set-but-empty value is refused at startup. |
TOPGUN_REDB_PATH | path | ./topgun.redb | storage/datastores/redb.rs | The store file of earlier versions: migrated into the data directory once and kept as {path}.pre-migration; a small marker file then stays at this path so that an older binary refuses to start. Not the store. A set-but-empty value is refused at startup. See Storage backends. |
TOPGUN_WAL_DIR | path | ./topgun-wal | storage/datastores/write_behind.rs | Directory of the write-ahead log. Keep it on the same volume as the data directory. |
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. |
Embeddings and semantic search
Semantic / vector / hybrid search requires a configured embedding provider. When
TOPGUN_EMBEDDING_PROVIDER is unset, the 'semantic' method is disabled and
returns an explicit error instead of empty or meaningless results — 'exact' and
'fullText' are unaffected. The server logs the active provider, dimension, and
auto-embedded maps in a single config line at startup.
| Variable | Type | Default | Notes |
|---|---|---|---|
TOPGUN_EMBEDDING_PROVIDER | string | unset (disabled) | One of ollama, openai (alias http), deterministic, noop. Unset / none disables semantic search. |
TOPGUN_EMBEDDING_MODEL | string | provider default | Embedding model name. Ollama default: nomic-embed-text. Required for openai/http. |
TOPGUN_EMBEDDING_BASE_URL | string | provider default | Endpoint base URL. Ollama default: http://localhost:11434. Required for openai/http. |
TOPGUN_EMBEDDING_API_KEY | string | none | Bearer token for openai/http providers. |
TOPGUN_EMBEDDING_DIMENSION | u16 | provider default | Output vector dimension; must match the model. Defaults: ollama 768, deterministic 256, noop 4. Required for openai/http. |
TOPGUN_VECTOR_MAPS | JSON | none | Maps auto-vectorized on write: {"docs":{"fields":["title","body"],"dimension":768}}. Each map’s dimension must equal the provider dimension. |
deterministic produces stable hashed bag-of-words vectors with no external
dependency — intended for tests and local experiments, not production semantics.
noop returns all-zero vectors and logs a loud warning; it is a development-only
mode and never the default. Use ollama or openai for real semantic search.
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
Writes the server acknowledged during a storage outage stay readable and are stored when the storage returns: a write the storage refuses stays in the write-behind buffer and is retried until the storage takes it, however long that is. The price is back-pressure. Nothing leaves the buffer while the storage refuses, so once TOPGUN_WRITEBEHIND_CAPACITY keys are buffered the server refuses writes of new keys — as a temporary error, so the client keeps them and sends them again — until the storage returns; writes of keys that are already buffered are still accepted. Watch topgun_write_behind_buffered_entries against the capacity. topgun_write_behind_flush_refusals_total counts the refused storage calls, topgun_write_behind_retrying_entries the buffered writes refused at least once, topgun_write_behind_oldest_refusal_age_seconds how long the oldest of them has been refused, and topgun_write_behind_store_hold is 1 while the server is leaving a refusing storage alone. topgun_write_behind_flush_passes_total, topgun_write_behind_flush_pass_entries_total and the histogram topgun_write_behind_flush_pass_seconds show how many flush passes were committed, how many writes they stored and how long each took. A record the storage never accepts is retried for as long as the server runs and keeps its place in the buffer.
| Variable | Type | Default | Consumer | Notes |
|---|---|---|---|---|
TOPGUN_WRITEBEHIND_FLUSH_INTERVAL_MS | u64 | 1000 | WriteBehindConfig::flush_interval_ms | How often the write-behind buffer flushes to the durable backend. |
TOPGUN_WRITEBEHIND_BATCH_SIZE | u32 | 100 | WriteBehindConfig::batch_size | Accepted and without effect: a flush pass takes at most 1000 entries or 8 MiB of encoded values, whichever comes first — fixed limits — and stores them in one durable commit, so the value neither groups nor limits the pass. It is not honoured on purpose: the documented example value 100 would cut every pass tenfold on deployments that copied it. |
TOPGUN_WRITEBEHIND_CAPACITY | u64 | 10000 | WriteBehindConfig::capacity | Soft bound on buffered keys, so the write buffer cannot grow without limit: a write of a key that is not already buffered is refused once this many keys are buffered (0 means unlimited). It is not a hard limit — the count can exceed it by the writes of new keys that passed the check at the same moment (W), plus one re-write of each key the running flush pass holds (H): buffered <= C - 1 + W + H, with H <= min(L, C - 1 + W), because a pass holds at most L = 1000 of the entries that were due when it began, and H <= 1 while the server is leaving a refusing store alone. In the worst case the count reaches up to about twice the capacity. A key stays counted until its entry is persisted or retired into a newer write of the same key; an entry the store refuses is neither, and stays counted until the store takes it. So while the store refuses writes the buffer stays at its bound, and every write of a key that is not already buffered is refused until the store returns. A refused write is not acknowledged: the server answers with the transient internal error (code 500), and the client keeps the operation and sends it again. TOPGUN_WRITEBEHIND_CAPACITY=0 together with a store that refuses writes is not supported: nothing then limits the buffer, and memory grows for as long as the store refuses. |
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 the store’s maps, reserved maps left out; entryCount is the in-memory record count (0 for a map not loaded). Empty on STORAGE_BACKEND=null. |
/api/admin/settings | GET, PUT | admin JWT | Read or update server configuration at runtime. |
/api/openapi.json | GET | none | OpenAPI spec of the admin API, as 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?;
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).