Deployment & Operations
Deployment & Operations
Operational reference for the Rust streaming backend (streaming-backend) — covering process startup, environment-based configuration, database migrations, Redis initialization, dual-protocol serving (Axum HTTP + gRPC-Web), background maintenance jobs, and the failure modes operators must plan for.
Purpose and Scope
This page documents how the Rust backend is configured, started, and operated in a deployment environment. It covers the full boot sequence in streaming-backend/src/main.rs, the environment-variable contract in streaming-backend/src/config.rs, the runtime topology (PostgreSQL, Redis, embedded SFU, background tasks), and operational concerns such as fail-fast validation, secrets handling, and periodic cleanup.
Intentionally left to sibling pages in the rust-backend catalog: the HTTP router/middleware internals, the gRPC service definitions and handlers, the data model and persistence layer, and the per-service business logic (auth, chat, signals, media). Those pages describe what the servers do; this page describes how they are brought up and kept running.
Overview
The backend is a single Rust binary that, at startup, connects to PostgreSQL and Redis, runs database migrations, constructs the shared application state, and then serves traffic on two protocols simultaneously:
- Axum HTTP server on
HOST:PORT(default0.0.0.0:8080) — REST/WebSocket-style traffic throughrouter::build_router. - gRPC-Web server on
HOST:GRPC_PORT(default0.0.0.0:50051) — started viagrpc::server::serve.
Everything is configured through environment variables (with a .env file loaded first as a convenience for local development). The process is designed to fail fast: required settings such as DATABASE_URL panic at startup if missing, and any connectivity failure to PostgreSQL or Redis aborts the boot before traffic is accepted.
Beyond serving, the process owns several operational responsibilities that run in background Tokio tasks:
- Identity key backfill — runs once at startup to derive deterministic identity keys for users missing a key bundle.
- Status purger — every 600 seconds, expires stale status entries via
StatusService::purge_expired_statuses. - SFU signal relay — forwards embedded SFU (str0m) signals to the call relay over an unbounded channel.
The key design intent: a single process owns the whole signaling/streaming lifecycle, so deployment is one binary plus two external dependencies (PostgreSQL, Redis). There is no external SFU or sidecar process to orchestrate; the media router is embedded.
Architecture
The following diagram shows the runtime topology and the startup dependency graph as implemented in main.rs and config.rs:
Component roles
| Component | Role | Evidence |
|---|---|---|
Config::from_env() |
Reads all runtime settings from environment variables with sane local-dev defaults | config.rs |
db::create_pool() |
Opens the PostgreSQL connection pool from DATABASE_URL |
main.rs |
db::run_migrations_safely() |
Applies schema migrations at startup, before serving | main.rs |
fred RedisClient |
Connection to Redis used by call tickets, relay, and signaling | main.rs |
AppStateData |
Single Arc-shared state handed to both HTTP and gRPC servers |
main.rs |
| Axum HTTP server | Primary traffic surface, bound via TcpListener on HOST:PORT |
main.rs |
| gRPC-Web server | Secondary traffic surface, spawned as a Tokio task on GRPC_PORT |
main.rs |
The design deliberately shares one AppStateData across both serving stacks so gRPC handlers and HTTP handlers see the same in-memory services (Snowflake ID generator, refresh tokens, call tickets, relay, SFU, storage) — there is no duplicated state and no cross-process synchronization needed.
Core Flow — Process Startup Sequence
The boot sequence is strictly ordered so that the process never accepts traffic before its dependencies are ready. main() awaits each initialization step and propagates errors with ?, so any failure aborts startup and the process exits non-zero (fail-fast).
Step-by-step walkthrough
-
Load
.env(best-effort) —dotenv().ok()loads a local.envfile but ignores the error when it is absent. This keeps local development ergonomic while production uses real environment variables injected by the platform (main.rs). -
Initialize logging —
tracing_subscriber::fmt::init()installs the tracing formatter; all subsequentinfo!output (e.g.,"Starting Secure Mesh Backend","HTTP → 0.0.0.0:8080") goes to stdout via this subscriber (main.rs). -
Read configuration —
Config::from_env()resolves every setting. MissingDATABASE_URLor non-numericPORT/GRPC_PORT/SNOWFLAKE_MACHINE_IDpanics immediately — the operator is told what is wrong before anything else happens (config.rs). -
PostgreSQL pool + migrations —
db::create_pool(&cfg.database_url)opens the pool, thendb::run_migrations_safely(&db_pool)applies schema migrations. Migrations run on every boot, so a deploy and a migration are the same operation — no separate migration step is required (main.rs). -
Redis connection — the fred client is built from
REDIS_URLand explicitly initialized withredis_client.init().await?. A Redis outage therefore blocks startup rather than causing mid-flight failures (main.rs). -
Snowflake ID service —
SnowflakeService::verify_and_init(&cfg.database_url, cfg.snowflake_machine_id)verifies/initializes the machine ID against the database. The comment in source warns the service must be instantiated exactly once — the singleArcis shared withRefreshTokenService(main.rs). -
Service construction — call tickets and relay are built against Redis; the embedded SFU (
str0m) is created with an unbounded MPSC channel whose receiver forwards every signal to the call relay in a dedicated task (main.rs). -
Shared state — all services are packed into
Arc<AppStateData>together withjwt_secretandstart_time(used for uptime/health reporting) (main.rs). -
Background tasks — identity key backfill is spawned once; the status purger loops every 600 seconds calling
StatusService::purge_expired_statuses(main.rs). -
Start servers — the gRPC-Web server is spawned as a Tokio task on
GRPC_PORT, thenmain()binds the Axum listener onPORTand blocks inaxum::serve(...), which keeps the process alive until the HTTP server terminates (main.rs).
Environment-Driven Configuration
Config is a plain Clone + Debug struct; there is no config file, no CLI parser, and no runtime reload. Every field maps 1 to an environment variable, which makes container orchestration straightforward: the image is immutable, and everything environment-specific is injected at deploy time.
#[derive(Clone, Debug)]
pub struct Config {
pub host: String,
pub port: u16,
pub grpc_port: u16,
pub database_url: String,
pub redis_url: String,
pub snowflake_machine_id: i32,
pub jwt_secret: String,
#[allow(dead_code)] // used to select storage backend (local vs s3)
pub storage_backend: String,
pub local_upload_dir: String,
}
Source: config.rs
Two distinct defaulting strategies are used, and the difference matters operationally:
- Soft defaults (
unwrap_or_else) —HOST,PORT,GRPC_PORT,REDIS_URL,SNOWFLAKE_MACHINE_ID,JWT_SECRET,STORAGE_BACKEND,LOCAL_UPLOAD_DIRfall back to values tuned for local development. - Hard requirement (
expect) —DATABASE_URLhas no default; the process refuses to start without it. This is deliberate: every other service depends on the database, so there is no point booting into a half-configured state.
The default JWT_SECRET ("super-secret-default-key-change-in-prod") is explicitly named as a development placeholder — an operator who deploys without overriding it ships a well-known signing key. The STORAGE_BACKEND field carries #[allow(dead_code)] with a comment indicating it selects between local and S3 storage, signaling an intended extension point that is not yet wired into the boot path.
Usage Examples
Example 1 — Full boot sequence (entry point)
This is the entire startup path an operator observes: configuration resolution, dependency connection, migration, service construction, and server launch.
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load .env file first — ignores error if file is absent (production uses real env vars)
dotenv().ok();
tracing_subscriber::fmt::init();
let cfg = Config::from_env();
info!("Starting Secure Mesh Backend");
let db_pool = db::create_pool(&cfg.database_url).await?;
db::run_migrations_safely(&db_pool).await?;
// Build fred RedisClient and connect
let fred_config = FredConfig::from_url(&cfg.redis_url)?;
let redis_client = Builder::from_config(fred_config).build()?;
redis_client.init().await?;
let snowflake = Arc::new(SnowflakeService::verify_and_init(&cfg.database_url, cfg.snowflake_machine_id).await?);
Source: main.rs
Example 2 — Dual-protocol serving
Note the asymmetry: gRPC is spawned as a fire-and-forget Tokio task, while the Axum HTTP server is bound first and then awaited on the main task, keeping the process alive.
// Spawn gRPC (gRPC-Web) server
let grpc_addr = format!("{}:{}", cfg.host, cfg.grpc_port).parse()?;
tokio::spawn(crate::grpc::server::serve(state.clone(), grpc_addr));
let cleanup_state = state.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(600));
loop {
interval.tick().await;
let _ = crate::modules::status::service::StatusService::purge_expired_statuses(&cleanup_state).await;
}
});
// Spawn Axum HTTP service on PORT
let app = router::build_router(state);
let http_addr = format!("{}:{}", cfg.host, cfg.port);
let listener = TcpListener::bind(&http_addr).await?;
info!("HTTP → {}", http_addr);
axum::serve(listener, app).await?;
Ok(())
}
Source: main.rs
Example 3 — Configuration resolution patterns
This excerpt shows the two defaulting strategies and the validation errors operators will see:
pub fn from_env() -> Self {
Self {
host: env::var("HOST").unwrap_or_else(|_| "0.0.0.0".into()),
port: env::var("PORT")
.unwrap_or_else(|_| "8080".into())
.parse()
.expect("PORT must be a number"),
grpc_port: env::var("GRPC_PORT")
.unwrap_or_else(|_| "50051".into())
.parse()
.expect("GRPC_PORT must be a number"),
database_url: env::var("DATABASE_URL")
.expect("DATABASE_URL must be set"),
redis_url: env::var("REDIS_URL")
.unwrap_or_else(|_| "redis://127.0.0.1:6379".into()),
snowflake_machine_id: env::var("SNOWFLAKE_MACHINE_ID")
.unwrap_or_else(|_| "0".into())
.parse()
.expect("SNOWFLAKE_MACHINE_ID must be a number"),
jwt_secret: env::var("JWT_SECRET")
.unwrap_or_else(|_| "super-secret-default-key-change-in-prod".into()),
storage_backend: env::var("STORAGE_BACKEND")
.unwrap_or_else(|_| "local".into()),
local_upload_dir: env::var("LOCAL_UPLOAD_DIR")
.unwrap_or_else(|_| "./uploads".into()),
}
}
Source: config.rs
Configuration Options
| Environment Variable | Type | Default | Required | Description |
|---|---|---|---|---|
HOST |
string | "0.0.0.0" |
No | Bind address for both the Axum HTTP and gRPC-Web servers |
PORT |
u16 | 8080 |
No | Port for the Axum HTTP server; must parse as a number or startup panics |
GRPC_PORT |
u16 | 50051 |
No | Port for the gRPC-Web server; must parse as a number or startup panics |
DATABASE_URL |
string | — | Yes | PostgreSQL connection string; process panics with "DATABASE_URL must be set" if absent |
REDIS_URL |
string | "redis://127.0.0.1:6379" |
No | Redis connection URL consumed by the fred client |
SNOWFLAKE_MACHINE_ID |
i32 | 0 |
No | Snowflake machine/worker ID used by SnowflakeService::verify_and_init |
JWT_SECRET |
string | "super-secret-default-key-change-in-prod" |
No | JWT signing secret; the default is a development placeholder and must be overridden in production |
STORAGE_BACKEND |
string | "local" |
No | Selects storage backend (local vs S3); currently reserved/not yet wired into boot |
LOCAL_UPLOAD_DIR |
string | "./uploads" |
No | Directory used by LocalStorage for file uploads |
Configuration semantics
- Validation is fail-fast and synchronous. Three fields (
PORT,GRPC_PORT,SNOWFLAKE_MACHINE_ID) use.parse().expect(...)with descriptive panic messages, andDATABASE_URLuses.expect(...)directly. Operators get immediate, actionable errors during image startup instead of runtime misbehavior. .envis a convenience, not a deployment contract.dotenv().ok()swallows the missing-file error, so production deployments can rely purely on injected environment variables.- Secrets travel in env vars.
JWT_SECRETis read from the environment and stored inAppStateDatafor the request pipeline; no secret material is written to disk by the application itself.
Failure Modes, Edge Cases & Concurrency
Startup failure modes (fail-fast)
| Failure | Mechanism | Operator impact |
|---|---|---|
DATABASE_URL unset |
expect("DATABASE_URL must be set") panics in Config::from_env |
Process exits immediately with a clear message; fix env and restart |
Non-numeric PORT / GRPC_PORT / SNOWFLAKE_MACHINE_ID |
.parse().expect(...) panics |
Same fail-fast behavior with field-specific message |
| PostgreSQL unreachable | db::create_pool(...).await? returns Err |
Startup aborts; no traffic is served — DB availability is a hard prerequisite |
| Migration failure | db::run_migrations_safely(...).await? propagates error |
Startup aborts; deployment is blocked until schema issue is resolved |
| Redis unreachable | redis_client.init().await? returns Err |
Startup aborts before serving; Redis must be up before the binary |
| Port already in use | TcpListener::bind(&http_addr).await? returns Err |
Process exits with the OS bind error (e.g., Address already in use) |
The consistent pattern: every dependency error is a startup error. There are no retry loops in main() for database or Redis — the orchestrator (container platform / supervisor) is expected to restart the process. Retry-on-restart keeps the boot path simple and deterministic.
Concurrency model
- Single shared state.
AppStateDatais wrapped inArcand cloned into the gRPC task, the HTTP router, and the background tasks. All in-process handlers observe the same service instances — no per-request copies, no split-brain between protocols (main.rs). Mutex-protected services.FileDetectServiceis constructed asArc<Mutex<FileDetectService>>, indicating interior mutability that must be serialized across handlers (main.rs).- Unbounded channel for SFU signals. The SFU-to-relay path uses
tokio::sync::mpsc::unbounded_channel(main.rs). Unbounded means the producer never blocks, but under a signal burst the queue can grow without backpressure — a consideration for capacity planning under heavy call volume. - Single-instance Snowflake. The comment in source explicitly warns
verify_and_initmust never be called twice; the service is constructed once and shared (main.rs). Operators must not run two replicas with the sameSNOWFLAKE_MACHINE_IDagainst the same database without understanding the ID-uniqueness contract. - Background task isolation. The status purger runs in its own task with a fixed 600-second interval and discards errors (
let _ = ...), so a transient purge failure never crashes the process (main.rs).
Edge cases
- Missing
.envis fine —dotenv().ok()ignores the error, so containers without a.envfile boot normally (main.rs). - Default
JWT_SECRETis a live risk — the shipped default"super-secret-default-key-change-in-prod"is a known value; any production instance that forgets to override it has a publicly known signing key. start_timeuptime tracking —Instant::now()is captured at boot intoAppStateData, giving handlers a stable reference point for uptime/health reporting without a separate clock service (main.rs).
Performance & Operational Considerations
- Migrations run on every boot — startup cost scales with schema size; keep migrations idempotent (
run_migrations_safely) so concurrent replicas or redeploys do not conflict. - One binary, two ports, one process — the HTTP server is the lifecycle owner (
main()awaitsaxum::serve); the gRPC server is a spawned task. When the HTTP server terminates,main()returns and the whole process — including the gRPC task — shuts down. There is no separate lifecycle management for the two protocol stacks. - Logging —
tracing_subscriber::fmt::init()writes formatted tracing output to stdout; collect container stdout as the primary log stream (main.rs). - Periodic maintenance — the status purger runs every 600 seconds; consider aligning observability alerts with this cadence when monitoring table growth.
- Startup ordering matters for probes — because the HTTP listener is bound last (after DB, Redis, migrations, and service construction), a successful bind on
PORTimplies the entire dependency chain is healthy. A TCP readiness probe onPORTis therefore a meaningful signal; HTTP responses onGRPC_PORTconfirm the gRPC stack. - No graceful-shutdown handler observed —
axum::serveis awaited directly without an explicittokio::signalhandler inmain(). Termination relies on the runtime/container platform’s default SIGTERM behavior. (Verified only formain(); a shutdown hook may exist deeper in the router or server modules, which are outside this page’s evidence.)
Extension Points
- Storage backend selection —
STORAGE_BACKENDis declared with#[allow(dead_code)]and a comment noting it selects local vs S3; this is the intended seam for plugging in a new storage implementation without changing the boot contract (config.rs). - Module-based feature growth — the crate is organized into
modules/(e.g.,modules::status::service) plusgrpc/,middleware/,services/, androuter/(main.rs). New operational jobs follow the same pattern: construct inmain(), share viaAppStateData, spawn as a Tokio task. - Configuration surface is env-driven by design — adding a new operational knob means adding a field to
Configand a line infrom_env(); no config-file schema or CLI migration is required.
Related Links
Source files (primary evidence for this page)
- streaming-backend/src/main.rs — process entry point, full boot sequence, dual-protocol serving, background tasks
- streaming-backend/src/config.rs —
Configstruct andfrom_env()environment contract - streaming-backend/src/db.rs —
create_poolandrun_migrations_safely(called frommain()) - streaming-backend/src/grpc/server.rs — gRPC-Web server launched as a Tokio task
- streaming-backend/src/router.rs — Axum router built by
router::build_router(file referenced viamod routerin main.rs) - streaming-backend/src/state.rs —
AppStateDatadefinition (referenced in main.rs)
Related catalog pages
- HTTP Router & Middleware (
rust-backend.http-router-middleware) — request pipeline, auth and rate-limit middleware that consumeAppStateData. - gRPC Services (
rust-backend.grpc-services) — service handlers served onGRPC_PORT, including auth, chat, history, and signals. - Data Models & Persistence (
rust-backend.data-models) — entities migrated byrun_migrations_safelyat boot. - Realtime Core — the browser-side counterpart (
realtime-core/src/) that connects to these servers; its transport and signaling behavior determines the operational load profile.