System Overview
This document describes a full-stack browser-based multiplayer football management game backend. It uses a relational database and clean architecture, exposes REST APIs for a SPA / game client, and covers: user auth, teams, players, formations, tactics, energy, training, match simulation, leagues/cups, transfer market, sponsors, buildings, guilds, messaging, and admin tooling.
Target Stack
- Backend: Node.js + TypeScript + Express (or Fastify)
- DB: PostgreSQL (recommended) or MySQL
- ORM: Prisma or TypeORM
- Auth: JWT-based, HTTP-only cookies or Authorization header
- Infra: REST over HTTPS, horizontal-scaling friendly
Clean Architecture Layers
- Domain: Entities, value objects, domain services, match engine.
- Application: Use-cases (interactors), DTOs, ports.
- Infrastructure: DB repositories, mail, schedulers, queues.
- Interface: HTTP controllers, REST routes, validation.
+---------------------------+
| Presentation | <- Express Routes, Controllers, DTO mappers
+---------------------------+
| Application | <- Use Cases (CreateTeam, StartMatch, BidPlayer)
+---------------------------+
| Domain | <- Entities (User, Team, Player, Match, League)
+---------------------------+
| Infrastructure | <- Repos (SQL), external services, schedulers
+---------------------------+
Bounded Contexts
Identity & Access
Users, sessions, permissions, bans, admin roles.
Club Management
Teams, players, training, buildings, sponsors, finances.
Competition
Leagues, cups, fixtures, results, match engine, live state.
Market
Transfers, contracts, auctions, wage budgets, fees.
Social
Guilds (alliances), private messages, chat channels.
Admin
Moderation, balancing, season rollovers, statistics.
Recommended Technical Choices
- Use PostgreSQL for JSON support and constraints (FKs, cascades, check constraints).
- Use Prisma or TypeORM with migrations to evolve schema over seasons.
- Decouple match simulation into a stateless service which can be scaled or run in workers.
- Use cron / job queue (e.g., BullMQ) to process daily/weekly events: training ticks, energy regen, league fixtures.
- All heavy writes (matches, league standings) are transactional to avoid double-results or ghost matches.
Users & Authentication
- User: id, email, hashed_password, username, locale, created_at, is_banned.
- Session/JWT: short-lived tokens; refresh tokens persisted in DB.
- Roles: USER, MOD, ADMIN for admin panel & moderation endpoints.
Teams, Players, Formations & Tactics
Team
- Owner user_id (1:1 primary team by default).
- Name, short_name, country, logo_url, stadium_name.
- Economy: balance, wage_budget, stadium_capacity.
- Relations: players, formations, tactics profiles, sponsors, buildings.
Formations
Predefined templates: 4-4-2, 4-3-3, 3-5-2, each instance with actual player assignments.
Player
- Core: name, age, nationality, preferred_positions (GK, CB, FB, DM, CM, AM, W, ST).
- Stats (0–100): pace, shooting, passing, dribbling, defending, physical, goalkeeping.
- Mental: vision, work_rate, discipline, creativity.
- State: energy (0–100), morale (0–100), fitness/injury, contract (wage, end_date, release_clause).
- History: previous_teams, transfer_fees, total_goals, total_assists, yellow/red cards.
Training & Energy System
- Players have energy that decreases in matches and recovers over time and via facilities.
- Training plans: per-player or per-squad; attributes receive experience; over threshold => stat increase.
- Training difficulty vs injury risk & energy use.
- Daily cron: applies training results, energy regen; writes progression logs.
Formations & Tactics Model
- Formation: base schema (4-4-2, 4-3-3) + specialized positions (e.g., LW/RW/DM).
- Tactics:
- Style: possession, counter-attack, long-ball, high-press.
- Defensive line: deep, normal, high.
- Width: narrow, normal, wide.
- Tempo: slow, normal, fast.
- Pressing & aggression: integer scale (0–100).
Matches, Leagues & Cups
Match Entity
- home_team_id, away_team_id, competition_id, round, kickoff_at.
- status: SCHEDULED, LIVE, FINISHED, CANCELED.
- tactics & formation snapshots: store lineup+tactics at kickoff to preserve history.
- Result: goals_home, goals_away, events JSON (goals, cards, injuries, substitutions, xG, etc).
League & Cup
- League: seasons, divisions, standings table, auto-promotion/relegation rules.
- Cup: knockout rounds (R64, R32, R16, QF, SF, F), with tie-breakers & replay/penalties.
- Scheduler service to generate fixtures at season start and assign match kickoff times.
Match Simulation Engine (Domain Service)
Simulated as discrete time steps (e.g., 1 simulated minute). The engine uses players' stats, energy, tactics, morale, and randomness to decide events.
- Pre-match: calculate team strengths in defence, midfield, attack; home advantage & morale factor.
- Time loop: from 0 to 90 (+ stoppage). Each minute:
- Determine possession probability based on midfield control and tactics.
- If possession, chance to create: cross, through-ball, long-shot, set-piece.
- Success probability uses relevant stats (passing, shooting, defending, GK) & energy multipliers.
- On goal: update score; log event; adjust morale & tactics (optional dynamic AI).
- Yellow/red cards, injuries triggered by aggression & randomness.
- Post-match: update player stats (goals, assists), fitness/energy drop, XP gain, competition tables, finances (tickets, sponsors win bonus).
Economy, Transfer Market, Sponsors & Buildings
Transfer Market
- Auction-style or fixed-price listings.
- Constraints: window periods, squad size min/max, wage budget checks.
- Bid service: ACID transactions to avoid race-condition double sales.
Sponsors
- Contracts give periodic income; may have objectives (league finish, wins, promotion).
- On objective completion/failure, pay bonuses or penalties.
Buildings
- Training facilities, youth academy, medical center, stadium, fan shop, HQ.
- Each level modifies regen speed, training XP, injury recovery, ticket income, etc.
Admin Panel Domain
- CRUD on users, teams, matches, competitions, buildings, sponsors.
- Game balancing: change global configuration (regen rates, tax, wage caps) with effective-from date.
- Season tools: trigger season rollover, promotions/relegations, cup draws.
- Monitoring: high-level dashboards of concurrent matches, DB health, error logs.
Relational Schema (ER Overview)
[users] 1---* [teams] 1---* [players]
| |
| *---* [guilds]
|
*---* [guilds]
[teams] 1---* [formations] 1---* [formation_slots] *---1 [players]
[teams] 1---* [tactic_profiles]
[competitions] 1---* [seasons] 1---* [matches]
[teams] *---* [sponsors] via [team_sponsors]
[teams] 1---* [buildings]
[players] 1---* [contracts]
[players] 1---* [transfer_listings] 1---* [transfer_bids]
[users] 1---* [messages] (sender)
[users] 1---* [messages] (recipient or guild_id)
[users] *---* [roles] via [user_roles]
[admins] - logical subset of users with ADMIN role
Core SQL DDL (Simplified)
This is an illustrative PostgreSQL schema skeleton. Omit or extend fields as needed.
-- USERS & AUTH
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email CITEXT UNIQUE NOT NULL,
username CITEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
locale VARCHAR(8) DEFAULT 'en',
is_banned BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE roles (
id SERIAL PRIMARY KEY,
name VARCHAR(32) UNIQUE NOT NULL -- USER, MOD, ADMIN
);
CREATE TABLE user_roles (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
refresh_token TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL
);
-- TEAMS & PLAYERS
CREATE TABLE teams (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id),
name VARCHAR(64) NOT NULL,
short_name VARCHAR(8),
country VARCHAR(2),
logo_url TEXT,
stadium_name VARCHAR(64),
balance BIGINT NOT NULL DEFAULT 0,
wage_budget BIGINT NOT NULL DEFAULT 0,
stadium_capacity INTEGER NOT NULL DEFAULT 1000,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TYPE position AS ENUM (
'GK','CB','FB','DM','CM','AM','W','ST'
);
CREATE TABLE players (
id SERIAL PRIMARY KEY,
team_id INTEGER REFERENCES teams(id) ON DELETE SET NULL,
name VARCHAR(64) NOT NULL,
age INTEGER NOT NULL,
nationality VARCHAR(2),
preferred_pos position NOT NULL,
pace SMALLINT NOT NULL CHECK (pace BETWEEN 0 AND 100),
shooting SMALLINT NOT NULL CHECK (shooting BETWEEN 0 AND 100),
passing SMALLINT NOT NULL CHECK (passing BETWEEN 0 AND 100),
dribbling SMALLINT NOT NULL CHECK (dribbling BETWEEN 0 AND 100),
defending SMALLINT NOT NULL CHECK (defending BETWEEN 0 AND 100),
physical SMALLINT NOT NULL CHECK (physical BETWEEN 0 AND 100),
goalkeeping SMALLINT NOT NULL CHECK (goalkeeping BETWEEN 0 AND 100),
vision SMALLINT NOT NULL CHECK (vision BETWEEN 0 AND 100),
work_rate SMALLINT NOT NULL CHECK (work_rate BETWEEN 0 AND 100),
discipline SMALLINT NOT NULL CHECK (discipline BETWEEN 0 AND 100),
energy SMALLINT NOT NULL DEFAULT 100 CHECK (energy BETWEEN 0 AND 100),
morale SMALLINT NOT NULL DEFAULT 50 CHECK (morale BETWEEN 0 AND 100),
value BIGINT NOT NULL DEFAULT 0,
wage BIGINT NOT NULL DEFAULT 0,
contract_until DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- FORMATIONS & TACTICS
CREATE TABLE formations (
id SERIAL PRIMARY KEY,
team_id INTEGER NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
name VARCHAR(64) NOT NULL, -- e.g. "Default 4-4-2"
base_system VARCHAR(16) NOT NULL CHECK (base_system IN ('4-4-2','4-3-3','3-5-2')),
is_default BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE formation_slots (
id SERIAL PRIMARY KEY,
formation_id INTEGER NOT NULL REFERENCES formations(id) ON DELETE CASCADE,
slot_index SMALLINT NOT NULL, -- 1..11
position position NOT NULL,
player_id INTEGER REFERENCES players(id) ON DELETE SET NULL
);
CREATE TABLE tactic_profiles (
id SERIAL PRIMARY KEY,
team_id INTEGER NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
name VARCHAR(64) NOT NULL,
style VARCHAR(32) NOT NULL, -- possession, counter, etc.
defensive_line SMALLINT NOT NULL CHECK (defensive_line BETWEEN 0 AND 100),
width SMALLINT NOT NULL CHECK (width BETWEEN 0 AND 100),
tempo SMALLINT NOT NULL CHECK (tempo BETWEEN 0 AND 100),
pressing SMALLINT NOT NULL CHECK (pressing BETWEEN 0 AND 100),
aggression SMALLINT NOT NULL CHECK (aggression BETWEEN 0 AND 100),
is_default BOOLEAN NOT NULL DEFAULT FALSE
);
-- COMPETITIONS
CREATE TABLE competitions (
id SERIAL PRIMARY KEY,
name VARCHAR(64) NOT NULL,
type VARCHAR(16) NOT NULL CHECK (type IN ('LEAGUE','CUP')),
country VARCHAR(2),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE seasons (
id SERIAL PRIMARY KEY,
competition_id INTEGER NOT NULL REFERENCES competitions(id) ON DELETE CASCADE,
season_number INTEGER NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL
);
CREATE TABLE matches (
id SERIAL PRIMARY KEY,
season_id INTEGER NOT NULL REFERENCES seasons(id) ON DELETE CASCADE,
competition_stage VARCHAR(32), -- league round, cup round
home_team_id INTEGER NOT NULL REFERENCES teams(id),
away_team_id INTEGER NOT NULL REFERENCES teams(id),
kickoff_at TIMESTAMPTZ NOT NULL,
status VARCHAR(16) NOT NULL CHECK (status IN ('SCHEDULED','LIVE','FINISHED','CANCELED')),
goals_home SMALLINT,
goals_away SMALLINT,
events JSONB, -- serialized timeline
home_formation_id INTEGER REFERENCES formations(id),
away_formation_id INTEGER REFERENCES formations(id),
home_tactic_id INTEGER REFERENCES tactic_profiles(id),
away_tactic_id INTEGER REFERENCES tactic_profiles(id)
);
-- MARKET
CREATE TABLE transfer_listings (
id SERIAL PRIMARY KEY,
player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE,
seller_team_id INTEGER NOT NULL REFERENCES teams(id),
type VARCHAR(16) NOT NULL CHECK (type IN ('AUCTION','FIXED')),
min_price BIGINT NOT NULL,
buy_now_price BIGINT,
ends_at TIMESTAMPTZ NOT NULL,
status VARCHAR(16) NOT NULL CHECK (status IN ('OPEN','SOLD','EXPIRED','CANCELED'))
);
CREATE TABLE transfer_bids (
id SERIAL PRIMARY KEY,
listing_id INTEGER NOT NULL REFERENCES transfer_listings(id) ON DELETE CASCADE,
bidder_team_id INTEGER NOT NULL REFERENCES teams(id),
amount BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- SPONSORS & BUILDINGS
CREATE TABLE sponsors (
id SERIAL PRIMARY KEY,
name VARCHAR(64) NOT NULL,
base_fee BIGINT NOT NULL,
win_bonus BIGINT NOT NULL DEFAULT 0,
objective VARCHAR(128)
);
CREATE TABLE team_sponsors (
id SERIAL PRIMARY KEY,
team_id INTEGER NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
sponsor_id INTEGER NOT NULL REFERENCES sponsors(id),
start_date DATE NOT NULL,
end_date DATE NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE buildings (
id SERIAL PRIMARY KEY,
team_id INTEGER NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
type VARCHAR(32) NOT NULL, -- stadium, training, academy, etc.
level SMALLINT NOT NULL DEFAULT 1,
upgrading BOOLEAN NOT NULL DEFAULT FALSE,
upgrade_ends_at TIMESTAMPTZ
);
-- GUILDS & MESSAGING
CREATE TABLE guilds (
id SERIAL PRIMARY KEY,
name VARCHAR(64) UNIQUE NOT NULL,
tag VARCHAR(8) UNIQUE NOT NULL,
created_by INTEGER NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE guild_members (
guild_id INTEGER NOT NULL REFERENCES guilds(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(16) NOT NULL CHECK (role IN ('LEADER','OFFICER','MEMBER')),
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (guild_id, user_id)
);
CREATE TABLE messages (
id SERIAL PRIMARY KEY,
sender_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
recipient_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
guild_id INTEGER REFERENCES guilds(id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted BOOLEAN NOT NULL DEFAULT FALSE,
CHECK (
(recipient_id IS NOT NULL AND guild_id IS NULL)
OR (recipient_id IS NULL AND guild_id IS NOT NULL)
)
);
REST API – Auth & Users
POST /api/auth/register
Create user, hash password, create main team or waitlist.
Request:
POST /api/auth/register
{
"email": "user@example.com",
"username": "FootyMaster",
"password": "S3cret!"
}
Response 201:
{
"id": 1,
"email": "user@example.com",
"username": "FootyMaster",
"createdAt": "2026-01-01T00:00:00Z"
}
POST /api/auth/login
Request:
POST /api/auth/login
{
"email": "user@example.com",
"password": "S3cret!"
}
Response 200:
{
"accessToken": "jwt...",
"refreshToken": "jwt...",
"user": {
"id": 1,
"username": "FootyMaster"
}
}
POST /api/auth/refresh
GET /api/users/me
Return profile, owned teams, guilds.
REST API – Teams, Players, Training
POST /api/teams
Create a team for current user (1 max or limited).
GET /api/teams/:id
Public team profile with players, facilities, sponsors (no sensitive info).
GET /api/teams/:id/squad
PUT /api/teams/:id/formation
Request:
PUT /api/teams/1/formation
{
"formationId": 23,
"slots": [
{ "slotIndex": 1, "playerId": 101 },
...
]
}
PUT /api/teams/:id/tactics
POST /api/teams/:id/training-plan
Define per-player or global plan; domain handles daily progression.
REST API – Matches & Simulation
GET /api/matches/:id
Return match details, lineups, live or final events.
POST /api/matches/:id/simulate
Admin or background-only endpoint: triggers simulation in match-engine service. In production, prefer async job queue.
GET /api/teams/:id/matches
REST API – Leagues & Cups
GET /api/competitions
GET /api/leagues/:id/standings
GET /api/cups/:id/bracket
REST API – Transfer Market
GET /api/transfers
Query open listings with filters (position, age, price range).
POST /api/transfers/list
List a player for transfer.
POST /api/transfers/:listingId/bid
Place a bid; domain enforces funds & window rules.
POST /api/transfers/:listingId/buy-now
REST API – Guilds & Messaging
POST /api/guilds
POST /api/guilds/:id/join
GET /api/guilds/:id/messages
POST /api/messages
Send direct or guild message (mutually exclusive).
REST API – Admin Panel
All admin endpoints require ADMIN role; use a dedicated route group with middleware.
GET /api/admin/users
PATCH /api/admin/users/:id/ban
POST /api/admin/seasons/rollover
POST /api/admin/matches/:id/force-simulate
Match Simulation Engine – Algorithm Sketch
- Input: match entity, lineups, tactics, player stats & energy at kickoff.
- Preprocessing:
- Compute aggregated ratings: defence, midfield, attack for each team.
- Apply energy & morale multipliers.
- Calculate possession baseline:
midfieldShare = midRatingsA / (midRatingsA + midRatingsB)
- Loop minutes 1..90:
- For each minute, roll which team has possession using midfieldShare.
- Roll event type (build-up, long-ball, set-piece) depending on tactics.
- Compute chanceQuality: difference between attack and defence + randomness.
- If chanceQuality > threshold: create shot; probability of goal uses shooter stats vs GK & defence.
- Log events in a timeline array; update live score & momentum.
- Occasionally roll fouls & cards; suspensions handled post-match.
- Post-match:
- Persist final score & timeline.
- Update players' fitness, energy, morale, XP, stats (goals/assists/cards).
- Update league/cup tables.
- Grant finances (tickets, sponsors, bonuses).
Example Pseudo-Code (Domain Service)
function simulateMatch(matchId: MatchId, repo: MatchSimulationRepo): void {
const ctx = repo.loadMatchContext(matchId);
const state = initState(ctx);
for (let minute = 1; minute <= 90; minute++) {
stepMinute(state, minute);
}
applyPostMatch(state, repo);
repo.saveResult(state);
}
API Playground (Mock)
This is a lightweight in-browser mock that demonstrates how the REST API could be called from a frontend. It does not contact a real server here; instead, it simulates responses according to the spec.
Choose an endpoint and click "Simulate Request".
Example Express Router (Server-Side Sketch)
This shows how to map HTTP routes to application use-cases in a clean architecture style.
// Express-style pseudo-code (server-side)
router.post('/api/auth/login', async (req, res) => {
const dto = loginRequestSchema.parse(req.body);
const result = await loginUseCase.execute(dto);
if (result.isErr()) {
return res.status(401).json({ error: result.error.message });
}
const { accessToken, refreshToken, user } = result.value;
return res.json({ accessToken, refreshToken, user });
});
router.post('/api/teams', authMiddleware('USER'), async (req, res) => {
const dto = createTeamSchema.parse(req.body);
const userId = req.auth.userId;
const team = await createTeamUseCase.execute({ ...dto, ownerId: userId });
return res.status(201).json(team);
});
router.post('/api/matches/:id/simulate', adminOnly, async (req, res) => {
const matchId = Number(req.params.id);
await simulateMatchUseCase.execute({ matchId });
return res.status(202).json({ status: 'QUEUED' });
});
Guilds & Messaging