Phase 1 Database Implementation (DB-001 through DB-012): Models: - User: OAuth support (Google/Discord), premium subscriptions - Collection: Card ownership with CardSource enum - Deck: JSONB cards/energy_cards, validation state - CampaignProgress: One-to-one with User, medals/NPCs as JSONB - ActiveGame: In-progress games with GameType enum - GameHistory: Completed games with EndReason enum, replay data Infrastructure: - Alembic migrations with sync psycopg2 (avoids async issues) - Docker Compose for Postgres (5433) and Redis (6380) - App config with Pydantic settings - Redis client helper Test Infrastructure: - 68 database tests (47 model + 21 relationship) - Async factory pattern for test data creation - Sync TRUNCATE cleanup (solves pytest-asyncio event loop mismatch) - Uses dev containers instead of testcontainers for reliability Key technical decisions: - passive_deletes=True for ON DELETE SET NULL relationships - NullPool for test sessions (no connection reuse) - expire_on_commit=False with manual expire() for relationship tests
442 lines
15 KiB
JSON
442 lines
15 KiB
JSON
{
|
|
"meta": {
|
|
"version": "1.0.0",
|
|
"created": "2026-01-27",
|
|
"lastUpdated": "2026-01-27",
|
|
"planType": "phase",
|
|
"phaseId": "PHASE_1",
|
|
"phaseName": "Database + Infrastructure",
|
|
"description": "PostgreSQL models, Redis caching, CardService, and development environment setup",
|
|
"totalEstimatedHours": 40,
|
|
"totalTasks": 18,
|
|
"completedTasks": 0,
|
|
"masterPlan": "../PROJECT_PLAN_MASTER.json"
|
|
},
|
|
|
|
"goals": [
|
|
"Establish PostgreSQL as durable storage with async SQLAlchemy",
|
|
"Set up Redis for game state caching and future matchmaking",
|
|
"Implement GameStateManager with write-behind persistence",
|
|
"Create CardService to load bundled JSON into CardDefinitions",
|
|
"Configure environment-based settings (dev/staging/prod)",
|
|
"Docker compose for local development"
|
|
],
|
|
|
|
"architectureNotes": {
|
|
"gameStatePersistence": {
|
|
"strategy": "Write-behind cache",
|
|
"primary": "Redis (fast reads/writes during gameplay)",
|
|
"durable": "PostgreSQL (write at turn boundaries + game end)",
|
|
"recovery": "On restart, load active games from Postgres → Redis"
|
|
},
|
|
"asyncEverywhere": "All DB and Redis operations use async/await",
|
|
"sessionManagement": "Scoped async sessions with proper cleanup"
|
|
},
|
|
|
|
"directoryStructure": {
|
|
"db": "backend/app/db/",
|
|
"services": "backend/app/services/",
|
|
"config": "backend/app/config.py",
|
|
"docker": "backend/docker-compose.yml"
|
|
},
|
|
|
|
"tasks": [
|
|
{
|
|
"id": "DB-001",
|
|
"name": "Create environment config module",
|
|
"description": "Pydantic Settings class for environment-based configuration (database URLs, Redis URL, secrets)",
|
|
"category": "critical",
|
|
"priority": 1,
|
|
"completed": false,
|
|
"dependencies": [],
|
|
"files": [
|
|
{"path": "app/config.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"Use pydantic-settings for env var parsing",
|
|
"Support .env files for local dev",
|
|
"Separate configs: DATABASE_URL, REDIS_URL, SECRET_KEY, OAUTH_*",
|
|
"Environment detection: dev/staging/prod"
|
|
],
|
|
"estimatedHours": 2
|
|
},
|
|
{
|
|
"id": "DB-002",
|
|
"name": "Create Docker compose for local dev",
|
|
"description": "Docker compose file with PostgreSQL and Redis services",
|
|
"category": "critical",
|
|
"priority": 2,
|
|
"completed": false,
|
|
"dependencies": ["DB-001"],
|
|
"files": [
|
|
{"path": "docker-compose.yml", "status": "pending"},
|
|
{"path": ".env.example", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"PostgreSQL 15 with health check",
|
|
"Redis 7 with persistence disabled (dev only)",
|
|
"Volume mounts for data persistence",
|
|
"Network for service communication"
|
|
],
|
|
"estimatedHours": 1
|
|
},
|
|
{
|
|
"id": "DB-003",
|
|
"name": "Set up SQLAlchemy async engine",
|
|
"description": "Create async engine, session factory, and base model class",
|
|
"category": "critical",
|
|
"priority": 3,
|
|
"completed": false,
|
|
"dependencies": ["DB-001"],
|
|
"files": [
|
|
{"path": "app/db/__init__.py", "status": "pending"},
|
|
{"path": "app/db/session.py", "status": "pending"},
|
|
{"path": "app/db/base.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"AsyncEngine with connection pooling",
|
|
"async_sessionmaker for scoped sessions",
|
|
"Base declarative class with common columns (id, created_at, updated_at)",
|
|
"get_session() async context manager"
|
|
],
|
|
"estimatedHours": 2
|
|
},
|
|
{
|
|
"id": "DB-004",
|
|
"name": "Create User model",
|
|
"description": "SQLAlchemy model for user accounts with OAuth support",
|
|
"category": "high",
|
|
"priority": 4,
|
|
"completed": false,
|
|
"dependencies": ["DB-003"],
|
|
"files": [
|
|
{"path": "app/db/models/user.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"Fields: id (UUID), email, display_name, avatar_url",
|
|
"OAuth fields: oauth_provider, oauth_id (composite unique)",
|
|
"Premium fields: is_premium, premium_until (nullable datetime)",
|
|
"Timestamps: created_at, last_login",
|
|
"Index on (oauth_provider, oauth_id)"
|
|
],
|
|
"estimatedHours": 1.5
|
|
},
|
|
{
|
|
"id": "DB-005",
|
|
"name": "Create Collection model",
|
|
"description": "SQLAlchemy model for player card collections",
|
|
"category": "high",
|
|
"priority": 5,
|
|
"completed": false,
|
|
"dependencies": ["DB-004"],
|
|
"files": [
|
|
{"path": "app/db/models/collection.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"Fields: id, user_id (FK), card_definition_id (str), quantity",
|
|
"Source tracking: source (enum: starter, booster, reward, purchase)",
|
|
"Timestamps: obtained_at",
|
|
"Unique constraint on (user_id, card_definition_id)",
|
|
"Index on user_id for fast collection lookups"
|
|
],
|
|
"estimatedHours": 1.5
|
|
},
|
|
{
|
|
"id": "DB-006",
|
|
"name": "Create Deck model",
|
|
"description": "SQLAlchemy model for player decks",
|
|
"category": "high",
|
|
"priority": 6,
|
|
"completed": false,
|
|
"dependencies": ["DB-004"],
|
|
"files": [
|
|
{"path": "app/db/models/deck.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"Fields: id, user_id (FK), name, cards (JSONB), energy_cards (JSONB)",
|
|
"Validation: is_valid (bool), validation_errors (JSONB nullable)",
|
|
"Metadata: is_starter (bool), starter_type (nullable)",
|
|
"Timestamps: created_at, updated_at",
|
|
"Index on user_id"
|
|
],
|
|
"estimatedHours": 1.5
|
|
},
|
|
{
|
|
"id": "DB-007",
|
|
"name": "Create CampaignProgress model",
|
|
"description": "SQLAlchemy model for single-player campaign state",
|
|
"category": "high",
|
|
"priority": 7,
|
|
"completed": false,
|
|
"dependencies": ["DB-004"],
|
|
"files": [
|
|
{"path": "app/db/models/campaign.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"Fields: id, user_id (FK unique), current_club, medals (JSONB)",
|
|
"Progress: defeated_npcs (JSONB), total_wins, total_losses",
|
|
"Economy: booster_packs (int), mantibucks (int)",
|
|
"Timestamps: created_at, updated_at"
|
|
],
|
|
"estimatedHours": 1.5
|
|
},
|
|
{
|
|
"id": "DB-008",
|
|
"name": "Create ActiveGame model",
|
|
"description": "SQLAlchemy model for in-progress games (Postgres backup)",
|
|
"category": "high",
|
|
"priority": 8,
|
|
"completed": false,
|
|
"dependencies": ["DB-003"],
|
|
"files": [
|
|
{"path": "app/db/models/game.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"Fields: id (UUID), game_type (enum: campaign, freeplay, ranked)",
|
|
"Players: player1_id (FK), player2_id (FK nullable), npc_id (nullable)",
|
|
"State: rules_config (JSONB), game_state (JSONB), turn_number",
|
|
"Timing: started_at, last_action_at, turn_deadline (nullable)",
|
|
"Index on player IDs for reconnection lookup"
|
|
],
|
|
"estimatedHours": 2
|
|
},
|
|
{
|
|
"id": "DB-009",
|
|
"name": "Create GameHistory model",
|
|
"description": "SQLAlchemy model for completed games",
|
|
"category": "medium",
|
|
"priority": 9,
|
|
"completed": false,
|
|
"dependencies": ["DB-003"],
|
|
"files": [
|
|
{"path": "app/db/models/game.py", "status": "append"}
|
|
],
|
|
"details": [
|
|
"Fields: id, game_type, player1_id, player2_id, npc_id",
|
|
"Result: winner_id, end_reason, turn_count, duration_seconds",
|
|
"Replay: replay_data (JSONB for future replay feature)",
|
|
"Timestamp: played_at",
|
|
"Indexes for leaderboard queries"
|
|
],
|
|
"estimatedHours": 1.5
|
|
},
|
|
{
|
|
"id": "DB-010",
|
|
"name": "Create models __init__ and export",
|
|
"description": "Consolidate all models in db/models/__init__.py",
|
|
"category": "medium",
|
|
"priority": 10,
|
|
"completed": false,
|
|
"dependencies": ["DB-004", "DB-005", "DB-006", "DB-007", "DB-008", "DB-009"],
|
|
"files": [
|
|
{"path": "app/db/models/__init__.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"Import and re-export all models",
|
|
"Import Base for Alembic"
|
|
],
|
|
"estimatedHours": 0.5
|
|
},
|
|
{
|
|
"id": "DB-011",
|
|
"name": "Set up Alembic migrations",
|
|
"description": "Initialize Alembic and create initial migration",
|
|
"category": "high",
|
|
"priority": 11,
|
|
"completed": false,
|
|
"dependencies": ["DB-010"],
|
|
"files": [
|
|
{"path": "alembic.ini", "status": "pending"},
|
|
{"path": "app/db/migrations/env.py", "status": "pending"},
|
|
{"path": "app/db/migrations/versions/001_initial.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"Configure async Alembic",
|
|
"Auto-generate from SQLAlchemy models",
|
|
"Initial migration with all tables",
|
|
"Add to pyproject.toml dependencies"
|
|
],
|
|
"estimatedHours": 2
|
|
},
|
|
{
|
|
"id": "DB-012",
|
|
"name": "Create Redis connection utilities",
|
|
"description": "Async Redis client with connection pooling",
|
|
"category": "high",
|
|
"priority": 12,
|
|
"completed": false,
|
|
"dependencies": ["DB-001"],
|
|
"files": [
|
|
{"path": "app/db/redis.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"Use redis.asyncio (redis-py)",
|
|
"Connection pool configuration",
|
|
"get_redis() async context manager",
|
|
"Helper methods: get_json, set_json, delete"
|
|
],
|
|
"estimatedHours": 1.5
|
|
},
|
|
{
|
|
"id": "DB-013",
|
|
"name": "Create GameStateManager",
|
|
"description": "Redis-primary, Postgres-backup game state management",
|
|
"category": "critical",
|
|
"priority": 13,
|
|
"completed": false,
|
|
"dependencies": ["DB-012", "DB-008"],
|
|
"files": [
|
|
{"path": "app/services/game_state_manager.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"load_state(): Try Redis, fallback to Postgres",
|
|
"save_to_cache(): Write to Redis only (fast path)",
|
|
"persist_to_db(): Write to Postgres (turn boundaries)",
|
|
"delete_game(): Clean up Redis + Postgres",
|
|
"recover_active_games(): Load from Postgres → Redis on startup",
|
|
"Key format: game:{game_id}"
|
|
],
|
|
"estimatedHours": 3
|
|
},
|
|
{
|
|
"id": "DB-014",
|
|
"name": "Create CardService",
|
|
"description": "Load card definitions from bundled JSON files",
|
|
"category": "critical",
|
|
"priority": 14,
|
|
"completed": false,
|
|
"dependencies": [],
|
|
"files": [
|
|
{"path": "app/services/card_service.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"Load from data/cards/_index.json on startup",
|
|
"Parse JSON → CardDefinition models",
|
|
"get_card(card_id) → CardDefinition",
|
|
"get_cards(card_ids) → dict[str, CardDefinition]",
|
|
"get_cards_by_set(set_code) → list[CardDefinition]",
|
|
"search_cards(filters) for deck building UI",
|
|
"Singleton pattern or dependency injection"
|
|
],
|
|
"estimatedHours": 3
|
|
},
|
|
{
|
|
"id": "DB-015",
|
|
"name": "Create database tests",
|
|
"description": "Tests for models, sessions, and basic CRUD",
|
|
"category": "high",
|
|
"priority": 15,
|
|
"completed": false,
|
|
"dependencies": ["DB-011"],
|
|
"files": [
|
|
{"path": "tests/db/__init__.py", "status": "pending"},
|
|
{"path": "tests/db/conftest.py", "status": "pending"},
|
|
{"path": "tests/db/test_models.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"Use testcontainers-postgres for isolated DB",
|
|
"Test each model: create, read, update, delete",
|
|
"Test relationships and constraints",
|
|
"Test JSONB serialization"
|
|
],
|
|
"estimatedHours": 4
|
|
},
|
|
{
|
|
"id": "DB-016",
|
|
"name": "Create GameStateManager tests",
|
|
"description": "Tests for Redis + Postgres state management",
|
|
"category": "high",
|
|
"priority": 16,
|
|
"completed": false,
|
|
"dependencies": ["DB-013", "DB-015"],
|
|
"files": [
|
|
{"path": "tests/services/test_game_state_manager.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"Use testcontainers for Redis",
|
|
"Test cache hit (Redis has state)",
|
|
"Test cache miss (fallback to Postgres)",
|
|
"Test persist_to_db writes correctly",
|
|
"Test recovery on startup",
|
|
"Test cleanup on game end"
|
|
],
|
|
"estimatedHours": 3
|
|
},
|
|
{
|
|
"id": "DB-017",
|
|
"name": "Create CardService tests",
|
|
"description": "Tests for card loading and lookup",
|
|
"category": "high",
|
|
"priority": 17,
|
|
"completed": false,
|
|
"dependencies": ["DB-014"],
|
|
"files": [
|
|
{"path": "tests/services/test_card_service.py", "status": "pending"}
|
|
],
|
|
"details": [
|
|
"Test loads all cards from JSON",
|
|
"Test get_card returns correct definition",
|
|
"Test get_cards batch lookup",
|
|
"Test search with filters (type, set, rarity)",
|
|
"Test handles missing card gracefully"
|
|
],
|
|
"estimatedHours": 2
|
|
},
|
|
{
|
|
"id": "DB-018",
|
|
"name": "Update dependencies in pyproject.toml",
|
|
"description": "Add all required packages for Phase 1",
|
|
"category": "high",
|
|
"priority": 18,
|
|
"completed": false,
|
|
"dependencies": [],
|
|
"files": [
|
|
{"path": "pyproject.toml", "status": "update"}
|
|
],
|
|
"details": [
|
|
"sqlalchemy[asyncio] >= 2.0",
|
|
"asyncpg (Postgres async driver)",
|
|
"alembic",
|
|
"redis >= 5.0 (async support)",
|
|
"pydantic-settings",
|
|
"testcontainers[postgres,redis] (dev)"
|
|
],
|
|
"estimatedHours": 0.5
|
|
}
|
|
],
|
|
|
|
"testingStrategy": {
|
|
"approach": "Testcontainers for isolated Postgres/Redis",
|
|
"fixtures": "Async fixtures in conftest.py",
|
|
"coverage": "Target 90%+ on new code"
|
|
},
|
|
|
|
"weeklyRoadmap": {
|
|
"week1": {
|
|
"theme": "Foundation",
|
|
"tasks": ["DB-001", "DB-002", "DB-003", "DB-018"],
|
|
"goals": ["Dev environment running", "SQLAlchemy configured"]
|
|
},
|
|
"week2": {
|
|
"theme": "Models + Migrations",
|
|
"tasks": ["DB-004", "DB-005", "DB-006", "DB-007", "DB-008", "DB-009", "DB-010", "DB-011"],
|
|
"goals": ["All models defined", "Migrations working"]
|
|
},
|
|
"week3": {
|
|
"theme": "Services + Testing",
|
|
"tasks": ["DB-012", "DB-013", "DB-014", "DB-015", "DB-016", "DB-017"],
|
|
"goals": ["GameStateManager working", "CardService working", "Full test coverage"]
|
|
}
|
|
},
|
|
|
|
"acceptanceCriteria": [
|
|
"docker-compose up starts Postgres + Redis locally",
|
|
"Alembic migrations run successfully",
|
|
"All models have CRUD tests passing",
|
|
"GameStateManager persists at turn boundaries",
|
|
"GameStateManager recovers from Postgres on restart",
|
|
"CardService loads all 372 cards from JSON",
|
|
"All tests pass with testcontainers"
|
|
]
|
|
}
|