15 KiB
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-accept mock
| Method | Path | Purpose |
|---|---|---|
| POST | /api/payments/robokassa/link |
Returns mock success, grants shards |
| POST | /api/payments/bundles/link |
Instantly unlocks bundle |
| GET | /api/payments/deals?steamId= |
Returns mock deal catalog |
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_PASSWORDenv 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.tsCREATE 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)