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):

FlagTypeDefaultDescription
--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>u168080 (or env PORT)Client WebSocket port. 0 means OS-assigned. Also reads the PORT environment variable.
--cluster-port <PORT>u160 (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

VariableTypeDefaultConsumerNotes
STORAGE_BACKENDstring"redb"bin/topgun_server.rs:109One of redb or null; any other value is refused at startup. null is in-memory ephemeral (test only).
TOPGUN_DATA_DIRpathtopgun-data beside TOPGUN_REDB_PATH when that is set, else ./topgun-datastorage/datastores/redb.rsThe store directory (used when STORAGE_BACKEND=redb); it holds engine.redb. A set-but-empty value is refused at startup.
TOPGUN_REDB_PATHpath./topgun.redbstorage/datastores/redb.rsThe 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_DIRpath./topgun-walstorage/datastores/write_behind.rsDirectory of the write-ahead log. Keep it on the same volume as the data directory.

Networking

VariableTypeDefaultConsumerNotes
PORTu168080bin/topgun_server.rs:168 (via #[arg(env = "PORT")])Client WebSocket port. Equivalent to --port.
TOPGUN_BIND_ADDRstring"0.0.0.0"bin/topgun_server.rs:203Bind address for the client WebSocket listener. Override to 127.0.0.1 for loopback-only deployments.

Authentication

VariableTypeDefaultConsumerNotes
JWT_SECRETstringnonenetwork/module.rs:454HMAC secret for signing/verifying JWTs. Required for production auth.
TOPGUN_NO_AUTHtruthyunsetbin/topgun_server.rs:477If set ("true", "1"), disables auth. Test and demo use only.
INSECURE_FORWARD_AUTH_ERRORStruthyunsetbin/topgun_server.rs:470If set, forwards upstream auth errors verbatim in responses. Insecure; do not enable in production.
TOPGUN_ADMIN_USERNAMEstring"admin"network/handlers/admin.rs:111Username for the admin dashboard login.
TOPGUN_ADMIN_PASSWORDstringrequired if admin enablednetwork/handlers/admin.rs:112Password for the admin dashboard login.
TOPGUN_ADMIN_DIRpathbundled SPA (npm) / ./admin-dashboard/dist (monorepo)network/module.rs:478Static-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

VariableTypeDefaultConsumerNotes
TOPGUN_INDEX_PATHpathnonebin/topgun_server.rs:516, network/module.rs:316, network/handlers/admin.rs:1078Directory holding the BM25 full-text-search index.
TOPGUN_VECTOR_INDEX_PATHpathnonenetwork/module.rs:286, network/handlers/admin.rs:1139Directory holding the HNSW vector index.

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.

VariableTypeDefaultNotes
TOPGUN_EMBEDDING_PROVIDERstringunset (disabled)One of ollama, openai (alias http), deterministic, noop. Unset / none disables semantic search.
TOPGUN_EMBEDDING_MODELstringprovider defaultEmbedding model name. Ollama default: nomic-embed-text. Required for openai/http.
TOPGUN_EMBEDDING_BASE_URLstringprovider defaultEndpoint base URL. Ollama default: http://localhost:11434. Required for openai/http.
TOPGUN_EMBEDDING_API_KEYstringnoneBearer token for openai/http providers.
TOPGUN_EMBEDDING_DIMENSIONu16provider defaultOutput vector dimension; must match the model. Defaults: ollama 768, deterministic 256, noop 4. Required for openai/http.
TOPGUN_VECTOR_MAPSJSONnoneMaps 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

VariableTypeDefaultConsumerNotes
RUST_LOGfilter 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_FORMATstringtextservice/middleware/observability.rs:130Set to "json" for structured JSON logs. Anything else uses the text formatter.

Memory and eviction

VariableTypeDefaultConsumerNotes
TOPGUN_MAX_RAM_MBu641024storage/eviction_config.rs:85RAM ceiling for the in-memory record cache.
TOPGUN_EVICTION_HIGH_PCTu885storage/eviction_config.rs:105High water mark (% of TOPGUN_MAX_RAM_MB). Eviction engages above this threshold.
TOPGUN_EVICTION_LOW_PCTu870storage/eviction_config.rs:125Low water mark. Eviction stops once usage falls below this.
TOPGUN_EVICTION_INTERVAL_MSu641000storage/eviction_config.rs:145Eviction 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.

VariableTypeDefaultConsumerNotes
TOPGUN_WRITEBEHIND_FLUSH_INTERVAL_MSu641000WriteBehindConfig::flush_interval_msHow often the write-behind buffer flushes to the durable backend.
TOPGUN_WRITEBEHIND_BATCH_SIZEu32100WriteBehindConfig::batch_sizeAccepted 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_CAPACITYu6410000WriteBehindConfig::capacitySoft 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

EndpointMethodDescription
/healthGETDetailed health JSON (server state, uptime, connection count).
/health/liveGETKubernetes liveness probe. Returns 200 if the process is alive.
/health/readyGETKubernetes readiness probe. Returns 200 once the server accepts connections.
/metricsGETPrometheus metrics endpoint (text/plain).

Client communication

EndpointMethodDescription
/wsGET (Upgrade)WebSocket upgrade. Primary real-time channel for CRDT sync, queries, and pub/sub. Sends AUTH_REQUIRED on connect when auth is enabled.
/syncPOSTStateless HTTP sync endpoint. Accepts MsgPack-encoded batched operations and returns deltas.

Admin API

EndpointMethodAuthDescription
/api/statusGETnoneServer status (version, uptime, node info).
/api/auth/loginPOSTnoneAdmin login. Returns JWT for admin API access.
/api/admin/cluster/statusGETadmin JWTCluster membership and partition status.
/api/admin/mapsGETadmin JWTList 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/settingsGET, PUTadmin JWTRead or update server configuration at runtime.
/api/openapi.jsonGETnoneOpenAPI spec of the admin API, as JSON.
/admin/*GETnoneSPA 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:

  1. Transitions health state to Draining.
  2. Sends a Close frame to every active WebSocket connection.
  3. Gives in-flight requests up to 30 seconds to complete.
  4. 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).


← Adapters · MCP →