Why FastAPI for a Game?
The obvious choices for a browser game backend are Node.js or PHP. We picked Python with FastAPI, and it turned out to be the right call for a game like VvW.
Turn-based browser RPGs are not real-time games. There are no WebSocket latency requirements, no frame-rate-critical game loops. Every action is an HTTP request. The server resolves the action, updates the database, and returns the result. This is exactly what REST APIs are built for.
FastAPI gave us:
- Async I/O — critical for PostgreSQL connection pooling at scale
- Pydantic validation — every request body is validated before touching the DB
- Automatic OpenAPI docs — the game's API is self-documenting at
/api/docs - Built-in rate limiting integration with SlowAPI — no separate middleware stack
- SQLAlchemy 2.0 async ORM — first-class support for the latest SQLAlchemy
Architecture Overview
The application is a single FastAPI app with 27 routers, each handling a feature domain.
All routers are registered in main.py under the /api/ prefix.
vampires-vs-werewolves/
├── backend/
│ ├── main.py # FastAPI app, middleware, routers
│ ├── config.py # Settings via pydantic-settings
│ ├── database.py # AsyncEngine, AsyncSession, get_db
│ ├── auth.py # JWT, bcrypt, get_current_user
│ ├── models.py # All SQLAlchemy ORM models
│ ├── limiter.py # SlowAPI rate limiter
│ ├── scheduler.py # APScheduler (daily reset, clan wars)
│ ├── game_logic/
│ │ ├── battle_engine.py # Combat formulas, crits, skills
│ │ ├── xp_engine.py # XP curves, level-up logic
│ │ └── achievements.py # Achievement seeding & checking
│ ├── routers/ # 27 routers
│ │ ├── auth.py # register, login, refresh, export-data
│ │ ├── character.py # stats, work, level-up
│ │ ├── battle.py # hunt, pvp, battle history
│ │ ├── inventory.py # equip, sell, use
│ │ ├── shop.py # NPC shop
│ │ ├── crafting.py # recipes, forge, alchemy
│ │ ├── dungeons.py # 10 dungeons, boss encounters
│ │ ├── boss.py # World bosses
│ │ ├── clans.py # create, join, war declare
│ │ ├── events.py # Eclipse War, Blood Moon scoring
│ │ ├── quests.py # 100 quests
│ │ ├── skills.py # 100 skills
│ │ ├── achievements.py # 80 achievements
│ │ ├── ranking.py # Global/faction leaderboards
│ │ ├── auction.py # Player-to-player marketplace
│ │ ├── chat.py # Zone/Global/DM chat
│ │ ├── social.py # Friends, activity feed
│ │ ├── mail.py # In-game mail
│ │ ├── notifications.py # Push notifications
│ │ ├── daily.py # Daily login rewards, streak
│ │ ├── prestige.py # Endgame prestige system
│ │ ├── missions.py # Time-based missions
│ │ ├── map.py # 30 locations
│ │ ├── admin.py # Admin endpoints
│ │ ├── support.py # Support tickets
│ │ ├── waitlist.py # Pre-launch email waitlist
│ │ └── tutorial.py # New player tutorial
│ ├── alembic/ # 13 database migrations
│ └── tests/ # 33 test files, 300+ tests
├── static/
│ ├── css/ # 12 CSS files (variables, components)
│ ├── js/ # cookie-consent.js, etc.
│ └── data/ # monsters.json, items.json, etc.
├── game/ # All in-game HTML pages
├── blog/ # SEO blog posts
└── legal/ # Terms, Privacy Policy
Database Design
The database has 35+ tables. The core model graph looks like this:
User → Character → Equipment (one-to-one)
Character → InventoryItem[]
Character → CharacterSkill[]
Character → ActiveQuest[]
Character → Clan (via ClanMember)
The trickiest table is BattleLog. It logs every PvE and PvP fight with the full
round-by-round breakdown (stored as JSON). This powers:
- Battle history replays
- Eclipse War faction scoring (aggregated with a single SQL query)
- Achievement progress tracking (e.g., "win 100 PvP battles")
- Anti-cheat analysis (unusual win rates, impossible damage numbers)
We used Alembic for migrations from the start. Lesson: always set up Alembic before you write your first model. Adding it later to an existing schema is painful.
Authentication System
Auth is JWT-based with access + refresh token rotation. The access token expires in 15 minutes;
refresh tokens last 7 days. Both are stored client-side in localStorage
(not httpOnly cookies, since the game runs as a static SPA without server-side rendering).
Security mitigations for localStorage token storage:
- CSP header blocks all inline script injection on the domain
- X-Frame-Options: DENY prevents clickjacking
- Rate limiting on all auth endpoints (3 register attempts/hour per IP)
- Refresh token rotation — stealing an old refresh token doesn't work after rotation
- Opaque refresh tokens (64-byte random, stored as hashed value in DB)
Passwords are hashed with bcrypt, rounds=12. The GDPR data export endpoint
returns all user data but explicitly excludes hashed_password.
Game Logic Engine
The battle engine is the heart of the game. It lives in
backend/game_logic/battle_engine.py and handles:
- Damage calculation with variance:
base_dmg × random(0.85, 1.15) - Defense reduction formula:
damage_reduction = DEF / (DEF + 50) - Critical hits:
LCK / 100chance for 1.75× damage - Dodge:
DEX / (DEX + 30)chance to avoid all damage - Skill effects: bleed, stun, life steal, damage amplification
- Safeguard: battles end after max 50 rounds to prevent infinite loops
The PvP formula uses ELO rating for matchmaking and ranking, with a ±400 point restriction on attacks (you can't attack someone 400 ELO points above you). This prevents high-level players from farming beginners.
Frontend: No Framework Needed
We deliberately avoided React, Vue, and Angular. The reasons:
- Page-based RPG model is a perfect fit for multi-page apps. Each game action navigates to a new page (hunt results, shop checkout, dungeon entry). This is how BiteFight worked in 2006. It still works today.
-
Zero build step. The frontend is plain HTML + CSS + vanilla JS.
No webpack, no Babel, no
node_modules. The entire frontend deploys as static files. - SEO is straightforward. Every page is a real HTML file that crawlers can index directly. No SSR needed, no hydration complexity.
The api helper object wraps all fetch() calls with automatic
JWT header injection and token refresh logic. Every page includes this helper and calls
the relevant API endpoints on load.
Security Layers
We targeted OWASP Top 10 compliance from day one:
| OWASP Category | Mitigation |
|---|---|
| A01 — Broken Access Control | JWT auth on every protected endpoint, require_admin dependency for admin routes, audit log for 401/403/429 |
| A02 — Cryptographic Failures | bcrypt 12 rounds, JWT HS256, HTTPS enforced via Nginx HSTS |
| A03 — Injection | SQLAlchemy ORM (parameterized queries only), Pydantic input validation |
| A05 — Security Misconfiguration | CSP, X-Frame-Options, X-XSS-Protection headers via SecurityHeadersMiddleware |
| A07 — Auth Failures | Rate limiting on auth (3 attempts/hour), refresh token rotation, token revocation on logout |
| A09 — Logging Failures | AuditLog model — all 401/403/429 events written to DB + honeypot endpoint |
Lessons Learned
-
Alembic migrations from day one. We started with
create_all()in development for speed, but setting up proper Alembic migrations early saves enormous pain during staging/production syncing. - Seed data is a feature, not an afterthought. The achievement seeder runs on startup and is idempotent. Same approach for monster data, item catalogs, and quest definitions. Loading static game data from JSON files and seeding to DB at startup is much cleaner than querying JSON files at runtime.
- Rate limiting everything from day one. SlowAPI is trivial to integrate and a hacker's first move is always hammering auth endpoints. We added rate limits before writing the first router.
- The scheduler is essential. APScheduler running daily resets, token cleanup, and clan war resolution in the background is what makes the game feel alive. Don't launch without scheduled tasks — games live and die by their cadence.
-
Test in SQLite, deploy to PostgreSQL. We use SQLite+aiosqlite for CI tests
(fast, no docker-compose required). Works seamlessly with SQLAlchemy's dialect abstraction.
The only gotcha: SQLite doesn't enforce foreign keys by default — run
PRAGMA foreign_keys = ONin your test setup.
What's Coming Next
DevLog #2 will cover the PvP system — specifically how we designed the ELO-based ranking, the immunity window mechanic that protects new players, and the clan war auto-resolver scheduler.
DevLog #3 will be about balance — the math behind the XP curve, gold economy, and how we tune encounter difficulty so that every level range feels appropriately challenging without being a grind wall.
The full source code will be made available after the public beta launch. Join the waitlist to be notified.