Skip to content

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.character for downstream load() 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:

GET /api/expeditions?status=resolved&since={last_check_timestamp}

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.