Architecture

Backend structure

For a guided tour of the Rust code (reading order, Clone/Arc/async patterns, trace-throughs), see Backend Rust guide.


Handlers and routes

  • src/http/routes.rs — All route groups (health, auth, users, posts, …). Each function returns a Router<AppState>.
  • src/http/handlers.rs — All endpoint handlers in one file (project convention).
  • src/http/mod.rs — Composes the full router and middleware stack.

Path parameters use Axum conventions (:id in the route string, Path(Uuid) in handlers).

Versioning convention:

  • /health and /metrics live at root.
  • Product APIs are nested under /v1/....

src/app/ services (module map)

Each file under src/app/ is a service module (business logic + SQL/cache). Declared in src/app/mod.rs:

ModuleRole
authLogin, registration, token issue/refresh, access-token validation used by HTTP extractors
usersProfiles, account settings, user lookup
postsPhoto posts, captions, visibility
feedHome and related feeds, Redis-backed caching, cursor pagination
engagementLikes, comments, reactions
socialFollow graph, relationships
mediaUpload flow, presigned URLs, enqueue processing
storiesEphemeral stories
searchSearch queries over public data
notificationsCreate and list notifications; jobs::notifications invokes this service from the mpsc worker
invitesInvite codes and redemption
moderationReports and moderation actions
trustTrust / safety-related user state
fingerprintClient fingerprinting helpers for abuse signals
rate_limiterRate-limit bookkeeping backed by Redis/DB as implemented

src/jobs/

Long-running or asynchronous work outside the request hot path:

ModuleRole
media_processorSQS poll loop (or combined spawn), derivatives, idempotent DB updates
notificationsConsumes NotificationJob from mpsc, writes notification-related rows
cleanupPeriodic tasks (e.g. expired stories)

APP_MODE=serverless-worker in main.rs exposes a small HTTP router from jobs::media_processor for one-shot processing in serverless environments—separate from the main http::router stack.


src/infra/

Adapters for external systems; constructed at startup and held on AppState:

ModuleTypeTypical use
dbDb (SQLx pool)All persistent state
cacheRedisCacheFeed and rate-limit acceleration
storageObjectStorageS3-compatible uploads and processed keys
queueQueueClientEnqueue media jobs for workers

Services take Db / cache handles via ::new rather than calling infra modules directly from handlers.


src/http/validation.rs

Shared input validation helpers: max lengths, trimmed required strings, handle format rules. Handlers return AppError::bad_request(...) when validation fails, keeping rules in one place instead of duplicating checks across handlers.rs.


Services

Services are stateless Clone structs holding a Db handle (and optionally RedisCache). Example pattern:

let svc = PostService::new(state.db.clone(), state.redis.clone());

They return anyhow::Result<T> internally; handlers map failures to AppError and HTTP status codes.

Why this pattern

  • No request-local mutable service state keeps handlers easy to reason about and safe to clone.
  • Explicit constructor dependencies make service wiring visible (Db, cache, storage) instead of hidden globals.
  • Failure mapping at the edge preserves rich internal errors while providing stable API semantics.

Auth

  • PASETO access tokens (short-lived) and refresh tokens (longer-lived).
  • Argon2 for password hashing.
  • AuthUser extractor — validates Authorization: Bearer; 401 if missing or invalid.
  • AdminToken — separate secret for moderation/admin-style endpoints.

Token keys are loaded from environment as 32-byte base64-decoded values; startup fails fast when malformed.


Errors

AppError in http/error.rs centralizes HTTP-facing errors. Handlers often use:

  • Database constraint codes (23505 unique, 23503 foreign key) for 409/404 semantics.
  • anyhow message checks for domain-level failures exposed as 400/403/404.

This keeps SQL/infra details out of response payloads while still making errors actionable in logs.


Query and data patterns

SQL conventions

AreaConvention
SQL APIsqlx::query() with .bind(), not query!
Row accessrow.get("column_name") via sqlx::Row
UUID PKsGenerated in Postgres (uuid_generate_v4()), not in Rust
EnumsBound as strings via as_db() / parsed with from_db()
PaginationCursor (timestamp, uuid) with limit + 1

Cache conventions

  • Redis is used for acceleration, never as source of truth.
  • Keys follow namespace:entity:id (for paged feeds, cursor values are part of the key).
  • Cache failures are logged and tolerated; DB remains the fallback path.

Background processing conventions

  • Queue consumers are idempotent against DB status transitions.
  • Message deletion happens only after successful or permanently failed processing.
  • Transient failures are retried using queue semantics, not custom retry tables.

AppState boundary

AppState is defined in src/lib.rs and constructed in src/main.rs—the composition root for runtime dependencies:

  • Db, RedisCache, ObjectStorage, QueueClient
  • auth/token settings and TTLs
  • upload constraints (upload_max_bytes, upload URL TTL)
  • trusted proxy CIDRs and notification channel capacity

Centralizing this boundary keeps handler and middleware construction uniform across API and worker-adjacent paths.

Previous
System architecture