# Blueprint Server — Contrabbandieri MMO Mobile ## 1. Obiettivo Realizzare un backend funzionante per un gioco mobile multiplayer asincrono, persistente e trasferibile tra server Linux tramite Docker. Il server deve gestire: - autenticazione utenti; - profilo giocatore; - denaro, reputazione e livello; - inventario; - mercato dinamico; - missioni generate dal server; - città e prezzi locali; - eventi globali; - classifiche; - notifiche realtime opzionali; - deploy tramite Docker Compose. Il client mobile non deve mai essere considerato affidabile. Tutte le regole di gioco vivono sul server. --- ## 2. Stack consigliato ### Backend - Node.js 22 LTS - TypeScript - Fastify - Prisma ORM - PostgreSQL - Redis - Socket.IO - Zod - JWT - Docker Compose ### Perché questo stack Fastify è veloce, semplice e adatto a API REST. Prisma semplifica schema, migrazioni e accesso al database. PostgreSQL è affidabile per dati persistenti. Redis serve per cache, classifiche, cooldown e notifiche realtime. Socket.IO consente eventi live verso client mobile. --- ## 3. Architettura generale ```text Mobile Client | | HTTPS REST / WebSocket v Nginx Reverse Proxy | v Game API Node.js | | | v | Redis v PostgreSQL ``` --- ## 4. Struttura repository ```text contrabbandieri-server/ ├── docker-compose.yml ├── docker-compose.prod.yml ├── .env.example ├── README.md ├── data/ │ ├── postgres/ │ ├── redis/ │ └── backups/ ├── package.json ├── tsconfig.json ├── prisma/ │ ├── schema.prisma │ └── seed.ts ├── src/ │ ├── app.ts │ ├── server.ts │ ├── config/ │ │ └── env.ts │ ├── db/ │ │ ├── prisma.ts │ │ └── redis.ts │ ├── modules/ │ │ ├── auth/ │ │ ├── players/ │ │ ├── market/ │ │ ├── inventory/ │ │ ├── missions/ │ │ ├── cities/ │ │ ├── events/ │ │ └── leaderboard/ │ ├── realtime/ │ │ └── socket.ts │ ├── jobs/ │ │ ├── scheduler.ts │ │ ├── updateMarketPrices.job.ts │ │ ├── generateMissions.job.ts │ │ └── generateWorldEvent.job.ts │ ├── shared/ │ │ ├── authGuard.ts │ │ ├── errors.ts │ │ ├── result.ts │ │ └── validators.ts │ └── types/ └── nginx/ └── nginx.conf ``` --- ## 5. Modello di dominio MVP ### Player Rappresenta il giocatore. Campi principali: - id - userId - displayName - money - reputation - level - experience - currentCityId - createdAt - updatedAt ### City Ogni città ha mercato e missioni locali. Esempi iniziali: - Milano - Napoli - Palermo Campi: - id - name - riskLevel - policePressure - economyModifier ### Item Merce commerciabile. Esempi: - Gioielli - Componenti elettronici - Auto rubate - Informazioni riservate - Armi leggere Campi: - id - name - basePrice - rarity - illegalLevel - weight ### InventoryItem Quantità posseduta da un player. Campi: - playerId - itemId - quantity ### MarketPrice Prezzo locale per item e città. Campi: - cityId - itemId - buyPrice - sellPrice - demand - supply - updatedAt ### Mission Missione generata dal server. Campi: - id - cityId - title - type - difficulty - durationSeconds - rewardMoney - rewardXp - risk - requiredItemId - requiredItemQuantity - expiresAt ### PlayerMission Missione iniziata da un player. Campi: - id - playerId - missionId - status - startedAt - completesAt - resolvedAt ### WorldEvent Evento globale o locale che altera economia/rischio. Campi: - id - title - description - type - cityId nullable - priceModifier - policeModifier - startsAt - endsAt --- ## 6. Schema Prisma MVP ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } enum MissionStatus { STARTED COMPLETED FAILED } enum MissionType { DELIVERY THEFT SMUGGLING INTEL } enum EventType { MARKET_BOOM POLICE_RAID SHORTAGE BLACKOUT } model User { id String @id @default(uuid()) email String @unique passwordHash String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt player Player? } model Player { id String @id @default(uuid()) userId String @unique displayName String @unique money Int @default(1000) reputation Int @default(0) level Int @default(1) experience Int @default(0) currentCityId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) currentCity City @relation(fields: [currentCityId], references: [id]) inventory InventoryItem[] missions PlayerMission[] } model City { id String @id @default(uuid()) name String @unique riskLevel Int policePressure Float @default(1.0) economyModifier Float @default(1.0) createdAt DateTime @default(now()) players Player[] prices MarketPrice[] missions Mission[] events WorldEvent[] } model Item { id String @id @default(uuid()) name String @unique basePrice Int rarity Int illegalLevel Int weight Int @default(1) createdAt DateTime @default(now()) inventory InventoryItem[] prices MarketPrice[] missions Mission[] } model InventoryItem { playerId String itemId String quantity Int player Player @relation(fields: [playerId], references: [id]) item Item @relation(fields: [itemId], references: [id]) @@id([playerId, itemId]) } model MarketPrice { cityId String itemId String buyPrice Int sellPrice Int demand Float @default(1.0) supply Float @default(1.0) updatedAt DateTime @updatedAt city City @relation(fields: [cityId], references: [id]) item Item @relation(fields: [itemId], references: [id]) @@id([cityId, itemId]) } model Mission { id String @id @default(uuid()) cityId String title String type MissionType difficulty Int durationSeconds Int rewardMoney Int rewardXp Int risk Float requiredItemId String? requiredItemQuantity Int? expiresAt DateTime createdAt DateTime @default(now()) city City @relation(fields: [cityId], references: [id]) requiredItem Item? @relation(fields: [requiredItemId], references: [id]) playerMissions PlayerMission[] } model PlayerMission { id String @id @default(uuid()) playerId String missionId String status MissionStatus @default(STARTED) startedAt DateTime @default(now()) completesAt DateTime resolvedAt DateTime? player Player @relation(fields: [playerId], references: [id]) mission Mission @relation(fields: [missionId], references: [id]) } model WorldEvent { id String @id @default(uuid()) title String description String type EventType cityId String? priceModifier Float @default(1.0) policeModifier Float @default(1.0) startsAt DateTime endsAt DateTime createdAt DateTime @default(now()) city City? @relation(fields: [cityId], references: [id]) } ``` --- ## 7. API REST MVP Base path: ```text /api/v1 ``` ### Auth ```http POST /auth/register POST /auth/login GET /auth/me ``` #### POST /auth/register Request: ```json { "email": "player@example.com", "password": "password123", "displayName": "ShadowFox" } ``` Response: ```json { "accessToken": "jwt", "player": { "id": "...", "displayName": "ShadowFox", "money": 1000, "level": 1 } } ``` ### Player ```http GET /player/me POST /player/travel ``` #### POST /player/travel Request: ```json { "cityId": "..." } ``` Regola server: - verifica che la città esista; - calcola costo viaggio; - verifica soldi disponibili; - aggiorna città corrente; - scala il denaro. ### Cities ```http GET /cities GET /cities/:cityId ``` ### Market ```http GET /market/current POST /market/buy POST /market/sell ``` #### POST /market/buy Request: ```json { "itemId": "...", "quantity": 3 } ``` Regole server: - il player compra nel mercato della città corrente; - il prezzo lo decide il server; - il costo è `buyPrice * quantity`; - se il player non ha soldi, errore; - aggiorna inventario; - aggiorna domanda/offerta. #### POST /market/sell Request: ```json { "itemId": "...", "quantity": 2 } ``` Regole server: - verifica inventario; - paga `sellPrice * quantity`; - riduce inventario; - aggiorna domanda/offerta. ### Inventory ```http GET /inventory ``` ### Missions ```http GET /missions/available POST /missions/:missionId/start POST /missions/:playerMissionId/claim GET /missions/active ``` #### Avvio missione Il server calcola `completesAt = now + durationSeconds`. #### Claim missione Regole: - se `now < completesAt`, errore; - se già risolta, errore; - calcola successo in base a rischio, livello e reputazione; - assegna premio o fallimento; - aggiorna XP, soldi, reputazione. ### Events ```http GET /events/current ``` ### Leaderboard ```http GET /leaderboard/money GET /leaderboard/reputation ``` --- ## 8. Realtime Socket.IO Endpoint: ```text /ws ``` Eventi server → client: ```text market:updated mission:completed worldEvent:started worldEvent:ended player:notification leaderboard:updated ``` Eventi client → server: ```text client:ping ``` Nel prototipo, le azioni importanti restano REST. Socket.IO serve solo per notificare. --- ## 9. Regole mercato dinamico Ogni città ha prezzi indipendenti. Formula base: ```text rawPrice = basePrice * city.economyModifier * demand / max(supply, 0.25) * eventModifier ``` Prezzo acquisto: ```text buyPrice = round(rawPrice * 1.12) ``` Prezzo vendita: ```text sellPrice = round(rawPrice * 0.88) ``` Limiti: ```text minPrice = basePrice * 0.35 maxPrice = basePrice * 3.50 ``` Ogni acquisto: ```text demand += quantity * 0.02 supply -= quantity * 0.01 ``` Ogni vendita: ```text demand -= quantity * 0.01 supply += quantity * 0.02 ``` Ogni ciclo scheduler: ```text demand = demand * 0.98 + 1.0 * 0.02 supply = supply * 0.98 + 1.0 * 0.02 ``` Questo fa tornare lentamente il mercato verso l’equilibrio. --- ## 10. Scheduler Implementare un processo interno nel backend. ### Ogni minuto - aggiornare prezzi di mercato; - completare notifiche missioni; - cancellare missioni scadute; - generare nuove missioni se sotto soglia. ### Ogni 15 minuti - generare possibile evento locale; - aggiornare classifiche Redis. ### Ogni ora - generare possibile evento globale; - pulizia dati temporanei. Librerie consigliate: ```text node-cron ``` Per versione avanzata: ```text BullMQ + Redis ``` Per MVP basta `node-cron`. --- ## 11. Sicurezza ### Principi - mai fidarsi del client; - validare ogni payload con Zod; - usare JWT con scadenza breve; - password con Argon2 o bcrypt; - rate limit su login e register; - usare HTTPS tramite Nginx; - non esporre PostgreSQL e Redis su internet; - loggare azioni economiche importanti. ### Variabili segrete Nel file `.env`, non committare mai: ```text DATABASE_URL JWT_SECRET POSTGRES_PASSWORD REDIS_URL ``` --- ## 12. File `.env.example` ```env NODE_ENV=development PORT=3000 DATABASE_URL=postgresql://gameuser:gamepassword@db:5432/game REDIS_URL=redis://redis:6379 JWT_SECRET=change_me_in_production JWT_EXPIRES_IN=7d POSTGRES_DB=game POSTGRES_USER=gameuser POSTGRES_PASSWORD=gamepassword ``` --- ## 13. Docker Compose sviluppo ### Regola sui dati persistenti Il server deve salvare tutti i dati persistenti in volumi montati nella stessa directory del progetto, non in volumi Docker anonimi o nominati. Struttura dati locale: ```text contrabbandieri-server/ └── data/ ├── postgres/ # database PostgreSQL persistente ├── redis/ # snapshot Redis, se abilitati └── backups/ # dump manuali o automatici ``` Vantaggi: - puoi copiare l'intera cartella del progetto su un altro server; - i dati sono visibili e controllabili dal filesystem Linux; - backup e restore sono più semplici; - Docker rimane solo il runtime, non il proprietario nascosto dei dati. Prima del primo avvio: ```bash mkdir -p data/postgres data/redis data/backups ``` Permessi consigliati se PostgreSQL ha problemi di scrittura: ```bash sudo chown -R 999:999 data/postgres sudo chown -R 999:999 data/redis ``` ```yaml services: api: build: . container_name: contrabbandieri-api restart: unless-stopped ports: - "3000:3000" env_file: - .env depends_on: - db - redis volumes: - .:/app - /app/node_modules command: npm run dev db: image: postgres:16 container_name: contrabbandieri-db restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - ./data/postgres:/var/lib/postgresql/data ports: - "5432:5432" redis: image: redis:7 container_name: contrabbandieri-redis restart: unless-stopped ports: - "6379:6379" volumes: - ./data/redis:/data ``` --- ## 13.1 Backup e migrazione server Poiché i volumi sono bind-mount locali, la migrazione minima consiste in: 1. fermare i container; 2. fare un dump PostgreSQL; 3. copiare repository, `.env` e cartella `data/backups`; 4. riavviare Docker Compose sul nuovo host. Backup PostgreSQL: ```bash docker compose exec db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > ./data/backups/game_$(date +%Y%m%d_%H%M).sql ``` Restore su nuovo server: ```bash cat ./data/backups/game.sql | docker compose exec -T db psql -U "$POSTGRES_USER" "$POSTGRES_DB" ``` Spegnimento controllato prima di copiare dati grezzi: ```bash docker compose down tar -czf contrabbandieri-data.tar.gz data .env docker-compose.yml ``` Nota: il dump SQL è preferibile alla copia a caldo di `data/postgres`, perché evita inconsistenze se il database è in esecuzione. --- ## 14. Dockerfile backend ```dockerfile FROM node:22-alpine AS base WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npx prisma generate RUN npm run build FROM node:22-alpine AS production WORKDIR /app ENV NODE_ENV=production COPY package*.json ./ RUN npm ci --omit=dev COPY --from=base /app/dist ./dist COPY --from=base /app/prisma ./prisma COPY --from=base /app/node_modules/.prisma ./node_modules/.prisma EXPOSE 3000 CMD ["node", "dist/server.js"] ``` --- ## 15. Package.json base ```json { "name": "contrabbandieri-server", "version": "0.1.0", "type": "module", "scripts": { "dev": "tsx watch src/server.ts", "build": "tsc", "start": "node dist/server.js", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:deploy": "prisma migrate deploy", "prisma:seed": "tsx prisma/seed.ts" }, "dependencies": { "@fastify/cors": "latest", "@fastify/jwt": "latest", "@fastify/rate-limit": "latest", "@prisma/client": "latest", "argon2": "latest", "dotenv": "latest", "fastify": "latest", "ioredis": "latest", "node-cron": "latest", "socket.io": "latest", "zod": "latest" }, "devDependencies": { "@types/node": "latest", "prisma": "latest", "tsx": "latest", "typescript": "latest" } } ``` --- ## 16. Deploy su Linux ### Installazione base ```bash sudo apt update sudo apt install -y ca-certificates curl git ufw ``` ### Docker ```bash curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER ``` Disconnettersi e riconnettersi. ### Avvio ```bash git clone cd contrabbandieri-server cp .env.example .env nano .env docker compose up -d --build ``` ### Migrazioni ```bash docker compose exec api npx prisma migrate deploy docker compose exec api npx prisma db seed ``` --- ## 17. Backup e migrazione ### Backup database ```bash docker exec contrabbandieri-db pg_dump -U gameuser game > backup.sql ``` ### Restore database ```bash cat backup.sql | docker exec -i contrabbandieri-db psql -U gameuser game ``` ### File da portare su altro server ```text .env backup.sql docker-compose.yml nginx/nginx.conf ``` Il codice può essere recuperato da Git. --- ## 18. MVP funzionale minimo Ordine di implementazione: 1. setup Fastify + TypeScript; 2. Docker Compose con PostgreSQL e Redis; 3. Prisma schema; 4. seed città e item; 5. register/login JWT; 6. player profile; 7. inventory; 8. market current; 9. buy/sell; 10. missioni disponibili; 11. start/claim mission; 12. scheduler prezzi; 13. leaderboard; 14. Socket.IO notifiche. --- ## 19. Seed iniziale Città: ```text Milano: economia alta, rischio medio Napoli: economia media, rischio alto Palermo: economia bassa, rischio alto Torino: economia media, rischio basso Bari: economia bassa, rischio medio ``` Item: ```text Gioielli, base 500 Componenti elettronici, base 350 Documenti falsi, base 700 Auto rubate, base 1800 Informazioni riservate, base 1200 Armi leggere, base 1500 ``` Missioni template: ```text Consegna discreta Recupero merce Scambio al porto Infiltrazione uffici Trasporto notturno ``` --- ## 20. Estensioni successive Dopo MVP: - clan/gang; - territori; - PvP asincrono; - aste tra giocatori; - crafting oggetti; - stagioni competitive; - anti-cheat comportamentale; - pannello admin web; - log economico completo; - bilanciamento automatico dell’economia.