Football Manager Backend Spec & API Explorer
FootyVerse Backend
Clean architecture · REST API · Multiplayer

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

  1. Domain: Entities, value objects, domain services, match engine.
  2. Application: Use-cases (interactors), DTOs, ports.
  3. Infrastructure: DB repositories, mail, schedulers, queues.
  4. 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.

Guilds & Messaging

  • Guilds: multi-team alliances; share chat, bonuses, cup competitions.
  • Messaging: direct 1:1 messages & guild channels. Stored message history with pagination.
  • Moderation: report flags on messages; admin tools to mute, kick, or ban users/guilds if needed.

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

  1. Input: match entity, lineups, tactics, player stats & energy at kickoff.
  2. Preprocessing:
    • Compute aggregated ratings: defence, midfield, attack for each team.
    • Apply energy & morale multipliers.
    • Calculate possession baseline: midfieldShare = midRatingsA / (midRatingsA + midRatingsB)
  3. 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.
  4. 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' });
});