Backend Architecture & Bootstrap
Backend Architecture & Bootstrap
The Secure Mesh Rust backend (streaming-backend) is a zero-trust, end-to-end encrypted real-time messaging and calling server built on Axum (REST) + Tonic (gRPC-Web) with PostgreSQL persistence, Redis pub/sub signaling, an embedded Rust SFU (str0m), and a single async bootstrap sequence in src/main.rs that wires every service into shared application state before serving traffic.
Purpose and Scope
This page documents the overall architecture and startup/bootstrap mechanism of the Rust backend:
- The layered module layout under
streaming-backend/src/(HTTP router, gRPC plane, middleware, models, modules, services, state). - The complete bootstrap sequence in
src/main.rs— from.envloading and configuration, through database migrations, Redis connection, Snowflake initialization, service construction, background task spawning, to the final Axum/gRPC server start. - Shared application state (
AppStateData) and how every subsystem is wired together. - Runtime topology: HTTP port, gRPC port, PostgreSQL, and Redis responsibilities.
The following topics are intentionally left to sibling pages: individual gRPC service contracts (see the gRPC services page), the data model / SQLx entities (see Data Models & Persistence), middleware behavior (see Middleware & Rate Limiting), and the realtime signaling / SFU media plane (see Realtime Signaling & SFU). This page only covers them where they participate in bootstrap.
Overview
Secure Mesh is an end-to-end encrypted, real-time multimedia messaging and calling application. The backend is a single Rust binary (streaming-backend, edition 2024, Tokio async runtime) that runs two servers side by side:
- Axum 0.8.9 — HTTP REST API (auth, users, groups, uploads with a 10 MB body limit).
- Tonic 0.14.6 + tonic-web — the single realtime plane for chats and calls over gRPC-Web, fed by Redis
PSUBSCRIBE call_signals:*fan-out.
The backend follows one hard design constraint: every module must be a single-responsibility unit. This is why main.rs itself is a short orchestration script that delegates each concern (config, db, grpc, middleware, models, modules, router, services, state, utils) to a dedicated module, and why AppStateData exists as a single shared handle to all services.
Bootstrap design intent: every component (database pool, Redis, Snowflake, refresh tokens, call tickets, call relay, SFU, file detection, storage) is constructed before any server starts accepting traffic, so the process never serves a request with a half-initialized dependency graph. Fail-fast initialization, then concurrent runtime servers.
Architecture
The diagram mirrors the exact construction order in main.rs: configuration → database (pool + migrations) → Redis → Snowflake → services → shared state → servers. Each arrow is a real dependency created or consumed at startup.
Module Layout
Path (under streaming-backend/) |
Role in the architecture |
|---|---|
src/main.rs |
Entry point; orchestrates the full bootstrap sequence |
src/config.rs |
Config::from_env() — typed configuration loaded from environment / .env |
src/db.rs |
create_pool (SQLx PostgreSQL pool) and run_migrations_safely |
src/state.rs |
AppStateData — shared Arc handle injected into routers and servers |
src/router.rs |
Builds the Axum HTTP application (router::build_router) |
src/grpc/ |
Tonic gRPC-Web servers: server.rs entry, auth, chat_service, history, signals |
src/middleware/ |
auth_middleware, rate_limit (tower-governor) |
src/models/ |
SQLx entities: user, user_key, message, group |
src/modules/ |
Feature modules (e.g., auth/controller.rs, status service) |
src/services/ |
Domain services: snowflake, refresh_token, call_ticket, call_relay, sfu, file_detect, storage/local, identity |
src/error.rs, src/utils/ |
Shared error type and utilities |
build.rs |
Build-time hooks |
Bootstrap Walkthrough (main.rs)
The entire process lifetime is defined in #[tokio::main] async fn main() in main.rs. It executes the following stages, in order:
- Environment loading —
dotenv().ok()reads.envif present; the error is ignored because production supplies real environment variables. - Tracing —
tracing_subscriber::fmt::init()starts structured logging. - Configuration —
Config::from_env()parses all settings once; the resultingcfgis borrowed throughout startup. - Database —
db::create_pool(&cfg.database_url)builds the SQLx pool, thendb::run_migrations_safely(&db_pool)applies migrations before anything else touches the DB. - Redis — a
fredclient is built fromFredConfig::from_url(&cfg.redis_url)andredis_client.init().await?connects it. - Snowflake IDs —
SnowflakeService::verify_and_init(...)validates the machine ID with a PostgreSQL advisory lock, so only one valid ID generator exists. - Service construction —
RefreshTokenService,CallTicketService,CallRelayService,FileDetectService,LocalStorage, andSfuServiceare created and shared viaArc/Mutexwhere interior mutability is needed. - SFU relay wiring — an unbounded mpsc channel connects the embedded
str0mSFU to the call relay: outgoing signals are forwarded to Redis. - Shared state — everything is packed into
Arc<AppStateData>. - Background tasks — identity key backfill (for existing users missing key bundles) and a 600-second status-purge loop are spawned with
tokio::spawn. - Servers — the gRPC-Web server is spawned on
host:grpc_port, then Axum serves HTTP onhost:portandaxum::serveawaits forever (the process lives as long as the HTTP server does).
Usage Examples
Bootstrap skeleton: config → DB → Redis → services → state
The startup script shows the fail-fast ordering principle: every dependency is resolved and connected before the first server spawns.
#[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
Note the design intent of verify_and_init being called exactly once: the comment in the next excerpt warns to share the single SnowflakeService via Arc rather than calling verify_and_init twice, because the initialization is guarded by a PostgreSQL advisory lock — a second call would block or fail.
Shared service graph and single-instance discipline
let refresh_tokens = Arc::new(RefreshTokenService::new(
db_pool.clone(),
snowflake.clone(), // share the single SnowflakeService — never call verify_and_init twice
cfg.jwt_secret.clone(),
));
let call_tickets = Arc::new(CallTicketService::new(redis_client.clone()));
let call_relay = CallRelayService::new(redis_client.clone(), db_pool.clone(), &cfg.redis_url);
let file_detect = Arc::new(Mutex::new(FileDetectService::new()?));
let storage = Arc::new(LocalStorage::new(&cfg.local_upload_dir));
Source: main.rs
Key wiring facts:
RefreshTokenServicedepends on the DB pool, the shared Snowflake generator, and the JWT secret — it issues/rotates refresh tokens with Snowflake-derived IDs.CallTicketServiceandCallRelayServiceboth share the same Redis client (redis_client.clone()); the relay additionally holds the DB pool to persist call state.FileDetectServiceis wrapped inArc<Mutex<…>>because it maintains mutable internal state (e.g., MIME signatures) and is notSync-friendly; every other service is shared with plainArc.LocalStorageis a simple filesystem-backed upload store rooted atcfg.local_upload_dir.
Embedded SFU → Redis relay wiring
The str0m-based SFU never talks to Redis directly. A dedicated mpsc forwarding task bridges the two so the SFU’s send_signal API stays synchronous and decoupled:
// Embedded Rust SFU (str0m) — outgoing signals forward to the relay
let (sfu_tx, mut sfu_rx) = tokio::sync::mpsc::unbounded_channel();
let relay_for_sfu = call_relay.clone();
tokio::spawn(async move {
while let Some(sig) = sfu_rx.recv().await {
let _ = relay_for_sfu.send_signal(sig).await;
}
});
let sfu = crate::services::sfu::SfuService::new(sfu_tx).await?;
Source: main.rs
An unbounded channel is used deliberately: call-signaling messages are small and latency-sensitive, so dropping or backpressuring a signal could stall a WebRTC negotiation. The relay push is best-effort (let _ =), matching the pub/sub nature of Redis signaling.
Shared state and background tasks
let state = Arc::new(AppStateData {
db: db_pool,
redis: redis_client,
snowflake,
refresh_tokens,
call_tickets,
call_relay,
sfu,
file_detect,
storage,
jwt_secret: cfg.jwt_secret.clone(),
start_time: Instant::now(),
});
// Backfill deterministic identity keys for users missing a key bundle
// (server-derives from identity_seed so existing users get keys too).
let backfill_state = state.clone();
tokio::spawn(async move {
let _ = crate::services::identity::backfill_missing_keys(&backfill_state).await;
});
Source: main.rs
AppStateData is the single injection point for the whole process: every Axum handler and gRPC service receives this Arc and pulls only the pieces it needs. Background work (identity backfill, the 600-second status purge, the gRPC server itself) uses state.clone() so the same handle is shared without locking. start_time: Instant::now() records process uptime for health/observability endpoints.
Server startup order
// 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));
// 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
The gRPC-Web plane starts first on grpc_port (realtime must be ready), then the Axum HTTP server binds port and axum::serve blocks forever — the HTTP server is the process’s main lifetime anchor. If the HTTP bind fails, the process exits with an error; gRPC keeps running until then.
Core Flow: Process Startup Sequence
Stage-by-stage rationale:
- Migrations before servers — no request can observe a missing table, because
run_migrations_safelycompletes before any listener is bound. - Single Snowflake — the advisory-lock-verified generator is created once and shared; concurrent initialization is impossible by design (see the “never call
verify_and_inittwice” comment in main.rs). - Background tasks after state — backfill and purge loops only start once
AppStateDataexists, so they can clone it safely; both ignore errors withlet _ =because they are periodic, non-critical maintenance work. - gRPC before HTTP — realtime signaling is available before REST accepts traffic, then the HTTP server anchors the process.
Configuration Options
All settings are loaded once by Config::from_env() from environment variables (with .env file support via dotenv). The fields below are the ones consumed directly during bootstrap in main.rs; see the configuration page for the full variable list and defaults.
Field (on Config) |
Used by | Purpose |
|---|---|---|
database_url |
db::create_pool, SnowflakeService::verify_and_init |
PostgreSQL connection string for the SQLx pool and advisory-lock validation |
redis_url |
FredConfig::from_url |
Redis connection URL for the fred client (pub/sub signaling + call tickets) |
jwt_secret |
RefreshTokenService, AppStateData |
Secret for signing/validating JWT refresh tokens |
snowflake_machine_id |
SnowflakeService::verify_and_init |
This node’s Snowflake machine id, validated against PostgreSQL |
host |
HTTP + gRPC bind addresses | Bind host for both servers (format!("{}:{}", cfg.host, cfg.port) / grpc_port) |
port |
axum::serve |
HTTP REST listen port |
grpc_port |
grpc::server::serve |
gRPC-Web realtime listen port |
local_upload_dir |
LocalStorage::new |
Filesystem root for uploaded media |
Design intent: configuration is resolved once at the top of main and threaded through constructors rather than read lazily inside services. This makes the startup graph explicit and testable, and it guarantees all services see one consistent view of the environment.
Failure Modes, Edge Cases & Concurrency
The bootstrap code reveals several deliberate failure-handling strategies:
- Missing
.envis not fatal.dotenv().ok()discards the error because production supplies real environment variables (main.rs). Conversely, a missing required config value surfaces later atConfig::from_env()/FredConfig::from_url(...)?and aborts startup with an error — fail fast, never run half-configured. - Infrastructure failures abort startup.
db::create_pool(...).await?,redis_client.init().await?, andSnowflakeService::verify_and_init(...).await?all use?, so the process refuses to start if PostgreSQL or Redis is unreachable. This is intentional: serving traffic without the database or signal bus would corrupt state. - Advisory-lock serialization for Snowflake.
verify_and_initvalidates the machine id against PostgreSQL with a dedicated advisory lock; the source comment explicitly warns against calling it twice, because a second call would attempt to re-acquire the lock. The singleArc<SnowflakeService>is shared by all consumers (main.rs). - Interior mutability via
Mutex.FileDetectServiceisArc<Mutex<…>>— the only service needing mutable shared state (signature tables). The mutex is process-local; there is no contention across processes because file detection is per-node. - Best-effort background maintenance. Identity backfill (
let _ = backfill_missing_keys(...)) and the 600-second status purge (let _ = purge_expired_statuses(...)) ignore errors and run in detachedtokio::spawntasks, so a failure in maintenance never takes down the servers (main.rs). - Best-effort SFU signal relay. The mpsc forwarding loop drops
send_signalerrors (let _ =), matching the at-most-once semantics of Redis pub/sub call signaling; a lost signal is recovered by ICE/negotiation retries in the realtime layer (main.rs). - Concurrency model. Everything is
Arc-shared; no locks are held across.awaitpoints in the bootstrap path. The unbounded mpsc channel between SFU and relay avoids backpressure stalls on the latency-critical signaling path.
Performance & Operational Considerations
- Single-binary, two-server process. One process hosts REST and gRPC-Web; horizontal scale-out is per-node with
snowflake_machine_iddisambiguating ID generation and Redis pub/sub distributing signals across nodes. - Redis as the fan-out bus.
CallRelayServicepushes call signals into Redis (per the master spec’sPSUBSCRIBE call_signals:*pattern), and per-user gRPC streams consume them — this keeps WebRTC signaling off the HTTP path. - Scheduled cleanup. The status purge loop runs every 600 seconds (
tokio::time::interval), bounding the growth of expired status rows (main.rs). - Startup cost.
run_migrations_safelyruns on every boot; combined with pool/Redis warm-up, this makes startup deterministic but not instant — deployments should allow a few seconds of readiness before routing traffic. - Observability.
tracing_subscriber::fmt::init()enables structured logs from the first line of startup, andAppStateData.start_time(Instant::now()) supports uptime reporting.
Extension Points
- New feature modules live under
src/modules/(e.g.,auth/controller.rs,status/service.rs) and are wired by adding their router intorouter::build_router(state)— the bootstrap only needs to construct any new service and add it toAppStateData. - New gRPC services are added under
src/grpc/and registered ingrpc::server::serve;main.rsrequires no change beyond the existing spawn sinceservereceives the fullArc<AppStateData>. - Storage backends implement the same interface as
services::storage::local::LocalStorage; swapping local disk for object storage is a construction-time change (LocalStorage::new(&cfg.local_upload_dir)in main.rs). - Startup hooks follow the established pattern: build the service, pack it into
AppStateData, then spawn any background loop withtokio::spawnafter state exists. - Single-responsibility modules are the primary structural extension constraint: new functionality should be added as new single-responsibility files, not by growing
main.rs.
Related Links
- Bootstrap entry point — the complete startup sequence documented on this page
- gRPC server entry — the spawned realtime server (see the gRPC Services page)
- Configuration module —
Config::from_env()source (see the Configuration page) - Database module —
create_pool/run_migrations_safely(see the Data Models & Persistence page) - HTTP router —
router::build_router(state)(see the REST API page) - Middleware and rate limiting (see the Middleware & Rate Limiting page)