System Overview¶
Tomoda is a real-time social platform for organising spontaneous in-person events. The product surfaces a friends-and-events map, real-time chat, ephemeral "moments", and discovery of nearby people and venues. It is delivered as a single Expo codebase shipping to iOS, Android, and the web, backed by a Go monolith that fronts Postgres, Redis, object storage, and a handful of third-party services.
High-level component map¶
The backend is a single Go binary built from backend/cmd/server/main.go. On boot it loads layered YAML config (config.yaml plus environment-specific overrides), initialises a Zap logger, connects to Postgres (pool sized 10 idle / 100 open in backend/internal/database), applies the goose migrations in non-production environments via db.Migrate, wires the dependency graph via Wire (backend/internal/wiring/providers.go), seeds category data, then starts three concurrent runtimes inside the same process:
- the HTTP server (chi over net/http) on
:8080, - the WebSocket Hub goroutine (
hub.Run()), - the Asynq worker server (background tasks) and the scheduler manager (cron-style enqueuers).
Request lifecycle (authenticated REST)¶
A typical authenticated request — e.g. POST /api/v1/events — traverses a deterministic middleware chain before reaching a handler:
SetupRouter; only the JWTAuth (purple) layer is identity-gated. Handlers stay thin, DTOs in, JSON out, while services own business logic and the per-domain store is the only layer that touches the database (via sqlc-generated queries over pgx).The middleware order is fixed in backend/internal/wiring/router.go (SetupRouter) via r.Use(...): Recovery → Logger → TraceID → Observability (metrics + tracing) → CORS → SecurityHeaders → IPBlocker.BlockMiddleware → IPBlocker.SuspiciousActivityMiddleware, with JWTAuth (and access-policy gates where applicable) applied per-group on protected routes via r.With / r.Group. Each middleware is a func(http.Handler) http.Handler. Handlers are thin: they parse DTOs, call into a service, and return JSON. Services hold business logic; the per-domain store is the only layer that touches the database, running that domain's sqlc-generated queries over pgx.
Tracing
The TraceID middleware assigns a per-request UUID and threads it through Zap log fields, making it possible to follow a single request across handler, service, store, and worker boundaries.
Real-time lifecycle¶
WebSocket traffic is routed through a separate top-level group:
r.Route("/ws", func(r chi.Router) {
r.Use(middleware.JWTAuth(app.AuthService))
r.Get("/chats/{id}", app.Handlers.ChatHandler.HandleWebSocket)
})
JWTs are passed via the token query parameter (browser WebSocket APIs cannot set headers). The handler upgrades the connection, registers a Client on the in-process Hub, and the Hub manages per-chat rooms with broadcast fan-out. See Real-time for the full picture.
Background job lifecycle¶
Background work uses Asynq on top of Redis. Three queues are configured in backend/internal/async/server.go:
| Queue | Weight | Used for |
|---|---|---|
| critical | 6 | Time-sensitive notifications, webhook fan-out |
| default | 3 | Standard async work |
| low | 1 | Cleanup, purges, opportunistic jobs |
The scheduler (backend/internal/async/scheduler.go) is a separate, Redis-coordinated cron runner that enqueues recurring jobs:
| Job | Cadence | Purpose |
|---|---|---|
status_update |
5 min | Roll events between scheduled / active / completed |
redis_sync |
5 min | Reconcile Redis caches with Postgres |
token_cleanup |
1 hr | Purge expired refresh tokens / sessions |
account_suspension |
12 hr | Apply scheduled suspensions |
purge |
24 hr | General hard-delete sweep |
deactivated_acct_purge |
24 hr | Hard-delete accounts past their deactivation grace |
moment_cleanup |
5 min | Soft-delete expired non-journaled moments |
moment_purge |
24 hr | Hard-delete moments soft-deleted > 7 days |
message_expiry |
30 s | Hard-delete disappearing messages past expires_at |
Deployment topology¶
In production the same Go binary runs on GKE behind an ingress, with images built by Google Cloud Build (cloudbuild-backend.yaml, cloudbuild-frontend.yaml), pushed to Artifact Registry, and rolled out by ArgoCD from a separate devops/ repo. Postgres is Cloud SQL (PostGIS enabled), Redis is Memorystore, object storage is GCS via S3-compatible APIs (MinIO locally), and Photon runs as a sidecar service for self-hosted geocoding. The mobile client is shipped through EAS Build with three profiles — development, preview, and production — defined in frontend/eas.json.