Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Delve — Technical Architecture

Table of Contents

  1. Architecture Overview
  2. Technology Stack
  3. Client Architecture
  4. Server Architecture
  5. Database Design
  6. Game Systems — Server
  7. Game Systems — Client
  8. Polling & Notifications
  9. Chat — Discord Integration
  10. Mobile Wrapping
  11. Infrastructure & DevOps
  12. Security
  13. Performance Targets

1. Architecture Overview

Delve is a server-authoritative async idle MMO rendered as a web application and wrapped for mobile distribution. Because combat is never real-time (players configure a skill queue, the server resolves it), the architecture optimizes for throughput of simulation ticks and low-frequency but reliable client updates rather than sub-100ms latency.

┌─────────────────────────────────────────────────────┐
│                     CLIENTS                         │
│  Browser (SPA)  ·  iOS (Capacitor)  ·  Android      │
└──────────┬──────────────────────────────┬───────────┘
           │ HTTPS (REST + polling)       │ Push
           ▼                              ▼
┌─────────────────────┐      ┌────────────────────────┐
│   API Gateway /     │      │  Push Notification     │
│   Load Balancer     │      │  Service (FCM / APNs)  │
│   (Caddy)           │      └────────────────────────┘
└──────────┬──────────┘
           │                  ┌────────────────────────┐
           ▼                  │  Discord               │
┌─────────────────────┐      │  (Chat, community,     │
│   REST API Servers  │      │   LFG, guild comms)    │
└──────────┬──────────┘      └────────────────────────┘
           │
┌──────────┴──────────────────────────────────────────┐
│              BullMQ Job Queue (Redis-backed)         │
└──────┬────────────┬────────────┬────────────────────┘
       ▼            ▼            ▼
┌────────────┐ ┌──────────┐ ┌──────────────┐
│  Simulation│ │ Economy  │ │  PVP Match   │
│  Workers   │ │ Workers  │ │  Workers     │
└────────────┘ └──────────┘ └──────────────┘
       │            │            │
       ▼            ▼            ▼
┌─────────────────────────────────────────────────────┐
│                   Data Layer                        │
│         PostgreSQL  ·  Redis  ·  Backblaze B2       │
└─────────────────────────────────────────────────────┘

Design Principles

  • Server-authoritative: All game state mutations happen server-side. The client is a view layer.
  • Async-first: Most gameplay resolves on the server without a connected client.
  • REST + polling, no WebSockets: The game is inherently async — players wait minutes to hours for results. Polling every 30–60s is perfectly adequate and dramatically simplifies the server (no persistent connections, no connection state, no reconnect logic).
  • Discord for chat: Community, guild coordination, LFG, and trade chat all happen on Discord. This is where the community already lives, and it eliminates an entire real-time system from the codebase.
  • Horizontally scalable: Stateless API servers; sharded workers keyed by character/guild ID.
  • Offline-tolerant: Runs, crafting, and gathering proceed whether the player is online or not.

2. Technology Stack

Client

LayerTechnologyRationale
UI FrameworkSvelteKit (SPA mode)Small bundle size (~30KB framework), fast reactivity, excellent mobile perf. SPA mode since all logic is server-side.
RenderingHTML/CSS + PixiJS (optional)Most of Delve is UI-driven (queues, inventories, logs). PixiJS available for animated run replays and map rendering.
State ManagementSvelte stores + TanStack QueryStores for local UI state; TanStack Query for server state caching, deduplication, and background refetching.
StylingTailwind CSSUtility-first, tree-shakes to small bundle. Fantasy theme via design tokens.
Mobile WrapperCapacitorWeb-to-native bridge for iOS/Android. Access to push notifications, haptics, secure storage.
BuildViteFast HMR in dev, optimized production builds with code splitting.

Server

LayerTechnologyRationale
LanguageRustHigh performance for the simulation engine. Strong type system catches bugs at compile time. Single binary deploys. Memory safety without GC pauses.
API FrameworkAxumTokio-based, ergonomic extractors, tower middleware ecosystem. The standard choice for Rust web services.
Task SchedulingCustom Redis-backed job queue (via redis crate)Delayed jobs for run completion, crafting timers, gathering expeditions, raid scheduling, auction expiry. Rust doesn’t have a BullMQ equivalent — a simple custom queue on Redis ZADD/ZPOPMIN with timestamps is sufficient and avoids a heavy dependency.
ORM / QuerySQLxCompile-time checked SQL queries against the real database. No ORM overhead — write SQL directly with type-safe results. Async Postgres driver built-in.
Serializationserde + serde_jsonIndustry-standard Rust serialization. Used for API request/response bodies, JSONB fields, and game data definitions.
Validationvalidator crate + custom typesDerive-based validation on request structs. Newtype pattern for domain-specific constraints (e.g., AttributeValue(u8) that enforces 1–99 range).
Authargon2 crate + custom session middlewareSession-based auth with Argon2id password hashing. Sessions stored in Redis. OAuth2 via oauth2 crate for social logins.

Data

LayerTechnologyRationale
Primary DBPostgreSQL 16JSONB for flexible item properties, strong indexing, reliable ACID transactions for economy.
Cache / JobsRedis 7 (Valkey)Session store, leaderboard sorted sets, rate limiting, job queue backing store.
Object StorageS3-compatible (Backblaze B2)Run replay logs, seasonal assets, user avatars. Accessed via aws-sdk-s3 crate.
Search (future)MeilisearchMarketplace full-text search, bestiary/recipe lookup.

Infrastructure

LayerTechnologyRationale
ContainersDocker + Docker Compose (dev), Kubernetes (prod)Single binary per service → tiny Docker images (~10–20MB with FROM scratch or Alpine).
Reverse ProxyCaddyAutomatic HTTPS, HTTP/2, simple config.
CI/CDGitHub ActionsBuild, test, deploy pipeline. Rust builds cached via sccache or cargo-chef Docker layer.
MonitoringPrometheus + GrafanaMetrics exported via metrics + metrics-exporter-prometheus crates.
Loggingtracing + tracing-subscriber → JSON → LokiStructured logging with spans. The tracing ecosystem is Rust’s standard for observability.
Error TrackingSentry (sentry-rust crate)Server-side panic/error capture. Client errors via Sentry JS SDK.

3. Client Architecture

3.1 Application Structure

src/
├── lib/
│   ├── api/              # API client, polling, query hooks
│   │   ├── client.ts     # Hono RPC typed client (end-to-end type safety)
│   │   └── queries/      # TanStack Query definitions per domain (with polling intervals)
│   ├── stores/           # Svelte stores for client-side state
│   │   ├── auth.ts       # Current user session
│   │   ├── notifications.ts
│   │   └── ui.ts         # Theme, sidebar state, modals
│   ├── components/       # Reusable UI components
│   │   ├── character/    # Character sheet, skill queue builder
│   │   ├── combat/       # Run replay viewer, encounter log
│   │   ├── inventory/    # Gear grid, item tooltips, drag-and-drop
│   │   ├── marketplace/  # Listings, search, buy/sell flows
│   │   ├── social/       # Guild panel, friends list, Discord links
│   │   └── ui/           # Buttons, modals, toasts, progress bars
│   ├── game/             # Client-side game logic
│   │   ├── tooltips.ts   # Stat calculation for item/skill tooltips
│   │   ├── timers.ts     # Countdown display for active runs/crafts
│   │   └── constants.ts  # Shared enums, rarity colors, etc.
│   └── types/            # TypeScript types matching the Rust API contract
│       ├── character.ts  # Character, Attributes, Species, Class
│       ├── item.ts       # Item, Rarity, ItemLocation, BonusProperty
│       ├── skill.ts      # SkillQueueSlot, QueueCondition
│       ├── combat.ts     # RunLog, EncounterLog, ActionLog
│       └── api.ts        # Request/response types per endpoint
├── routes/
│   ├── (auth)/           # Login, register, password reset
│   ├── (game)/           # Main game layout wrapper
│   │   ├── character/    # Character sheet, progression, skill queue
│   │   ├── quest-board/  # Available quests, active runs
│   │   ├── inventory/    # Gear, backpack, bank
│   │   ├── crafting/     # Profession UIs, recipe browser
│   │   ├── marketplace/  # Auction house
│   │   ├── guild/        # Guild management, lobby scheduling
│   │   ├── pvp/          # Arena queue, leaderboards
│   │   ├── world/        # Map, factions, bestiary
│   │   └── settings/     # Account, notifications, appearance
│   └── +layout.svelte    # Root layout with nav, polling init
└── app.html

3.2 Key Client Patterns

Server State vs. Client State: All game data (character stats, inventory, active runs) is server state managed via TanStack Query. The client never computes authoritative game values — it only displays them. Client state is limited to UI concerns (which tab is open, tooltip position, theme preference).

Optimistic Updates: For low-risk actions (equipping gear, reordering skill queue), the client optimistically updates the UI and rolls back on server rejection. For economy actions (marketplace buy, gold transfer), the client waits for server confirmation.

Polling Strategy: TanStack Query handles all server state polling. Different data types poll at different intervals based on how time-sensitive they are:

DataPoll IntervalRationale
Active runs/crafts/gathering60sClient shows countdown from completesAt — only needs to poll to detect completion
Notifications (in-app)30sGET /api/characters/:id/notifications — new completions, mail, PVP results
Marketplace listings60sNot urgent — player checks when ready
Party/raid lobby status15sMore time-sensitive when coordinating group content
PVP queue status10sNeeds faster feedback when waiting for a match
Everything elseOn demandFetched when the player navigates to that screen

When a countdown timer reaches zero, the client immediately refetches that resource (rather than waiting for the next poll interval) to show the result as soon as it’s available.

Timer Display: Active runs, crafts, and gathering expeditions show countdown timers. The client calculates display time from startedAt + duration timestamps provided by the server. No client-side simulation of progress — just a countdown to the known completion time. On timer expiry, TanStack Query refetches the resource immediately.

Run Replay Viewer: When a dungeon run completes, the server stores a structured log of every encounter, roll, and outcome. The client renders this as a scrollable timeline with expandable encounter cards. Optional PixiJS layer for animated combat playback.

3.3 Responsive Design

The game targets three breakpoints:

BreakpointWidthLayout
Mobile< 640pxSingle column, bottom tab navigation, stacked panels
Tablet640–1024pxTwo-column with collapsible sidebar
Desktop> 1024pxThree-column with persistent sidebar and detail panel

Touch-first interaction design: all drag-and-drop (inventory, skill queue) uses pointer events with touch support. No hover-dependent interactions — tooltips trigger on tap-and-hold on mobile.


4. Server Architecture

4.1 Service Topology

The server is a Rust workspace (Cargo monorepo) deployed as multiple binary targets from a single codebase. This avoids microservice complexity while allowing independent scaling of compute-heavy workers. All binaries share common crates for game logic, database access, and types.

delve-server/
├── Cargo.toml                  # Workspace root
├── crates/
│   ├── api/                    # REST API server binary
│   │   ├── src/
│   │   │   ├── main.rs         # Axum server entry point
│   │   │   ├── routes/         # Route handlers organized by domain
│   │   │   │   ├── auth.rs
│   │   │   │   ├── characters.rs
│   │   │   │   ├── quests.rs
│   │   │   │   ├── inventory.rs
│   │   │   │   ├── crafting.rs
│   │   │   │   ├── marketplace.rs
│   │   │   │   ├── guilds.rs
│   │   │   │   ├── pvp.rs
│   │   │   │   ├── factions.rs
│   │   │   │   ├── social.rs
│   │   │   │   ├── notifications.rs
│   │   │   │   └── admin.rs
│   │   │   ├── middleware/     # Auth, rate limiting, validation, tracing
│   │   │   └── extractors.rs  # Custom Axum extractors (AuthUser, ValidatedJson, etc.)
│   │   └── Cargo.toml
│   ├── workers/                # Background job processor binary
│   │   ├── src/
│   │   │   ├── main.rs         # Worker entry point — registers job handlers, polls Redis queue
│   │   │   ├── simulation/     # Dungeon run resolver
│   │   │   │   ├── engine.rs   # Core d100 combat engine
│   │   │   │   ├── encounters.rs
│   │   │   │   ├── skill_queue.rs
│   │   │   │   ├── conditions.rs
│   │   │   │   └── loot.rs
│   │   │   ├── economy.rs      # Marketplace matching, auction expiry
│   │   │   ├── crafting.rs     # Craft completion, critical craft rolls
│   │   │   ├── gathering.rs    # Expedition completion, yield calculation
│   │   │   ├── pvp.rs          # Arena match resolution, ELO updates
│   │   │   ├── guild.rs        # Guild XP tallying, buff expiry
│   │   │   └── scheduled.rs    # Daily reset, weekly rotation, season transitions
│   │   └── Cargo.toml
│   ├── game/                   # Core game logic library (shared by api + workers)
│   │   ├── src/
│   │   │   ├── combat.rs       # Damage formulas, hit chance, crit calculation
│   │   │   ├── progression.rs  # XP curves, level thresholds, feat unlocks
│   │   │   ├── economy.rs      # Tax rates, vendor prices, inflation formulas
│   │   │   ├── loot_tables.rs  # Drop rates, rarity weights per content tier
│   │   │   ├── time.rs         # Duration calculations for runs, crafts, gathering
│   │   │   └── rng.rs          # Seeded deterministic RNG (ChaCha8Rng)
│   │   └── Cargo.toml
│   ├── db/                     # Database layer (shared)
│   │   ├── src/
│   │   │   ├── lib.rs          # Connection pool (sqlx::PgPool), migrations
│   │   │   ├── models/         # Row types (FromRow derives)
│   │   │   ├── queries/        # SQLx query functions per domain
│   │   │   └── migrations/     # SQL migration files (sqlx migrate)
│   │   └── Cargo.toml
│   ├── types/                  # Shared types, enums, constants
│   │   ├── src/
│   │   │   ├── character.rs
│   │   │   ├── item.rs
│   │   │   ├── skill.rs
│   │   │   ├── quest.rs
│   │   │   ├── combat.rs
│   │   │   └── ids.rs          # Typed ID wrappers (CharacterId, ItemId, etc.)
│   │   └── Cargo.toml
│   └── jobs/                   # Job queue abstraction (shared)
│       ├── src/
│       │   ├── lib.rs          # Redis-backed delayed job queue
│       │   ├── enqueue.rs      # Enqueue jobs with optional delay
│       │   └── process.rs      # Poll and process jobs
│       └── Cargo.toml
├── data/                       # Static game data (TOML/RON files, compiled into binary)
│   ├── items/                  # Item template definitions
│   ├── quests/                 # Quest and dungeon definitions
│   ├── creatures/              # Enemy stat blocks
│   ├── skills/                 # Skill definitions
│   ├── recipes/                # Crafting recipes
│   └── loot_tables/            # Loot table definitions
└── Dockerfile                  # Multi-stage: build with rust image, run with scratch/alpine

Why a Cargo Workspace

  • Single compilation unit for shared code: The game, db, types, and jobs crates are compiled once and shared by both the api and workers binaries.
  • Compile-time guarantees: SQLx checks queries against the real database schema at compile time. Type mismatches between API routes and database models are caught before deployment.
  • Two binaries, one repo: cargo build --bin api and cargo build --bin workers produce independent binaries that can be deployed and scaled separately.
  • Static game data: Item templates, quests, creatures, and loot tables are defined as TOML or RON files in the data/ directory and loaded at startup (or compiled in via include_str!). Balance changes are code changes — reviewed in PRs, versioned in git.

4.2 Process Types

ProcessScalingRole
delve-apiHorizontal (2+ instances behind load balancer)REST endpoints, request validation, auth, notification polling. Axum binary.
delve-workersHorizontal (scale by queue depth)Polls Redis job queue, dispatches to simulation/economy/pvp/crafting handlers. Single binary handles all job types — scaling is just running more instances.
delve-workers --schedulerSingle instanceSame binary with a flag to also run cron-like triggers: daily resets, weekly rotations, season transitions. Only one instance runs scheduled jobs (Redis-based leader election).

Workers handle all background job types in a single binary. The economy queue is processed serially (single consumer) to prevent race conditions, while simulation/pvp/crafting jobs are processed concurrently across all worker instances.

4.3 Request Flow Example — Start a Dungeon Run

1. Client POST /api/quests/start
   Body: { characterId, questId, skillQueue, loadout, supplies }

2. API Server:
   a. Validate session → get userId
   b. Validate character belongs to user, is not already in a run
   c. Validate skill queue (skills owned, correct order, conditionals valid)
   d. Validate loadout (gear owned, correct slots)
   e. Validate supplies (owned, within slot limits)
   f. Calculate run duration based on quest + Patron status
   g. Deduct supplies from inventory
   h. Create run record in DB (status: "in_progress", completesAt: now + duration)
   i. Enqueue BullMQ delayed job: "resolve-run" with delay = duration
   j. Return { runId, completesAt } to client

3. [Time passes — player may be offline]

4. BullMQ triggers "resolve-run" job at completesAt:
   a. Simulation Worker picks up job
   b. Load run record, character snapshot, quest definition
   c. For each encounter in quest:
      - Roll initiative for all participants
      - Execute skill queues round-by-round (d100 rolls, conditional checks)
      - Apply damage, healing, conditions
      - Check for death/completion
      - If rest point: restore resources per rules
      - Log every action and roll to run_log JSONB
   d. Calculate loot drops from completed encounters
   e. Calculate XP and gold earned
   f. Write results: run status, loot, XP, gold, run_log
   g. Insert notification record: { characterId, type: "run_complete", runId }
   h. If character has push notifications enabled and is offline:
      - Send push notification via FCM/APNs: "Your dungeon run is complete!"

5. Client discovers result via one of:
   - Poll: GET /api/characters/:id/notifications returns the "run_complete" event
   - Timer: client countdown hits zero → immediate refetch of run status
   - Push: mobile notification tapped → app opens to run result screen

5. Database Design

5.1 PostgreSQL Schema (Key Tables)

Data-driven convention: All game content references (species, class, weapon type, rarity, faction, etc.) are stored as TEXT columns containing data IDs validated by the application against the GameData registry. No Postgres ENUM types are used for game content — this means adding new content never requires a database migration.

-- ==================== ACCOUNTS ====================

CREATE TABLE users (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email           TEXT UNIQUE NOT NULL,
    password_hash   TEXT NOT NULL,
    patron_tier     SMALLINT DEFAULT 0, -- 0=free, 1=patron
    patron_expires  TIMESTAMPTZ,
    character_slots SMALLINT DEFAULT 2,
    bank_slots      SMALLINT DEFAULT 50,
    marketplace_slots SMALLINT DEFAULT 10,
    created_at      TIMESTAMPTZ DEFAULT now(),
    last_login      TIMESTAMPTZ
);

CREATE TABLE sessions (
    id         TEXT PRIMARY KEY,
    user_id    UUID REFERENCES users(id) ON DELETE CASCADE,
    expires_at TIMESTAMPTZ NOT NULL
);

-- ==================== CHARACTERS ====================

CREATE TABLE characters (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID REFERENCES users(id) ON DELETE CASCADE,
    name            TEXT UNIQUE NOT NULL,
    species         TEXT NOT NULL,       -- data ID, validated by app against GameData registry
    class           TEXT NOT NULL,       -- data ID, validated by app against GameData registry
    subclass        TEXT,                -- data ID, NULL until level 10
    background      TEXT NOT NULL,
    level           SMALLINT DEFAULT 1,
    xp              INTEGER DEFAULT 0,
    paragon_level   INTEGER DEFAULT 0,  -- post-50 progression
    paragon_xp      BIGINT DEFAULT 0,

    -- Core attributes (base values before gear/buffs)
    might           SMALLINT NOT NULL,
    logic           SMALLINT NOT NULL,
    speed           SMALLINT NOT NULL,
    presence        SMALLINT NOT NULL,
    fortitude       SMALLINT NOT NULL,
    luck            SMALLINT NOT NULL,

    -- Non-combat skills (0-100)
    skill_athletics   SMALLINT DEFAULT 0,
    skill_acrobatics  SMALLINT DEFAULT 0,
    skill_stealth     SMALLINT DEFAULT 0,
    skill_perception  SMALLINT DEFAULT 0,
    skill_arcana      SMALLINT DEFAULT 0,
    skill_nature      SMALLINT DEFAULT 0,
    skill_religion    SMALLINT DEFAULT 0,
    skill_persuasion  SMALLINT DEFAULT 0,
    skill_deception   SMALLINT DEFAULT 0,
    skill_intimidation SMALLINT DEFAULT 0,
    skill_medicine    SMALLINT DEFAULT 0,
    skill_survival    SMALLINT DEFAULT 0,

    -- Currency
    gold            BIGINT DEFAULT 0,

    -- Rested bonus (accumulated offline XP multiplier)
    rested_bonus    REAL DEFAULT 0.0,   -- 0.0 to 0.5
    last_active     TIMESTAMPTZ DEFAULT now(),

    -- Chosen feats (JSONB array of feat IDs)
    feats           JSONB DEFAULT '[]',

    created_at      TIMESTAMPTZ DEFAULT now(),

    CONSTRAINT valid_attributes CHECK (
        might BETWEEN 1 AND 99 AND logic BETWEEN 1 AND 99 AND
        speed BETWEEN 1 AND 99 AND presence BETWEEN 1 AND 99 AND
        fortitude BETWEEN 1 AND 99 AND luck BETWEEN 1 AND 99
    )
);

CREATE INDEX idx_characters_user ON characters(user_id);
CREATE INDEX idx_characters_name ON characters(name);

-- ==================== EQUIPMENT & INVENTORY ====================

CREATE TABLE items (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    owner_id        UUID REFERENCES characters(id) ON DELETE CASCADE,
    template_id     TEXT NOT NULL,       -- data ID, references item template in GameData
    rarity          TEXT NOT NULL,       -- data ID, references rarity tier in GameData
    enhancement     SMALLINT DEFAULT 0,  -- +0 to +5
    bonus_properties JSONB DEFAULT '[]', -- random rolled properties for rare+
    location        TEXT NOT NULL,       -- 'equipped:main_hand', 'backpack', 'bank', 'mail'
    slot_index      SMALLINT,            -- position within location
    created_at      TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX idx_items_owner ON items(owner_id);
CREATE INDEX idx_items_owner_location ON items(owner_id, location);

-- Artifact tracking (server-unique items)
CREATE TABLE artifacts (
    template_id     TEXT PRIMARY KEY,
    held_by         UUID REFERENCES characters(id),
    acquired_at     TIMESTAMPTZ
);

-- ==================== SKILL QUEUE ====================

CREATE TABLE skill_queues (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    name            TEXT DEFAULT 'Default',
    is_active       BOOLEAN DEFAULT false,
    slots           JSONB NOT NULL,      -- ordered array of { skillId, condition? }
    created_at      TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX idx_skill_queues_char ON skill_queues(character_id);

-- ==================== WEAPON PROFICIENCY ====================

CREATE TABLE weapon_proficiencies (
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    weapon_type     TEXT NOT NULL,       -- sword, axe, bow, staff, etc.
    proficiency     SMALLINT DEFAULT 0,  -- 0-100
    PRIMARY KEY (character_id, weapon_type)
);

-- ==================== RUNS & QUESTS ====================

CREATE TABLE runs (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    quest_id        TEXT NOT NULL,        -- references static quest/dungeon definition
    difficulty      TEXT NOT NULL,
    status          TEXT DEFAULT 'in_progress', -- in_progress, completed, failed, abandoned
    skill_queue     JSONB NOT NULL,       -- snapshot of queue at run start
    loadout         JSONB NOT NULL,       -- snapshot of equipped gear at run start
    supplies        JSONB NOT NULL,       -- snapshot of supplies consumed
    party_id        UUID,                 -- NULL for solo, references party for group content
    started_at      TIMESTAMPTZ DEFAULT now(),
    completes_at    TIMESTAMPTZ NOT NULL,
    completed_at    TIMESTAMPTZ,
    run_log         JSONB,               -- full encounter-by-encounter replay log
    rewards         JSONB,               -- { xp, gold, items[], proficiencyGains }
    created_at      TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX idx_runs_character ON runs(character_id);
CREATE INDEX idx_runs_status ON runs(status) WHERE status = 'in_progress';
CREATE INDEX idx_runs_completes ON runs(completes_at) WHERE status = 'in_progress';

-- ==================== CRAFTING & GATHERING ====================

CREATE TABLE crafting_proficiencies (
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    profession      TEXT NOT NULL,        -- blacksmithing, alchemy, etc.
    skill_level     SMALLINT DEFAULT 1,   -- 1-100
    PRIMARY KEY (character_id, profession)
);

CREATE TABLE crafting_jobs (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    recipe_id       TEXT NOT NULL,
    status          TEXT DEFAULT 'in_progress',
    started_at      TIMESTAMPTZ DEFAULT now(),
    completes_at    TIMESTAMPTZ NOT NULL,
    result_item_id  UUID,                 -- set on completion
    is_critical     BOOLEAN
);

CREATE INDEX idx_crafting_jobs_char ON crafting_jobs(character_id);

CREATE TABLE gathering_expeditions (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    profession      TEXT NOT NULL,         -- mining, herbalism, logging, skinning
    zone            TEXT NOT NULL,
    tier            SMALLINT NOT NULL,     -- 1-5
    status          TEXT DEFAULT 'in_progress',
    started_at      TIMESTAMPTZ DEFAULT now(),
    completes_at    TIMESTAMPTZ NOT NULL,
    yields          JSONB                  -- set on completion: [{ materialId, quantity }]
);

CREATE TABLE known_recipes (
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    recipe_id       TEXT NOT NULL,
    learned_at      TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (character_id, recipe_id)
);

-- ==================== MARKETPLACE ====================

CREATE TABLE marketplace_listings (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    seller_id       UUID REFERENCES characters(id) ON DELETE CASCADE,
    item_id         UUID REFERENCES items(id),
    price           BIGINT NOT NULL,
    listing_fee     BIGINT NOT NULL,      -- 5% deducted at listing time
    status          TEXT DEFAULT 'active', -- active, sold, expired, cancelled
    listed_at       TIMESTAMPTZ DEFAULT now(),
    expires_at      TIMESTAMPTZ NOT NULL,  -- listed_at + 48 hours
    sold_to         UUID REFERENCES characters(id),
    sold_at         TIMESTAMPTZ
);

CREATE INDEX idx_marketplace_status ON marketplace_listings(status) WHERE status = 'active';
CREATE INDEX idx_marketplace_expires ON marketplace_listings(expires_at) WHERE status = 'active';
CREATE INDEX idx_marketplace_seller ON marketplace_listings(seller_id);

-- ==================== MAIL ====================

CREATE TABLE mail (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    sender_id       UUID REFERENCES characters(id),
    recipient_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    subject         TEXT,
    body            TEXT,
    gold_amount     BIGINT DEFAULT 0,
    item_ids        UUID[],               -- items attached
    is_read         BOOLEAN DEFAULT false,
    deliverable_at  TIMESTAMPTZ NOT NULL,  -- sent_at + 1 hour
    sent_at         TIMESTAMPTZ DEFAULT now(),
    expires_at      TIMESTAMPTZ            -- auto-delete after 30 days
);

CREATE INDEX idx_mail_recipient ON mail(recipient_id);

-- ==================== GUILDS ====================

CREATE TABLE guilds (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name            TEXT UNIQUE NOT NULL,
    tag             TEXT UNIQUE NOT NULL,  -- 2-4 char tag
    leader_id       UUID REFERENCES characters(id),
    level           SMALLINT DEFAULT 1,
    xp              BIGINT DEFAULT 0,
    bank_gold       BIGINT DEFAULT 0,
    active_buff     TEXT,                  -- current guild buff ID
    buff_expires    TIMESTAMPTZ,
    max_members     SMALLINT DEFAULT 50,   -- scales with guild level, max 200
    created_at      TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE guild_members (
    guild_id        UUID REFERENCES guilds(id) ON DELETE CASCADE,
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    rank            TEXT DEFAULT 'member', -- leader, officer, member, recruit
    joined_at       TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (guild_id, character_id)
);

CREATE INDEX idx_guild_members_char ON guild_members(character_id);

-- ==================== FACTIONS & REPUTATION ====================

CREATE TABLE character_reputation (
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    faction_id      TEXT NOT NULL,         -- iron_compact, shadow_court, etc.
    reputation      INTEGER DEFAULT 0,    -- -3000 to 21000+
    PRIMARY KEY (character_id, faction_id)
);

-- ==================== PVP ====================

CREATE TABLE pvp_ratings (
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    bracket         TEXT NOT NULL,         -- '1v1', '3v3'
    rating          INTEGER DEFAULT 1000,  -- ELO
    season          SMALLINT NOT NULL,
    wins            INTEGER DEFAULT 0,
    losses          INTEGER DEFAULT 0,
    PRIMARY KEY (character_id, bracket, season)
);

CREATE TABLE pvp_matches (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    bracket         TEXT NOT NULL,
    season          SMALLINT NOT NULL,
    team_a          UUID[] NOT NULL,       -- character IDs
    team_b          UUID[] NOT NULL,
    winner          TEXT,                  -- 'a', 'b', 'draw'
    match_log       JSONB,
    rating_changes  JSONB,                 -- { charId: delta }
    resolved_at     TIMESTAMPTZ DEFAULT now()
);

-- ==================== SOCIAL ====================

CREATE TABLE friends (
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    friend_id       UUID REFERENCES characters(id) ON DELETE CASCADE,
    status          TEXT DEFAULT 'pending', -- pending, accepted
    created_at      TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (character_id, friend_id)
);

-- ==================== PARTIES & RAIDS ====================

CREATE TABLE parties (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    leader_id       UUID REFERENCES characters(id),
    type            TEXT NOT NULL,         -- duo, standard, raid
    max_size        SMALLINT NOT NULL,     -- 2, 4, 8
    status          TEXT DEFAULT 'forming', -- forming, ready, in_run, completed
    quest_id        TEXT,
    scheduled_at    TIMESTAMPTZ,           -- for timed lobbies
    created_at      TIMESTAMPTZ DEFAULT now()
);

CREATE TABLE party_members (
    party_id        UUID REFERENCES parties(id) ON DELETE CASCADE,
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    role            TEXT,                  -- tank, healer, dps, support
    ready           BOOLEAN DEFAULT false,
    PRIMARY KEY (party_id, character_id)
);

-- ==================== ACHIEVEMENTS & COLLECTIONS ====================

CREATE TABLE character_achievements (
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    achievement_id  TEXT NOT NULL,
    earned_at       TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (character_id, achievement_id)
);

CREATE TABLE character_bestiary (
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    creature_id     TEXT NOT NULL,
    kills           INTEGER DEFAULT 0,
    first_killed    TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (character_id, creature_id)
);

-- ==================== DAILY/WEEKLY TRACKING ====================

CREATE TABLE daily_tracking (
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    date            DATE NOT NULL DEFAULT CURRENT_DATE,
    first_run_bonus BOOLEAN DEFAULT false,
    bounties_completed SMALLINT DEFAULT 0,
    bounty_chest_claimed BOOLEAN DEFAULT false,
    faction_quests  JSONB DEFAULT '{}',   -- { factionId: count }
    PRIMARY KEY (character_id, date)
);

CREATE TABLE weekly_tracking (
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    week_start      DATE NOT NULL,         -- Monday of the week
    challenge_completed BOOLEAN DEFAULT false,
    raid_tokens_earned INTEGER DEFAULT 0,
    PRIMARY KEY (character_id, week_start)
);

-- ==================== SEASONAL ====================

CREATE TABLE season_progress (
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    season_id       TEXT NOT NULL,
    track           TEXT NOT NULL,         -- 'free' or 'premium'
    tier_reached    SMALLINT DEFAULT 0,
    xp              INTEGER DEFAULT 0,
    PRIMARY KEY (character_id, season_id)
);

5.2 Static Game Data

Item templates, quest definitions, skill data, creature stats, recipes, and loot tables are not stored in PostgreSQL. They are defined as TOML files in the data/ directory, deserialized into Rust structs at startup via serde, and versioned with the codebase. This keeps game balance changes in source control and avoids DB migrations for tuning.

# data/items/iron_longsword.toml
[iron_longsword]
name = "Iron Longsword"
type = "weapon"
subtype = "sword"
slot = "main_hand"
damage = [8, 14]
speed = 1.0
level_req = 1
rarity = "common"
# data/quests/goblin_warren.toml
[goblin_warren]
name = "Goblin Warren"
type = "bounty"
level_range = [1, 5]
base_duration_secs = 1800  # 30 minutes
loot_table = "goblin_bounty_t1"

[[goblin_warren.encounters]]
type = "combat"
enemies = ["goblin_scout", "goblin_scout"]
# ...
#![allow(unused)]
fn main() {
// crates/game/src/data.rs
use once_cell::sync::Lazy;
use std::collections::HashMap;

pub static ITEMS: Lazy<HashMap<String, ItemTemplate>> = Lazy::new(|| {
    load_toml_dir("data/items")
});

pub static QUESTS: Lazy<HashMap<String, QuestDefinition>> = Lazy::new(|| {
    load_toml_dir("data/quests")
});
}

5.3 Redis Data Structures

# Session store
session:{sessionId} → JSON { userId, expiresAt }     TTL: 30 days

# Leaderboards (sorted sets)
leaderboard:pvp:1v1:season:{n} → ZADD score=rating member=charId
leaderboard:pvp:3v3:season:{n} → ZADD score=rating member=charId
leaderboard:achievements → ZADD score=points member=charId
leaderboard:wealth → ZADD score=gold member=charId

# Rate limiting
ratelimit:{userId}:{endpoint} → counter               TTL: window duration

# Active PVP queue
pvp:queue:1v1 → sorted set (score=rating, member=charId)
pvp:queue:3v3 → sorted set (score=rating, member=charId+teamId)

# BullMQ job queues (managed by BullMQ internally)
bull:resolve-run:*
bull:marketplace-buy:*
bull:pvp-resolve:*
bull:resolve-craft:*
bull:resolve-gathering:*

6. Game Systems — Server

6.1 Simulation Engine (Combat Resolution)

The simulation engine is the core of Delve. It runs entirely on the server with no real-time client involvement.

#![allow(unused)]
fn main() {
// crates/workers/src/simulation/engine.rs

use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;

pub struct SimulationContext {
    pub run: Run,
    pub party: Vec<CharacterSnapshot>,  // character stats + gear + queue at run start
    pub quest: QuestDefinition,
    pub rng: ChaCha8Rng,                // deterministic seeded RNG
    pub log: RunLogBuilder,
}

pub fn resolve_run(ctx: &mut SimulationContext) -> RunResult {
    for encounter in &ctx.quest.encounters {
        let result = resolve_encounter(ctx, encounter);
        ctx.log.add_encounter(&result);

        if result.outcome == EncounterOutcome::PartyWipe {
            return RunResult {
                status: RunStatus::Failed,
                log: ctx.log.build(),
                failed_at: Some(encounter.index),
                rewards: None,
            };
        }
    }

    let loot = roll_loot(ctx);
    let xp = calculate_xp(ctx);
    RunResult {
        status: RunStatus::Completed,
        log: ctx.log.build(),
        failed_at: None,
        rewards: Some(RunRewards { loot, xp }),
    }
}
}

Deterministic RNG: Each run is seeded with a ChaCha8Rng initialized from a seed stored in the run record. This means any run can be replayed identically for debugging or dispute resolution. The seed is derived from blake3::hash(run_id + started_at). ChaCha8 is fast and produces identical output across platforms — critical for deterministic simulation.

Encounter Resolution Loop:

  1. Sort all participants by initiative (Speed + weapon speed modifier + d100 roll)
  2. For each round (max 50 rounds per encounter):
    • For each participant in initiative order:
      • Evaluate skill queue: find first skill whose conditions are met and resources available
      • Roll d100 against success chance (derived from attacker stats vs. defender stats)
      • Apply effects: damage, healing, conditions, resource cost
      • Check for death/incapacitation
    • Tick conditions (poison damage, buff/debuff duration)
    • Check encounter end conditions
  3. Log all rolls and outcomes

6.2 Economy Worker

The economy worker handles marketplace transactions with serialized processing to prevent race conditions.

Marketplace Buy Flow:
1. API validates buyer has enough gold
2. API enqueues "marketplace.buy" job (NOT direct DB update)
3. Economy worker (single instance) processes:
   a. BEGIN TRANSACTION
   b. Verify listing still active (SELECT FOR UPDATE)
   c. Verify buyer gold >= price (SELECT FOR UPDATE on buyer character)
   d. Transfer gold: buyer -= price, seller += (price - 10% tax)
   e. Transfer item: update item.owner_id, item.location = 'mail'
   f. Create mail record for seller (gold received notification)
   g. Update listing status = 'sold'
   h. COMMIT
4. Insert notification records for buyer ("item purchased") and seller ("item sold")

6.3 PVP Match Resolution

Arena matches resolve instantly (no wait timer) using the same simulation engine as PvE, but with stat normalization applied.

PVP Flow:
1. Player enters arena queue → added to Redis sorted set by rating
2. Matchmaker (runs every 5 seconds):
   a. Scan queue for viable matches (rating within ±150, expanding over time)
   b. Pop matched players from queue
   c. Enqueue "pvp.resolve" job (immediate, no delay)
3. PVP Worker resolves match:
   a. Snapshot both characters with PVP stat normalization
   b. Run simulation engine (same as PvE but PVP-specific encounter rules)
   c. Calculate ELO changes
   d. Write match result + rating updates
   e. Insert notification records for both players
4. Both players discover result via polling (PVP poll interval: 10s) or push notification

6.4 Scheduled Jobs

JobScheduleDescription
daily-reset00:00 UTCReset daily bounties, first-run bonus, faction quest counts
weekly-resetMonday 00:00 UTCRotate weekly challenge, reset raid token caps
auction-expiryEvery 5 minExpire stale marketplace listings, return items via mail
rested-bonus-tickEvery 1 hourIncrement rested bonus for offline characters
season-transitionManual triggerEnd current season, archive ratings, distribute rewards
guild-buff-expiryEvery 1 minExpire guild buffs past their duration
mail-cleanupDailyDelete read mail older than 30 days
pvp-matchmakerEvery 5 secScan PVP queue and create matches

7. Game Systems — Client

7.1 Skill Queue Builder

The skill queue builder is the most complex client-side UI. Players drag-and-drop skills into an ordered list and configure optional conditions.

┌─────────────────────────────────────────────┐
│ Skill Queue: "Boss Rush Build"        [Save]│
├─────────────────────────────────────────────┤
│ 1. [🗡️ Power Strike]     always             │
│ 2. [🛡️ Shield Wall]      if HP < 50%       │
│ 3. [⚡ Cleave]            if enemies > 2    │
│ 4. [❤️ Second Wind]       if HP < 30%       │
│ 5. [🗡️ Execute]           if target HP < 20%│
│ 6. [🗡️ Basic Attack]      always (fallback) │
├─────────────────────────────────────────────┤
│ Available Skills:  [Drag to add]            │
│ [Whirlwind] [Taunt] [Parry] [Charge] ...   │
└─────────────────────────────────────────────┘

Condition types (predefined, not free-form):

  • always — default, no condition
  • if_hp_below(%) — self HP threshold
  • if_hp_above(%)
  • if_target_hp_below(%)
  • if_enemy_count_above(n)
  • if_enemy_count_below(n)
  • if_ally_hp_below(%) — any ally below threshold
  • if_resource_above(type, n) — e.g., “if Fury > 50”
  • if_resource_below(type, n)
  • if_has_condition(condition) — if affected by specific status
  • if_target_has_condition(condition)

The client validates the queue locally (correct number of slots, skills owned, conditions valid) and sends it to the server for authoritative validation on run start.

7.2 Run Replay Viewer

After a run completes, the client renders the server-generated run_log as a detailed timeline.

┌─────────────────────────────────────────────┐
│ Run Complete: Goblin Warren (Normal)        │
│ Result: ✅ Success  |  Duration: 32 min     │
│ XP: +450  |  Gold: +120  |  Items: 3       │
├─────────────────────────────────────────────┤
│                                             │
│ ▼ Encounter 1/5: Goblin Scouts (Combat)    │
│   Round 1:                                  │
│     You use Power Strike → Hit (rolled 34   │
│     vs 72% chance) → 18 damage to Goblin A  │
│     Goblin A attacks → Miss (rolled 88 vs   │
│     45% chance)                              │
│   Round 2: ...                              │
│   Result: Victory (2 rounds)                │
│                                             │
│ ▶ Encounter 2/5: Trapped Hallway (Trap)    │
│ ▶ Encounter 3/5: Fork in the Road (Choice) │
│ ▶ Encounter 4/5: Goblin Chieftain (Combat) │
│ ▶ Encounter 5/5: Treasure Room (Loot)      │
│                                             │
├─────────────────────────────────────────────┤
│ Loot: [Goblin Blade (Uncommon)] [Health     │
│ Potion x3] [12 Rough Leather]              │
│                                [Collect All]│
└─────────────────────────────────────────────┘

7.3 Activity Dashboard

The main game screen shows all concurrent activities at a glance:

┌─────────────────────────────────────────────┐
│ 🏰 Active Activities                        │
├─────────────────────────────────────────────┤
│ ⚔️ Dungeon Run: Goblin Warren     02:15:33 │
│ 🔨 Crafting: Iron Longsword         00:12:45│
│ ⛏️ Mining: Copper Vein (Tier 1)     01:45:00│
│ 📦 Marketplace: 3 active listings           │
│ 🎯 Arena Queue: Searching...               │
├─────────────────────────────────────────────┤
│ 📬 1 new mail  |  🔔 2 new notifications    │
└─────────────────────────────────────────────┘

8. Polling & Notifications

8.1 Why Polling, Not WebSockets

Delve is an async idle game. The fastest meaningful game event is a PVP match (resolved in seconds), and the slowest is a raid (12 hours). There is no real-time combat, no live player movement, no twitch gameplay. The client needs updates, not a live stream.

WebSockets add significant complexity for negligible benefit in this context:

  • Persistent connection state to manage on the server (memory per connection, reconnect logic, heartbeats)
  • Dedicated WebSocket server processes to scale independently
  • Connection lifecycle management on mobile (background/foreground transitions)
  • Missed-event recovery when connections drop

Polling via TanStack Query is simpler, stateless, and works identically whether the player is on desktop, mobile, or just opened the app after 8 hours offline. The server remains a pure REST API with no connection state.

8.2 Notification System

Workers write notification records to the database when events complete. The client polls for them.

CREATE TABLE notifications (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    character_id    UUID REFERENCES characters(id) ON DELETE CASCADE,
    type            TEXT NOT NULL,       -- run_complete, craft_complete, item_sold, etc.
    title           TEXT NOT NULL,
    body            TEXT,
    data            JSONB,              -- { runId, itemId, etc. } for deep linking
    is_read         BOOLEAN DEFAULT false,
    created_at      TIMESTAMPTZ DEFAULT now()
);

CREATE INDEX idx_notifications_unread
    ON notifications(character_id, created_at DESC)
    WHERE is_read = false;
GET /api/characters/:id/notifications?since={timestamp}
→ Returns unread notifications since last poll

POST /api/characters/:id/notifications/read
Body: { ids: [...] }
→ Marks notifications as read

8.3 Push Notifications (Mobile)

When a worker creates a notification record, it also checks whether to send a push notification. Push is sent when:

  • The player has push enabled for that notification type
  • The player’s last API request was > 5 minutes ago (likely offline/backgrounded)

Push notifications are delivered via FCM (Android) and APNs (iOS) through Capacitor’s push plugin.

Notification events (user-configurable):

  • Run completed
  • Crafting completed
  • Gathering expedition completed
  • Marketplace item sold
  • Raid lobby starting in 15 min / 5 min / now
  • Mail received
  • Guild war declared

Never sent (per design doc anti-burnout philosophy):

  • “Come back!” re-engagement nags
  • Daily streak reminders
  • “Your friends are playing” social pressure
  • Limited-time urgency notifications

8.4 Polling Impact on Server Load

At 10,000 concurrent players polling every 30s, that’s ~333 requests/second to the notification endpoint. This is a trivial load for an API server — it’s a single indexed query (WHERE character_id = $1 AND is_read = false AND created_at > $2). For comparison, a typical Node.js server handles 5,000–10,000 simple requests/second.

At 100,000 concurrent players: ~3,333 req/s. Still manageable with 2-3 API server instances. The notification query hits an index and returns a handful of rows — it’s one of the cheapest possible database operations.


9. Chat — Discord Integration

9.1 Why Discord, Not In-Game Chat

Building a real-time chat system requires WebSockets, persistent connections, message storage, moderation tools, spam filtering, mute/block systems, and mobile notification integration. Discord already does all of this better than we could build, and the Delve community will already be on Discord.

9.2 Integration Approach

Discord is the primary social layer. The game links to it but does not embed it.

In-game integration points:

  • Guild creation flow prompts the guild leader to link a Discord server
  • Guild detail page shows a “Join Discord” button that opens the linked invite
  • Party/raid lobby page shows a “Coordinate on Discord” link
  • LFG (looking for group) happens in a dedicated Discord channel, not in-game
  • Trade chat happens in a Discord channel

Server-side integration (Discord bot, optional):

  • Bot posts to a #notifications channel when server events happen (season starts, maintenance, patch notes)
  • Bot can optionally post run results or achievements to a guild’s Discord channel (if guild links their server and enables it)
  • Account linking: players can link their Discord account via OAuth2 for bot features

9.3 What This Eliminates

Removed ComponentComplexity Saved
WebSocket server processNo persistent connections, no connection state management
Chat message storageNo chat tables, no message history, no Redis pub/sub
Chat moderationNo profanity filter, no mute/ban system, no reporting UI
Presence systemNo online/offline tracking, no heartbeats
NATS message busWorkers write to DB directly, no pub/sub needed
Chat UI componentsNo message input, no channel switching, no emoji picker

This reduces the server to a pure stateless REST API, which is dramatically simpler to build, deploy, debug, and scale.


10. Mobile Wrapping

10.1 Capacitor Configuration

Capacitor wraps the SvelteKit SPA as a native iOS and Android app.

Native plugins used:
├── @capacitor/push-notifications  — FCM/APNs integration
├── @capacitor/haptics             — tactile feedback for loot drops, crits
├── @capacitor/app                 — app state (foreground/background detection)
├── @capacitor/status-bar          — immersive game UI
├── @capacitor/splash-screen       — branded loading screen
├── @capacitor/browser             — external links (terms, support, Discord)
└── @capacitor/preferences         — local key-value storage (settings, cached auth)

10.2 Mobile-Specific Adaptations

ConcernImplementation
App lifecycleDetect foreground → resume polling + refetch stale queries. Background → pause polling (push notifications still arrive).
OfflineShow cached data via TanStack Query persistence. Queue-able actions (queue edits) stored locally, synced on reconnect.
Deep linksdelve://character/{id}, delve://guild/{id}, delve://quest/{id} for sharing
App Store complianceAll purchases routed through Stripe web checkout (linked from app). No in-app purchase SDK to avoid 30% platform cut. Patron status synced via server.
PerformanceLazy-load routes. Limit PixiJS animations on low-end devices (detect via navigator.deviceMemory).
Safe areasCSS env(safe-area-inset-*) for notch/dynamic island handling
Splash + iconsGenerated via @capacitor/assets from a single source SVG

10.3 Build Pipeline

Web Build:
  pnpm build → SvelteKit static adapter → dist/

iOS:
  npx cap sync ios → Xcode project updated
  xcodebuild → .ipa
  Distribute via App Store Connect (or TestFlight for beta)

Android:
  npx cap sync android → Gradle project updated
  ./gradlew assembleRelease → .aab
  Distribute via Google Play Console (or internal testing track)

CI/CD:
  GitHub Actions:
    - On push to main: build web, run tests, deploy to staging
    - On tag (v*): build web + iOS + Android, deploy to production
    - iOS builds via Xcode Cloud or self-hosted Mac runner
    - Android builds via standard Linux runner

11. Infrastructure & DevOps

11.1 Deployment Architecture

Production Environment:

┌─────────────────────────────────────────────┐
│         CDN (Cloudflare Pages)              │
│         Static SPA assets                   │
└─────────────────────────────────────────────┘

┌─────────────────────────────────────────────┐
│            Caddy (Reverse Proxy)            │
│         TLS termination, /api → API         │
└──────────────────┬──────────────────────────┘
                   │
          ┌────────┴────────┐
          │   API Servers   │
          │   (2 replicas)  │
          └────────┬────────┘
                   │
     ┌─────────────┼─────────────┐
     ▼             ▼             ▼
┌─────────┐  ┌──────────┐  ┌──────────────┐
│Simulation│  │ Economy  │  │ PVP/Crafting │
│Workers(N)│  │Worker(1) │  │  Workers     │
└─────────┘  └──────────┘  └──────────────┘
     │             │             │
     └─────────────┼─────────────┘
                   │
          ┌────────┼────────┐
          ▼        ▼        ▼
     PostgreSQL  Redis   Backblaze B2

11.2 Environment Strategy

EnvironmentPurposeInfrastructure
LocalDevelopmentDocker Compose (all services)
StagingPre-production testingSingle node, real DB, seeded test data
ProductionLive gameMulti-node, replicated DB, Redis cluster

11.3 Database Operations

  • Migrations: Drizzle-kit generates SQL migration files, applied via CI before deployment.
  • Backups: PostgreSQL WAL archiving to S3 for point-in-time recovery. Daily full backups retained 30 days.
  • Connection pooling: PgBouncer in transaction mode between API/workers and PostgreSQL.

11.4 Monitoring & Alerting

MetricAlert Threshold
API response time p95> 500ms
Simulation worker queue depth> 1000 pending jobs
Error rate> 1% of requests
Database connections> 80% pool utilization
Redis memory> 80% capacity
Disk space> 85% utilization
Notification poll latency p95> 100ms

12. Security

12.1 Authentication & Authorization

  • Password hashing: Argon2id with recommended parameters
  • Sessions: Opaque session tokens stored in Redis, 30-day expiry, refreshed on use
  • CSRF: SameSite=Strict cookies + origin header validation
  • OAuth2: Optional social login (Google, Discord) via standard OAuth2 flow
  • Authorization: Every API call validates that the authenticated user owns the character being acted upon. No cross-user access.

12.2 Anti-Cheat

Because the game is server-authoritative, traditional client-side cheats are not possible. The main attack vectors are:

VectorMitigation
API manipulationAll inputs validated server-side. Skill queue validated against owned skills. Gear validated against owned items. Gold amounts verified in transactions.
Race conditionsEconomy worker serialized. SELECT FOR UPDATE on critical resources. Idempotency keys on purchase/trade endpoints.
Automation / bottingRate limiting on all endpoints. CAPTCHA on account creation. Behavioral analysis on marketplace patterns (future).
Multi-accountingSingle email per account. IP-based rate limiting on account creation. Marketplace patterns flagged for review.
Time manipulationAll timers are server-side. completesAt timestamps are authoritative. Client countdown is display-only.

12.3 Data Protection

  • Encryption at rest: PostgreSQL with disk encryption. Redis with TLS.
  • Encryption in transit: TLS everywhere (HTTPS, internal service communication).
  • PII minimization: Only email stored. No real names, addresses, or payment details (payments handled by Stripe).
  • GDPR/CCPA: Account deletion endpoint that cascades to all character data. Data export endpoint for portability.
  • Input sanitization: All user-provided strings (character names, guild names, mail messages) sanitized against XSS.

13. Performance Targets

13.1 Client

MetricTarget
Initial load (LCP)< 2s on 4G
Bundle size (gzipped)< 150KB initial, < 500KB total with lazy routes
Time to interactive< 3s on mid-range mobile
Frame rate (UI transitions)60fps
Memory usage< 100MB on mobile

13.2 Server

MetricTarget
API response time p50< 50ms
API response time p95< 200ms
Notification poll response< 30ms (indexed query, tiny result set)
Simulation throughput100+ concurrent run resolutions per worker
Marketplace transaction throughput1000+ transactions/minute
Target concurrent players (initial)10,000
Target concurrent players (scaled)100,000+

13.3 Scalability Path

Phase 1 (Launch): Single PostgreSQL instance, 2 API servers, 2 simulation workers, 1 economy worker. Supports ~10K concurrent players.

Phase 2 (Growth): Read replicas for PostgreSQL, Redis cluster, 4+ simulation workers, horizontal API scaling. Supports ~50K concurrent players.

Phase 3 (Scale): Database sharding by character ID range (if needed), dedicated PVP cluster, CDN for all static game data, regional API deployments for latency. Supports 100K+ concurrent players.

Without WebSocket servers, scaling is much simpler — every API server is stateless and interchangeable. No sticky sessions, no connection affinity, no need to coordinate which server holds which player’s connection.


Appendix A: Data-Driven Design

All game content (species, classes, items, skills, conditions, factions, etc.) is defined in TOML data files and referenced by string ID. See features/data-driven-architecture.md for the full design. Only engine-structural concepts are Rust enums.

Game Data Registry

#![allow(unused)]
fn main() {
// crates/game/src/registry.rs
// Loaded once at startup, shared via Axum state. All game content lives here.

pub struct GameData {
    pub species: HashMap<String, SpeciesDef>,
    pub classes: HashMap<String, ClassDef>,
    pub backgrounds: HashMap<String, BackgroundDef>,
    pub weapon_types: HashMap<String, WeaponTypeDef>,
    pub equipment_slots: IndexMap<String, EquipmentSlotDef>, // ordered
    pub rarities: Vec<RarityDef>,                            // ordered by tier
    pub resources: HashMap<String, ResourceDef>,
    pub conditions: HashMap<String, ConditionDef>,
    pub skills: HashMap<String, SkillDef>,
    pub item_templates: HashMap<String, ItemTemplateDef>,
    pub creatures: HashMap<String, CreatureDef>,
    pub quests: HashMap<String, QuestDef>,
    pub factions: HashMap<String, FactionDef>,
    pub reputation_tiers: Vec<ReputationTierDef>,
    pub loot_tables: HashMap<String, LootTableDef>,
    pub recipes: HashMap<String, RecipeDef>,
    pub achievements: HashMap<String, AchievementDef>,
}

impl GameData {
    pub fn load(data_dir: &Path) -> Result<Self, GameDataError>;
    pub fn validate(&self) -> Result<(), GameDataError>; // cross-reference check
}
}

Data-Driven IDs (game content — stored as TEXT in Postgres)

#![allow(unused)]
fn main() {
// crates/types/src/ids.rs
// Validated newtype wrappers over String. The inner value is a key into GameData.

macro_rules! data_id {
    ($name:ident) => {
        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
        #[sqlx(transparent)]
        pub struct $name(String);

        impl $name {
            pub fn as_str(&self) -> &str { &self.0 }
            /// Unchecked construction — only use when loading from DB (already validated)
            pub fn from_db(s: String) -> Self { Self(s) }
        }
    };
}

data_id!(SpeciesId);
data_id!(ClassId);
data_id!(SubclassId);
data_id!(BackgroundId);
data_id!(WeaponTypeId);
data_id!(SlotId);
data_id!(RarityId);
data_id!(ResourceId);
data_id!(ConditionId);
data_id!(SkillId);
data_id!(ItemTemplateId);
data_id!(CreatureId);
data_id!(QuestId);
data_id!(FactionId);
data_id!(RecipeId);
data_id!(AchievementId);
data_id!(LootTableId);
}

Character (uses data IDs, not enums)

#![allow(unused)]
fn main() {
// crates/types/src/character.rs

#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Character {
    pub id: Uuid,
    pub user_id: Uuid,
    pub name: String,
    pub species: SpeciesId,      // TEXT column — e.g., "ironborn"
    pub class: ClassId,          // TEXT column — e.g., "vanguard"
    pub subclass: Option<SubclassId>,
    pub background: BackgroundId,
    pub level: i16,
    pub xp: i32,
    pub gold: i64,
    pub might: i16,
    pub logic: i16,
    pub speed: i16,
    pub presence: i16,
    pub fortitude: i16,
    pub luck: i16,
}
}

Engine-Structural Enums (code, not data)

#![allow(unused)]
fn main() {
// These remain as Rust enums — they control code branching, not content.

/// Where an item is stored — each variant has different rules and UI.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ItemLocation {
    Equipped { slot: String },  // slot is a data ID (e.g., "main_hand")
    Backpack { index: u16 },
    Bank { index: u16 },
    Mail { mail_id: Uuid },
    Listed { listing_id: Uuid },
}

/// Run state machine — finite set of states with enforced transitions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RunStatus { InProgress, Completed, Failed }

/// Encounter dispatch — each variant calls a different resolver function.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EncounterType { Combat, Trap, Decision, Hazard, Rest, Puzzle }

/// Queue condition evaluation — each variant has different evaluation logic.
/// The parameters (resource ID, condition ID) are data references.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum QueueCondition {
    Always,
    IfHpBelow { threshold: u8 },
    IfHpAbove { threshold: u8 },
    IfTargetHpBelow { threshold: u8 },
    IfEnemyCountAbove { count: u8 },
    IfEnemyCountBelow { count: u8 },
    IfAllyHpBelow { threshold: u8 },
    IfResourceAbove { resource: String, amount: u16 },  // resource is a data ID
    IfResourceBelow { resource: String, amount: u16 },
    IfHasCondition { condition: String },                // condition is a data ID
    IfTargetHasCondition { condition: String },
}
}

Client/Server Type Sharing

Since the backend is Rust and the frontend is TypeScript, types are not shared at the language level. The API contract is the source of truth:

  • OpenAPI spec: The API server exports an OpenAPI schema (via utoipa crate), and the client generates TS types from it (via openapi-typescript).
  • Or: A types.ts file in the client is manually maintained to match the API.
  • Game data: The client fetches data definitions (species list, class list, rarity colors, etc.) via GET /api/game-data which returns the full registry for UI rendering.

Appendix B: API Route Summary

MethodPathDescription
Auth
POST/api/auth/registerCreate account
POST/api/auth/loginLogin, returns session
POST/api/auth/logoutDestroy session
Characters
GET/api/charactersList user’s characters
POST/api/charactersCreate character
GET/api/characters/:idGet character details
DELETE/api/characters/:idDelete character
PUT/api/characters/:id/skill-queueSave skill queue
GET/api/characters/:id/skill-queuesList saved queues
Equipment
GET/api/characters/:id/inventoryGet backpack + equipped
POST/api/characters/:id/equipEquip item
POST/api/characters/:id/unequipUnequip item
POST/api/characters/:id/salvageSalvage items
Quests & Runs
GET/api/quest-boardGet available quests for character
POST/api/runsStart a dungeon run
GET/api/runs/:idGet run status + results
GET/api/characters/:id/runsRun history
Crafting
GET/api/characters/:id/recipesKnown recipes
POST/api/crafting/startStart crafting job
GET/api/characters/:id/craftingActive crafting jobs
Gathering
POST/api/gathering/startStart expedition
GET/api/characters/:id/gatheringActive expeditions
Marketplace
GET/api/marketplaceSearch listings
POST/api/marketplace/listCreate listing
POST/api/marketplace/buy/:idBuy listing
DELETE/api/marketplace/:idCancel listing
Mail
GET/api/characters/:id/mailGet mailbox
POST/api/mail/sendSend mail
POST/api/mail/:id/collectCollect attachments
Guilds
POST/api/guildsCreate guild
GET/api/guilds/:idGuild details
POST/api/guilds/:id/joinRequest to join
POST/api/guilds/:id/inviteInvite player
PUT/api/guilds/:id/settingsUpdate guild settings
POST/api/guilds/:id/buffActivate guild buff
PVP
POST/api/pvp/queueEnter arena queue
DELETE/api/pvp/queueLeave arena queue
GET/api/pvp/historyMatch history
GET/api/pvp/leaderboard/:bracketLeaderboard
Factions
GET/api/characters/:id/reputationAll faction standings
GET/api/factions/:id/vendorFaction vendor inventory
Social
GET/api/characters/:id/friendsFriends list
POST/api/friends/addSend friend request
POST/api/friends/accept/:idAccept request
Parties
POST/api/partiesCreate party
POST/api/parties/:id/joinJoin party
POST/api/parties/:id/readyMark ready
GET/api/parties/:idParty details
Seasons & Dailies
GET/api/characters/:id/dailiesDaily progress
GET/api/characters/:id/weekliesWeekly progress
GET/api/seasons/currentCurrent season info
GET/api/characters/:id/season-progressSeason track progress
Notifications
GET/api/characters/:id/notificationsUnread notifications (polled)
POST/api/characters/:id/notifications/readMark notifications as read
Achievements
GET/api/characters/:id/achievementsEarned achievements
GET/api/characters/:id/bestiaryBestiary progress