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.
Recommended Service Split¶
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.Queuesor 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:
- Add
CharacterNotificationDOper character - Client connects via WebSocket to
/api/ws/{character_id} - DO hibernates when no connection; wakes on message push from Town/Expedition DOs
- 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 |