Files
Dota-Zombie-Invasion-Backend/docs/superpowers/specs/2026-05-29-zombie-invasion-backend-design.md
2026-05-29 20:04:31 +07:00

15 KiB

Zombie Invasion — Backend & Admin Panel Design

Overview

A lightweight Next.js application that serves as both:

  • The REST API backend that the Zombie Invasion Dota 2 custom game client talks to
  • An admin panel (served under /admin) for managing all player data

Deployed as a single Docker container with SQLite for storage. All payment-related endpoints auto-accept (no real payment integration).

Tech Stack

  • Framework: Next.js 14 (App Router)
  • Database: SQLite via better-sqlite3
  • Language: TypeScript
  • Container: Docker (single container, multi-stage build)
  • Port: 3000 (same as the original game server)

Architecture

nextjs-app/
├── Dockerfile                 # Multi-stage: build → run
├── docker-compose.yml         # Single service
├── docker-entrypoint.sh       # DB init + seed + start
├── next.config.js
├── package.json
├── src/
│   ├── app/
│   │   ├── layout.tsx                    # Root layout
│   │   ├── page.tsx                      # Redirects to /admin
│   │   ├── admin/
│   │   │   ├── layout.tsx                # Admin sidebar + auth check
│   │   │   ├── page.tsx                  # Dashboard (counts, quick-stats)
│   │   │   ├── login/page.tsx            # Simple password login
│   │   │   ├── players/[steamId]/page.tsx # Single player editor
│   │   │   ├── players/page.tsx          # Player list + search
│   │   │   ├── battlepass/[steamId]/page.tsx
│   │   │   ├── battlepass/page.tsx       # BP overview
│   │   │   ├── matches/page.tsx          # Match history browser
│   │   │   ├── promocodes/page.tsx       # Manage promo codes
│   │   │   ├── store/page.tsx            # Purchases, currencies
│   │   │   ├── contracts/page.tsx        # Death sentence contracts
│   │   │   └── arsenal/page.tsx          # Arsenal & marketplace
│   │   └── api/
│   │       └── [...path]/
│   │           └── route.ts              # Catch-all: dispatches game client requests
│   └── lib/
│       ├── db.ts                         # SQLite singleton + schema init
│       ├── seed.ts                       # Initial data (promo codes, sample quests)
│       ├── auth.ts                       # Simple admin auth helpers
│       └── handlers/
│           ├── player.ts                 # Profile, currency, history, purchases
│           ├── battlepass.ts             # BP data, quests, claim rewards
│           ├── game.ts                   # Match tracking, heartbeat
│           ├── payments.ts               # Auto-accept mock payments
│           ├── leaderboard.ts            # Leaderboard queries
│           ├── cards.ts                  # Card levels, decks
│           ├── equipment.ts              # Equipment state
│           ├── arsenal.ts               # Arsenal loadouts + inventory
│           ├── marketplace.ts            # Marketplace listings + sales
│           └── contracts.ts              # Death sentence contracts
├── data/                                 # SQLite DB file (Docker volume mount)
└── Dockerfile

Database Schema

players

Column Type Notes
steam_id TEXT PK
player_name TEXT NOT NULL
profile_level INTEGER Default 1
free_currency INTEGER Default 0
donate_currency INTEGER Default 0
dust_currency INTEGER Default 0
arcade_pack_credits TEXT JSON {standard, premium}
sounds_wheel TEXT JSON object of sound_id → true
created_at TEXT ISO datetime
updated_at TEXT ISO datetime

game_sessions

Column Type Notes
game_id TEXT PK
match_id INTEGER Shared across party
session_id TEXT
status TEXT active / completed
created_at TEXT

game_history

Column Type Notes
id INTEGER PK AUTO
steam_id TEXT NOT NULL
game_id TEXT
match_id INTEGER
result TEXT win / loss
hero TEXT
hero_level INTEGER
difficulty TEXT
duration INTEGER seconds
kills INTEGER
deaths INTEGER
score INTEGER net worth
outgoing_damage REAL
incoming_damage REAL
items TEXT comma-separated
modifiers TEXT comma-separated
aghanim_scepter INTEGER 0/1
aghanim_shard INTEGER 0/1
gold_earned INTEGER
session_id TEXT
created_at TEXT

battle_passes

Column Type Notes
steam_id TEXT PK
level INTEGER Default 0
experience INTEGER Default 0
has_premium INTEGER 0/1
claimed_rewards TEXT JSON array of level numbers
claimed_premium_rewards TEXT JSON array of level numbers
created_at TEXT
updated_at TEXT

battle_pass_quests

Column Type Notes
id INTEGER PK AUTO
steam_id TEXT NOT NULL
quest_id TEXT NOT NULL
type TEXT kill_zombies, survive_time, etc.
name TEXT
description TEXT
progress INTEGER
target INTEGER
completed INTEGER 0/1
claimed INTEGER 0/1
reward_exp INTEGER
reward_free_currency INTEGER
quality TEXT nullable
npc TEXT nullable
target_item TEXT nullable
created_at TEXT
updated_at TEXT

purchases

Column Type Notes
id INTEGER PK AUTO
steam_id TEXT NOT NULL
item_id TEXT NOT NULL
item_category TEXT items, cards, chat_wheel_sound, etc.
card_id INTEGER nullable
price_free INTEGER
price_donate INTEGER
price_dust INTEGER
created_at TEXT

active_effects

Column Type Notes
steam_id TEXT PK
effects TEXT JSON: {effect_type: effect_id}
updated_at TEXT

promo_codes

Column Type Notes
code TEXT PK
free_currency INTEGER reward amount
donate_currency INTEGER reward amount
dust_currency INTEGER reward amount
max_uses INTEGER default 1
current_uses INTEGER
expires_at TEXT nullable, ISO datetime

promo_redemptions

Column Type Notes
steam_id TEXT PK (composite)
code TEXT PK (composite)
redeemed_at TEXT

card_levels

Column Type Notes
steam_id TEXT PK
card_levels TEXT JSON: {card_id: level}
updated_at TEXT

decks

Column Type Notes
steam_id TEXT PK (composite)
deck_index INTEGER PK (composite)
name TEXT
cards TEXT JSON array of card IDs
updated_at TEXT

equipment

Column Type Notes
steam_id TEXT PK
equipment TEXT JSON: {weapon, armor, ...}
updated_at TEXT

arsenal_loadouts

Column Type Notes
steam_id TEXT PK (composite)
hero_name TEXT PK (composite)
loadout TEXT JSON: {weapon, armor}
updated_at TEXT

arsenal_inventory

Column Type Notes
steam_id TEXT PK (composite)
instance_id TEXT PK (composite)
item_name TEXT
quality TEXT
upgrade_level INTEGER
serial INTEGER
global_serial INTEGER
owner_name TEXT
pinned INTEGER 0/1
favorite INTEGER 0/1
stats TEXT JSON array

arsenal_market_listings

Column Type Notes
listing_id TEXT PK
steam_id TEXT NOT NULL
instance_id TEXT
item_name TEXT
quality TEXT
upgrade_level INTEGER
serial INTEGER
global_serial INTEGER
price_free INTEGER
status TEXT active / sold / cancelled
created_at TEXT

arsenal_market_sales

Column Type Notes
id INTEGER PK AUTO
listing_id TEXT
seller_steam_id TEXT
buyer_steam_id TEXT
item_name TEXT
price_free INTEGER
created_at TEXT

death_sentence_contracts

Column Type Notes
steam_id TEXT PK
contracts TEXT JSON roster
updated_at TEXT

API Endpoints (Game Client)

All under /api/. The catch-all route looks at the URL path and HTTP method to dispatch.

Player (/api/player/:steamId)

Method Path Purpose
POST /api/player Create profile
GET /api/player/:steamId Get profile + currencies + stats
GET /api/player/:steamId/history Match history (limit, offset)
GET /api/player/:steamId/currency Get currency balances
PUT /api/player/:steamId/currency Save currency
POST /api/player/:steamId/currency/give Grant currency (BP rewards)
POST /api/player/:steamId/purchases Record a purchase
POST /api/player/:steamId/promo/redeem Redeem promo code
GET /api/player/:steamId/sounds_wheel Get chat wheel sounds
PUT /api/player/:steamId/sounds_wheel Save chat wheel sounds
POST /api/player/:steamId/deal-purchase Buy a deal
GET /api/player/:steamId/active_effects Get equipped effects
PUT /api/player/:steamId/active_effects Save equipped effects
GET /api/player/:steamId/card-levels Get card levels
PUT /api/player/:steamId/card-levels Update card levels
GET /api/player/:steamId/decks Get all decks
GET /api/player/:steamId/decks/:index Get one deck
PUT /api/player/:steamId/decks/:index Save one deck
GET /api/player/:steamId/equipment Get equipment
PUT /api/player/:steamId/equipment Save equipment
POST /api/player/:steamId/equipment/drop Equipment drop
GET /api/player/:steamId/arsenal_loadouts Get arsenal loadouts
PUT /api/player/:steamId/arsenal_loadouts Save arsenal loadouts
GET /api/player/:steamId/arsenal_inventory Get arsenal inventory
PUT /api/player/:steamId/arsenal_inventory Save arsenal inventory
GET /api/player/:steamId/arsenal_market/my_listings My active listings
GET /api/player/:steamId/arsenal_market/slots Market slot info
GET /api/player/:steamId/arsenal_market/sales My sales history
POST /api/player/:steamId/arsenal_market/create Create listing
POST /api/player/:steamId/arsenal_market/buy Buy from listing
POST /api/player/:steamId/arsenal_market/cancel Cancel listing
GET /api/player/:steamId/death_sentence_contracts Get contracts
PUT /api/player/:steamId/death_sentence_contracts Save contracts

Battle Pass (/api/battlepass)

Method Path Purpose
POST /api/battlepass Create BP
GET /api/battlepass/:steamId Get BP data
POST /api/battlepass/:steamId/hero-played Record hero played
GET /api/battlepass/:steamId/quests Get quests
POST /api/battlepass/:steamId/quests/progress Sync quest progress
POST /api/battlepass/:steamId/quests/claim Claim quest reward
POST /api/battlepass/:steamId/claim Claim BP level reward
POST /api/battlepass/:steamId/claim-premium Claim premium level reward
POST /api/battlepass/:steamId/claim-all Claim all rewards
POST /api/battlepass/:steamId/buy-premium Buy premium BP
POST /api/battlepass/:steamId/addexp Add BP XP

Game (/api/game)

Method Path Purpose
POST /api/game/start Register game start
POST /api/game/heartbeat Match heartbeat
POST /api/game Save game result
GET /api/game/:id/players Get match participants

Payments (/api/payments) — auto-grant (no actual payment)

Method Path Purpose
POST /api/payments/robokassa/link Instantly grants purchased currency to player balance, writes to DB
POST /api/payments/bundles/link Instantly grants bundle items/writes purchase to DB
GET /api/payments/deals?steamId= Returns deal catalog (deals purchasable with in-game currency)

Leaderboard (/api/leaderboard)

Method Path Purpose
GET /api/leaderboard?limit=&offset=&board= Leaderboard by rating/wealth

Marketplace (/api/arsenal_market)

Method Path Purpose
GET /api/arsenal_market/listings Public listings (with optional stat filters)

Response Format

The game client (Lua) expects JSON responses. The catch-all handler wraps each response:

  • Success (2xx): returns the JSON body directly
  • 404: returns {error: "Not found"}
  • The game handles both wrapped responses {ok: true, data: ...} and unwrapped objects

Since different game modules expect different response shapes (some expect arrays, some expect objects, some look for specific keys), each handler returns the exact shape the game code expects.

Admin Panel

Authentication

  • Single password set via ADMIN_PASSWORD env var
  • Cookie-based session (simple, no JWT library needed)
  • Login page at /admin/login
  • All /admin/* routes check auth middleware

Pages

Path Content
/admin Dashboard with quick stats (player count, games played, active BPs)
/admin/players Searchable player list with currency/level/bp overview
/admin/players/[steamId] Edit all player fields, view purchases, effects
/admin/battlepass Overview of all BPs with search
/admin/battlepass/[steamId] Edit BP level/XP/premium, add/manage quests
/admin/matches Browse match history, filter by player/hero/difficulty
/admin/promocodes List, create, edit, delete promo codes
/admin/store View player purchases and active effects
/admin/contracts View/edit death sentence contracts
/admin/arsenal View inventory, loadouts, marketplace listings

Docker Setup

# Multi-stage build: node:20-alpine
# Stage 1: Install deps + build Next.js
# Stage 2: Run with production deps + SQLite data volume
# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "6100:3000"             # Host:6100 → Container:3000
    volumes:
      - ./data:/app/data          # Persist SQLite DB
    environment:
      - ADMIN_PASSWORD=admin123

Seed Data

On first run (empty DB), the entrypoint seeds:

  • A few promo codes (e.g. WELCOME100, ZOMBIE500)
  • The DB schema itself (via db.ts CREATE TABLE IF NOT EXISTS)
  • A test player if none exist

Non-Goals

  • No user registration / multi-tenant support (single personal server)
  • No real payment processing
  • No WebSocket / real-time features
  • No metrics / logging beyond basic requests
  • No automated test suite (manual testing via game client + admin panel)