FutsalLeague Manager is composed of five distinct layers: an Angular single-page application served by the browser, a Node.js/Express REST API that handles all business logic, a PostgreSQL database for persistent storage, a Redis cache for high-frequency read paths, and Cloudinary for binary asset storage. Each layer communicates over well-defined contracts — HTTP/JSON between the SPA and the API, theDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/Danielsl4/TFG_DAM_2526/llms.txt
Use this file to discover all available pages before exploring further.
pg connection pool between the API and PostgreSQL, ioredis between the API and Redis, and the Cloudinary v2 SDK for image uploads.
Layers at a glance
| Layer | Technology | Default port | Role |
|---|---|---|---|
| Frontend | Angular 20 SPA, Bootstrap 5, Bootstrap Icons | 4200 | User interface — fixtures, standings, admin panel, referee tools |
| Backend API | Node.js, Express 4 | 3000 | REST API, auth, business logic, cron jobs |
| Database | PostgreSQL | 5432 | Persistent storage for all league data (15 tables) |
| Cache | Redis (ioredis) | 6379 | Match data, standings, rate limiting |
| Storage | Cloudinary v2, Sharp | — | Team and player image upload and optimization |
| Auth | JWT (jsonwebtoken), bcrypt | — | Token issuance, password hashing, role-based middleware |
Database schema
The database is initialized byinit_db.js, which creates 5 PostgreSQL enums and 15 tables in dependency order.
Enums
| Enum | Values |
|---|---|
match_phase | fase_de_grupos, octavos, cuartos, semis, final |
match_status | pendiente, en_curso, finalizado |
event_type | gol, tarjeta_amarilla, tarjeta_roja |
role | admin, referee, user |
vote_type | local, empate, visitante |
| Table | Description |
|---|---|
seasons | Top-level container for each competition year |
groups | Division of teams within a season |
teams | Club records with kit color, logo, delegate, and coach |
players | Player registry with birth date and photo |
team_players | Many-to-many: player-team membership per season with jersey number |
fields | Venue records used when scheduling matches |
matches | Full match record including phase, status, score, locking metadata |
match_events | Individual in-game events (goals, yellow/red cards) per match |
users | Accounts with role, verification status, and password reset tokens |
user_points | Prediction game score per user per season |
team_followers | Many-to-many: users following teams |
team_stats | Aggregated win/draw/loss/goal/card statistics per team per group per season |
player_stats | Aggregated goals, cards, and appearances per player per season |
match_votes | Pre-match outcome predictions (one vote per user per match) |
audit_logs | Timestamped record of every admin action with JSONB details |
Request flow
A typical authenticated browser request follows this path:- Angular SPA constructs an HTTP request with an
Authorization: Bearer <token>header and sends it to the Express API athttp://localhost:3000. verifyTokenmiddleware decodes and validates the JWT using theJWT_SECRET. If the token is invalid or absent the request is rejected with401.- Role middleware (
verifyAdminorverifyReferee) checks the role claim on the decoded token. Insufficient privileges return403. - Route handler queries PostgreSQL via the
pgconnection pool (max 50 connections, 5 s timeout). For read-heavy endpoints (standings, match lists) the handler checks Redis first and writes through on a cache miss. - Response is serialized as JSON and returned to Angular, which updates the view.
logo_url or photo_url column.
Key design patterns
In-flight request deduplication
The cache layer uses aninflightRequests map to coalesce concurrent identical requests for the same resource into a single upstream database query. When multiple requests arrive simultaneously for an uncached key, only the first triggers a database call; the rest await the same promise. This prevents the thundering herd problem during cache invalidation under load.
Soft deletes
Teams, players, matches, fields, and users carry anis_active boolean column (default TRUE). Deletion through the admin panel sets is_active = FALSE rather than removing the row, preserving referential integrity and historical match records.
Audit logging
Every write operation performed through the admin panel is recorded inaudit_logs. Each row captures the acting user (user_id), the action name (action), the affected entity type and ID, a JSONB details blob with before/after values, and a creation timestamp. This provides a full audit trail for compliance and debugging.
Transactional match finalization
When a referee finalizes a match, the operation runs inside a PostgreSQL transaction that atomically:- Updates
matches.statustofinalizado - Recalculates and upserts rows in
team_statsfor both home and away teams - Upserts
player_statsfor every player who scored or received a card - Awards prediction points in
user_pointsfor every user whosematch_votesrow matches the final result
Match locking
To prevent edit conflicts between referees, thematches table has locked_by (user FK) and locked_at (timestamp) columns. The verifyMatchLock middleware checks these fields before every write. A lock expires automatically after 2 minutes of inactivity — the middleware cleans up expired locks proactively on the next request.
Where to go next
Backend deployment
Configure the Express API for production — process management, environment variables, and health checks.
Frontend deployment
Build and deploy the Angular SPA — static hosting, environment files, and base href configuration.
Database deployment
Provision PostgreSQL, run migrations, configure connection pooling, and set up backups.