Skip to content

Architecture

Hails edited this page Jun 18, 2026 · 2 revisions

Architecture

hailsDotGO is a single Go binary serving server-rendered HTML pages that are hydrated by TypeScript bundles. The backend uses the chi router with MySQL for persistence; game data from external sources lives in an in-memory store that background goroutines refresh on a schedule. This page is the big-picture map for contributors: how a request flows through the stack, how templates and assets are served, and where everything lives in the repository.

Stack at a glance

Layer Technology
HTTP server Go net/http with chi v5
Persistence MySQL (users, sessions, raids, trust, store, translations)
Templates Go html/template, one base layout plus one file per page
Frontend TypeScript compiled with esbuild into static/js/ bundles
CSRF gorilla/csrf
Passwords bcrypt (cost 13)
Game data In-memory pogodata.Store, refreshed in the background (see Data Sources)

The app is designed as a single instance: raid matchmaking is serialized with an in-process mutex (raidMu in internal/handlers), and caches (sprites, weather, geocoding, game data) are process-local.

Repository layout

Path Contents
main.go Entry point: env vars, DB open, locale registration, CSRF key, graceful shutdown
internal/auth/ Session create/lookup/delete, the User struct, role predicates (IsMod, IsAdmin, IsSuperAdmin, IsTester, IsTranslator, HasAPIAccess)
internal/db/ MySQL connection setup
internal/handlers/ All HTTP handlers: pages, JSON APIs, admin tools, raid lobbies, trust, awards, store, PayPal, weather, sprites, translations
internal/i18n/ Embedded locale JSON plus runtime override loading (see Localization)
internal/pogodata/ The game data store, external fetchers, disk cache, embedded fallback JSON, event page scraper
internal/server/ Router wiring (server.go), security headers, the per-IP bandwidth limiter (bwlimit.go)
templates/ base.html layout plus one template per page (raids.html, admin.html, ...)
ts/ TypeScript sources, bundled by esbuild into static/js/
static/ CSS, compiled JS, bundled sprite assets
schema.sql Complete current schema for fresh installs (reflects all migrations; run this once on a new database)
migrate.sql Numbered migration sections for upgrading an existing install to the current schema
migrate_tags.sql Standalone convenience script creating the tags and user_tags tables (useful for operators applying only that change)

Request flow

  1. Router: server.New builds a chi router with middleware.Logger, middleware.Recoverer, middleware.Compress(5), and a securityHeaders wrapper applied globally.
  2. CSRF group: every route except POST /api/store/webhook (PayPal server-to-server, verified by webhook signature instead) and the /static/* file server lives inside a csrf.Protect group. The CSRF cookie is Secure with SameSite=Strict; failures are logged with a diagnostic handler and answered with 403.
  3. Auth middleware: routes are wrapped per-route with the guard they need (table below). The session is read from the hgo_session cookie and looked up in the sessions table joined to users; disabled accounts resolve to "not logged in".
  4. Handler: page handlers gather data and call h.render; API handlers write JSON.

Middleware reference

Wrapper Requirement Failure behavior
RequireAuth Logged in Redirect to /login?next=...
RequireAuthAPI Logged in 401 JSON
RequireMod Moderator, admin, or superadmin 403
RequireAdmin Admin or superadmin 403
RequireSuperAdmin The SUPERADMIN_USER account only 403
RequireTranslator Translator flag or superadmin 403
RequireAPIAccess Superadmin, or admin with the api_access flag 403 JSON

See Accounts and Roles for what each role means.

Rendering: h.render()

Templates are parsed once at startup into a map of page name to template set (each set is base.html plus the page file). Per request, render:

  1. Checks the page's maintenance toggle in site_settings; if disabled it serves the maintenance template with HTTP 503 instead (see Admin Guide).
  2. Resolves the current user and bumps users.last_seen_at (this drives the "online" dot in the Trainer Directory).
  3. Detects the language: the user's saved preference, else the lang cookie, else en. Translators previewing a locale get their own pending edits overlaid (see Translator Workspace).
  4. Clones the template set and injects a per-request T translation func via Funcs. The clone is what makes per-request language injection safe under concurrency.
  5. Builds PageData and executes the base template.

PageData is the root data for every page:

Field Meaning
User *auth.User, nil when not logged in
Data Page-specific payload (any type)
CSRFToken Token rendered into the page; the frontend echoes it back in the X-CSRF-Token header
StoreEnabled Whether the Store is switched on
Maintenance Per-page enable flags
Lang Active locale code
Langs Locales shown in the public language switcher
AssetVersion Cache-busting hash (below)

Asset cache busting

computeAssetVersion (in internal/handlers/handlers.go) hashes the contents of every file under static/css and static/js with SHA-256 at startup and keeps the first 8 hex characters. Templates append it to every asset URL as ?v=<hash>, so a deploy that changes any bundle changes every asset URL.

The cache headers are intentionally split:

Response Cache-Control
HTML pages (including the maintenance page) no-cache
/static/* files public, max-age=31536000
Public JSON data endpoints public, max-age=300
Trainer sprite proxy (/api/trainer-sprite/{slug}) public, max-age=2592000, immutable

HTML is revalidated every time, so the browser always sees fresh ?v= URLs; the static files behind those URLs can then be cached for a year because a content change produces a new URL.

Security

  • Headers (set on every response): X-Frame-Options: SAMEORIGIN and Content-Security-Policy: frame-ancestors 'self' (SAMEORIGIN rather than DENY so the translator preview iframe can embed site pages), X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Strict-Transport-Security: max-age=31536000; includeSubDomains.
  • CSRF: gorilla/csrf on everything except the PayPal webhook and static files. Browser mutations must send the X-CSRF-Token header.
  • Sessions: 64-character hex tokens (32 random bytes) stored server-side in the sessions table with a 30-day expiry; the cookie is HttpOnly, Secure, SameSite=Lax.
  • Passwords: bcrypt with cost 13.
  • Rate limiting: per-IP request limits on auth and public data endpoints plus a per-IP bandwidth cap on the public API (internal/server/bwlimit.go). Details in the API Reference.
  • Scraped content: LeekDuck event HTML is sanitized with bluemonday before storage (see Data Sources).

Background work

  • Game data refreshers: pogodata.Store.Start() loads embedded fallback data, overlays the disk cache, then refreshes from the network on schedules described in Data Sources.
  • Raid sweeper: a goroutine started from server.New runs every 15 seconds to enforce raid lobby and queue timers for clients that stopped polling (see Raid Finder).

Related pages

Getting Started covers running the app locally, Configuration lists the environment variables, Building and Development covers the TypeScript build, and the Frontend Guide explains the bundle structure.

Clone this wiki locally