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:

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:

UserCharacterEquipment (one-to-one)
CharacterInventoryItem[]
CharacterCharacterSkill[]
CharacterActiveQuest[]
CharacterClan (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:

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:

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:

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:

  1. 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.
  2. 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.
  3. 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 CategoryMitigation
A01 — Broken Access ControlJWT auth on every protected endpoint, require_admin dependency for admin routes, audit log for 401/403/429
A02 — Cryptographic Failuresbcrypt 12 rounds, JWT HS256, HTTPS enforced via Nginx HSTS
A03 — InjectionSQLAlchemy ORM (parameterized queries only), Pydantic input validation
A05 — Security MisconfigurationCSP, X-Frame-Options, X-XSS-Protection headers via SecurityHeadersMiddleware
A07 — Auth FailuresRate limiting on auth (3 attempts/hour), refresh token rotation, token revocation on logout
A09 — Logging FailuresAuditLog model — all 401/403/429 events written to DB + honeypot endpoint

Lessons Learned

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 = ON in 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.