Client Routes¶
Framework¶
SvelteKit with @sveltejs/adapter-cloudflare. All routes are server-rendered by default; interactive components hydrate client-side. Auth gating in src/hooks.server.ts.
Auth Gate¶
hooks.server.ts handle() intercepts every request:
- Public routes:
/,/login,/register— pass through - All other routes: check for valid access token in
locals.session(set from cookie or header) - If no valid session: redirect to
/login?redirect=<requested_path> - If session valid but no character: redirect to
/character/create - Set
locals.characterfor downstreamload()functions
Token refresh happens transparently: if access token is expired but refresh cookie is present, handle() calls the refresh endpoint server-side and sets the new token before passing the request.
Route Tree¶
/
├── (public)
│ ├── / Landing page (logged-out) or board redirect (logged-in)
│ ├── /login Login form
│ └── /register Registration form
│
├── (app) [auth-gated]
│ ├── /character
│ │ ├── /create Character creation flow (multi-step; only if no character yet)
│ │ ├── / Character sheet (overview)
│ │ ├── /skills Skill list with bands, XP, proximity to next band
│ │ ├── /inventory Inventory grid
│ │ └── /codex Codex entries browser
│ │
│ ├── /board Redirect to /board/[character.location_id]
│ ├── /board/[locationId] Board for a specific location
│ │
│ ├── /expedition/[id] Expedition status detail
│ │
│ ├── /market Redirect to /market/browse (Trevalkaan exchange)
│ ├── /market/browse Browse sell orders
│ ├── /market/buy-orders Browse and post buy orders
│ ├── /market/auctions Active auctions
│ ├── /market/my-listings Own orders and pending coin
│ └── /market/warehouse Warehouse holds
│
│ ├── /guild Guild overview (or "join / found" landing if not in guild)
│ ├── /guild/board Guild board + departure slots
│ ├── /guild/roster Member list + contribution scores
│ ├── /guild/influence 14-day influence panel
│ ├── /guild/storage Guild storage chest
│ └── /guild/treasury Treasury ledger (officers+ only; 403 redirect otherwise)
│
│ ├── /clan Clan overview (or create landing if no clan)
│ ├── /clan/board Clan departure slots
│ └── /clan/roster Clan member list
│
│ ├── /map World map (hex grid, fog of war)
│ ├── /location/[id] Location hub — services, NPCs, board shortcut
│ │
│ ├── /messages Inbox (default tab)
│ ├── /messages/sent Sent folder
│ ├── /messages/compose Compose form (recipient search by display name)
│ ├── /messages/:id Read view
│ │
│ ├── /leaderboard Leaderboard (weekly/monthly/all-time + category tabs)
│ │
│ ├── /settings Account settings (display name, password change)
│ └── /settings/danger Danger zone (delete character — requires confirmation)
│
└── (admin) [auth + admin flag]
└── /admin Admin panel (restricted; server-checks admin flag in D1)
Route Specifications¶
/ (Landing / Board redirect)¶
- Logged out: marketing landing with login and register links. Static SSG page.
- Logged in with character: server redirect 302 →
/board - Logged in without character: server redirect 302 →
/character/create
/login¶
Form: email + password fields. POST to /api/auth/login. On success: store access token in memory, navigate to /board or ?redirect target. On fail: inline error under password field.
No "remember me" toggle — refresh cookie always set at 7-day TTL.
/register¶
Form: email, password, confirm password, display name. POST to /api/auth/register. On success: navigate to /character/create. On fail: inline field errors.
/character/create¶
Multi-step wizard. Steps match character-origins.md:
| Step | Content |
|---|---|
| 1 | Name + appearance |
| 2 | Lineage selection |
| 3 | Village origin |
| 4 | Archetype |
| 5 | Background |
| 6 | Review + confirm |
State persisted in sessionStorage between steps. POST on step 6 only — no partial-save API. If browser closes mid-flow, player starts over from step 1. Acceptable for Phase 1.
/board/[locationId]¶
load(): fetch GET /api/board/{locationId}. Board data from KV snapshot if available.
UI: filter bar (guild tabs, rank filter), board card list, card expand on click. If character not at location: read-only preview banner. Board refreshes on visibilitychange (tab focus).
/expedition/[id]¶
load(): fetch GET /api/expeditions/{id}. Polls every 60 seconds while status is active (client-side interval). On resolution: plays resolution animation, shows result card.
/market/browse¶
load(): fetch GET /api/market/browse?town_id={trevalkaan}. Shortage bands injected from town_snapshot in KV. Category filter tree, search bar, sort selector. Price sparkline loaded on item expand via GET /api/market/price-index.
/map¶
SVG hex grid rendered client-side. Initial tile data loaded server-side in load(). Fog of war state from character codex entries. Pan and zoom via CSS transforms + pointer events. Long-press → bottom sheet with hex info and travel options.
Client-Side Notification Model¶
Problem¶
Async game: expedition resolves server-side while the browser is closed or idle. Client needs to surface results without SSE or WebSocket.
Solution: Focus-Triggered Poll¶
On every visibilitychange event (tab becomes visible), the client calls:
Server returns expeditions resolved since the last check. Client stores last_check_timestamp in localStorage and updates on each poll.
If any resolved expeditions are found:
1. Append result notifications to the message log (persistent list at /messages)
2. Show a non-modal notification badge on the expedition nav item
3. If the player is on /board, auto-refresh the board
Interval poll while tab is visible: every 5 minutes as a background interval. Covers cases where the player leaves the tab open and idle.
No SSE or WebSocket at Phase 1. Added in Phase 2 if the polling lag becomes a product problem.
Message Log¶
Route: /messages — persistent nav item.
Inbox reads from server on each visit (D1 via Worker). Unread badge driven by inbox_meta KV in focus-poll response. Personal expedition results are reconstructed from resolved expedition records in the notification response.
Leaderboard Route¶
/leaderboard — tab bar: Weekly | Monthly | All-Time. Category selector: Expeditions | Coin Earned | Crafted | Influence | Elites | Boss Kills. Data from GET /api/leaderboard. Caller's row highlighted; own rank from GET /api/leaderboard/me pinned at bottom if outside top 100. Shows computed_at as "Last updated X hours ago".
Persistent Shell Layout¶
┌─────────────────────────────────────────────────────────┐
│ [Logo] [Location: Trevalkaan] [Coin: 840] [User▾] │ ← top bar (sticky)
├─────────────────────────────────────────────────────────┤
│ │
│ [Page content] │
│ │
├─────────────────────────────────────────────────────────┤
│ [Board] [Map] [Character] [Market] [Guild] │ ← bottom nav (mobile)
└─────────────────────────────────────────────────────────┘
Top bar: sticky, always visible. Location name links to /location/[id]. Coin is live from locals.character. User dropdown: character sheet link, settings, logout.
Bottom nav (mobile only, < 640px): 5 tabs. Desktop uses sidebar nav rail (left, 56px).
Active expedition count badge on the "Board" tab when any expedition is active. Unread message badge on Messages icon from inbox_meta KV value surfaced by focus-poll.
Error States¶
| Condition | Client behavior |
|---|---|
| 401 token expired | Transparent refresh attempt; if fails, redirect to /login |
| 503 town_degraded | Amber banner: "Town data temporarily unavailable. Last known state shown." Non-blocking. |
| Network error | Persistent amber banner: "Connection issue. Data may be out of date." Retry button. |
| 404 | Inline "Not found" message; back button. No full error page. |
| 500 | Inline "Something went wrong" with a reload button. Log error to console. |
| Board order stale (accepted by someone else) | 409 on accept → inline: "This order was just filled. Refreshing board." Auto-reload. |
No full-page error screens. All errors surface inline or as non-blocking banners so the player retains context.