Skip to content

Cloudflare Architecture Direction

Core Principle

The backend should be authoritative and timer-driven. Do not build a constant live-simulation server unless the design changes away from async play.

  • Pages + SvelteKit: frontend hosting via @sveltejs/adapter-cloudflare. SSR and API routes run as Pages Functions backed by Workers.
  • Workers: API routing, request validation, auth glue, settlement tick processing.
  • D1: persistent relational game data.
  • Durable Objects: serialized coordination for shared state.
  • KV: cached read models and public summaries.
  • R2: SVG assets, sound, and static content.
  • Queues or alarms: deferred settlement, expirations, and background work.

Durable Object Candidates

  • player session coordinator
  • group expedition instance
  • market or auction house per region
  • guild treasury or guild state coordinator
  • town governance controller

The guiding rule is to create one coordinator per contention boundary instead of one giant global object.

Cost-Safe Simulation Model

Store the following server-side:

  • action type
  • actor set
  • start time
  • end time estimate
  • seed or deterministic inputs
  • risk modifiers
  • supply usage rules

When the player checks in or the action settles, compute the outcome from authoritative state. This avoids expensive per-second simulation.

Data Domains

Likely relational domains include:

  • accounts and characters
  • inventories and item instances
  • recipes and professions
  • markets and orders
  • contracts and escrow
  • regions, areas, and sites
  • codex knowledge
  • guilds and towns
  • audit logs

Rejected Direction

Do not start with real-time open-world combat, large shared map combat loops, or fully synchronous party action. Those drive complexity and cost in exactly the wrong direction for the product goals.


Durable Object Ownership Map

Concrete assignment of which game entities own a DO:

DO Owns One per
TownSettlementDO Town reserve state, daily tick, board generation, event spawning, price index, public works progress Town
MarketExchangeDO Order book, fill matching, auction timing, anti-sniping logic, escrow locking Exchange region (Trevalkaan = 1)
GuildDO Treasury, storage, member permissions, departure slot coordination, influence ledger writes Guild
ExpeditionGroupDO Multi-character expedition assembly, departure slot resolution, group outcome Active group expedition instance
TownGovernanceDO Tax policy queue, control window scoring, public works voting Town (separate from settlement DO to prevent lock contention)

No Character DO. Character state lives in D1. A character record is read-heavy (skills, inventory, codex) and rarely has concurrent writers. D1 prepared statements with optimistic concurrency (updated_at version check) are sufficient. The only contention on character state is during expedition resolution — that write goes through the Expedition DO which then commits to D1.

DO Lifecycle

  • TownSettlementDO — created once per town at world init. Never destroyed. Sets its first alarm immediately on creation.
  • MarketExchangeDO — created once per exchange. Never destroyed.
  • GuildDO — created on guild founding. Destroyed when guild is disbanded (after a 7-day grace period and no members).
  • ExpeditionGroupDO — created when a group departure slot fires or when a multi-party expedition is assembled. Destroyed after resolution is committed to D1.
  • TownGovernanceDO — created per town. Wakes on governance actions and on control window open/close.

Client Notification Architecture

Phase 1: Focus-Triggered Polling

The client polls for updates on tab focus and on a 5-minute background interval. No persistent connection.

Client tab gains focus
  → GET /api/expeditions?status=resolved&since={last_check}
  → GET /api/notifications?since={last_check}
  → Update message log and badge counts
  → Store new last_check timestamp in localStorage

Worker serves these from KV snapshots — no D1 read per poll. KV keys:

resolved_expeditions:{character_id}   → list of expedition ids resolved since T-48h
character_notifications:{character_id} → recent notification objects; 48h TTL

Town DO and Expedition DO write to these KV keys on resolution. Worker reads from KV on poll. D1 is not hit per poll.

Why not SSE or WebSocket? Phase 1 is async; a player checking back after 4 hours gains nothing from a live connection. SSE adds a persistent Worker connection cost. Deferred to Phase 2 if live notification lag becomes a problem.

Phase 2 Path (deferred)

Cloudflare supports Durable Objects as WebSocket hibernation endpoints. Migration path:

  1. Add CharacterNotificationDO per character
  2. Client connects via WebSocket to /api/ws/{character_id}
  3. DO hibernates when no connection; wakes on message push from Town/Expedition DOs
  4. Push resolution events as they happen

Migration is backward-compatible: polling fallback remains for clients that fail WebSocket setup.


Worker Layout

One Worker (aelghar-api) handles all API routes. SvelteKit Pages Functions handle SSR. Separation:

aelghar-api (Workers)
├── /api/auth/*          → auth handlers
├── /api/characters/*    → D1 reads/writes
├── /api/board/*         → KV read + DO write on accept
├── /api/expeditions/*   → D1 reads + Expedition DO coordination
├── /api/market/*        → Market Exchange DO
├── /api/towns/*         → KV read (town snapshots)
├── /api/guild/*         → Guild DO
└── /api/admin/*         → admin-only, secret-gated

aelghar (Pages / SvelteKit)
├── SSR routes → calls aelghar-api internally via service binding
└── Static assets → served by Pages CDN

Internal calls from SvelteKit load() to the Worker use a SERVICE_BINDING (zero-latency intra-Cloudflare call), not an HTTP round-trip.


Bindings Reference

All bindings declared in wrangler.toml:

Binding name Type Purpose
DB D1 Primary relational DB
KV KV Namespace Cached read models, refresh tokens
R2 R2 Bucket SVG assets, sound
TOWN_DO Durable Object TownSettlementDO class
MARKET_DO Durable Object MarketExchangeDO class
GUILD_DO Durable Object GuildDO class
EXPEDITION_DO Durable Object ExpeditionGroupDO class
GOVERNANCE_DO Durable Object TownGovernanceDO class
JWT_SECRET Secret HMAC-SHA256 signing key for JWTs
ADMIN_SECRET Secret Admin endpoint guard