Backend Overview¶
The backend is a Go monorepo at ftl-backend/. It compiles three separate binaries from cmd/. All shared logic lives under internal/.
Module layout¶
ftl-backend/
├── cmd/
│ ├── api-server/main.go — HTTP REST server
│ ├── ws-server/main.go — WebSocket server
│ └── flusher/main.go — Redis→Postgres drain worker
├── internal/ — all packages (never import from cmd/)
├── internal/redis/lua/ — Lua scripts loaded by the Redis client
└── migrations/ — sql-migrate up/down files
internal/ packages¶
| Package | Purpose |
|---|---|
achievements |
Badge definitions, earned/locked state per user |
activity |
Social-proof feed (Redis-backed recent trade/goal events) |
adminsettings |
Admin-writable runtime config (scoring weights, cooldown secs); persisted to admin_settings table |
amm |
AMM pricing helpers (arithmetic series cost, fill price) |
auth |
JWT issue/validate (RS256), Google JWKS verifier, JTI blocklist, reg ticket HMAC signer, cookie helpers |
bot |
Synthetic bot engine: per-persona trading against live instruments for liquidity |
config |
Env-var loading via config.Load(); returns typed *Config struct |
db |
pgxpool wrapper with retry logic; single entry point db.NewPool() |
email |
Azure Communication Services sender + NoopSender fallback; transactional email templates |
featureflag |
Boolean feature flags persisted to feature_flags table; coming_soon gate |
flusher |
Periodic Redis→Postgres sync (dirty-set drain), trade outbox retrier, position outbox retrier |
handler |
All HTTP handlers + Deps struct; handler.New(Deps).Register() wires routes |
instrument |
Instrument CRUD + Redis hydration; AMM price computation via net_shares_sold |
leaderboard |
PnL, ROI, district, friends leaderboard reads and writes; Redis sorted sets |
lease |
Single-instance Redis lease primitive (used by backstop sweeper and StopWatch) |
margin |
ADR-0006 washout evaluator: replays price ticks for offline users, closes positions at breach price |
match |
Fixture CRUD, squad queries, Redis event-list reads |
middleware |
Fiber middlewares: JWTAuth, JWTAuthOptional, AdminOnly, RateLimit, SecurityHeaders, ComingSoonGate |
notify |
In-app notification create/list/read; plugged into positions and margin via notify hook |
otp |
6-digit OTP issue, verify (3 attempts), cooldown (60s), verified-flag storage in Redis |
portfolio |
Wallet balance + unrealized PnL summary; reads from Redis hash then Postgres fallback |
positions |
CFD position open/close/modify/list; StopWatch SL/TP enforcement goroutine |
redis |
Redis client constructor (ftlredis.NewClient) and Lua script loader |
referral |
Per-invite links, referral code validation, first-trade payout (PayoutOnFirstTrade) |
replay |
Admin gameplay-bot: loads bundled fixtures from internal/replay/fixtures/, replays as live ticks |
spinner |
Weekly tiebreaker spin; SpinFor uses ON CONFLICT DO NOTHING for idempotency |
sportmonks |
Sportmonks API client, live scheduler (2s poll), squad syncer, post-match processor |
trade |
Legacy BUY/SELL trades via trade_execute.lua; sell cooldown; system sell at FT |
user |
User registration, login, district validation, admin promotion |
wallet |
Wallet read/write; Redis hash as hot cache, Postgres as durable store |
ws |
WebSocket hub (connection map, subscribe/unsubscribe by instrument idx), Redis pub/sub subscriber |
xp |
XP/level award and read |
Dependency injection pattern¶
cmd/api-server/main.go constructs every service directly, in dependency order:
xpSvc := xp.NewService(pool)
walletSvc := wallet.NewService(pool, redisClient.RDB())
// ... build every service ...
h := handler.New(handler.Deps{
Pool: pool,
UserSvc: userSvc,
WalletSvc: walletSvc,
// ...
})
h.Register(api, !cfg.IsProduction())
The handler.Deps struct in internal/handler/routes.go is the single place where all injectable services are named. There is no global state or service locator.
How to add a new service¶
- Create
internal/<domain>/service.gowithtype Service structandfunc NewService(...) *Service. - Add a field to
handler.Depsininternal/handler/routes.go. - Add the matching field to
handler.Handlersand copy it inhandler.New(). - Wire the constructed service into the
handler.Deps{}literal incmd/api-server/main.go. - Register any routes in
handler.Handlers.Register()or in your ownRegister(r fiber.Router)method called from there.
Note
Construction order matters. Services that depend on others (e.g. referral needs wallet) must be constructed after their dependencies. The order in main.go is the canonical reference.