README#
SideQuest Guild website#
The website and API for SideQuest, an Alliance guild on World of Warcraft: Forever. Members sign in with Discord, sign up for events, check their loot points and see the Hall of Heroes. Officers run events, attendance and the roster from the Officers page. It runs at sidequest-guild.com.
Two other repos work with it: the Discord bot, SideQuest-Guild/bot, and the in-game addon, SideQuest-Guild/addon. Neither one opens the database. They both go through the API.
flowchart LR
Browser -->|"/ (static site)"| API
Discord <-->|gateway + REST| Bot["SideQuest-Guild/bot<br/>discord.py plugins"]
Bot -->|"/api/guilds/... + X-API-Key"| API["api/<br/>Litestar backend"]
Addon["SideQuest-Guild/addon<br/>in game"] -->|"raid nights via /officers/sync/"| API
Website["website/<br/>Astro → dist/"] -.->|built output| API
API --> DB[(PostgreSQL)]
Common["packages/sidequest-common<br/>models · schemas · leveling math"] -.-> Bot & API
What’s on the site#
Page |
Who sees it |
What it’s for |
|---|---|---|
|
everyone |
The next raid, who is in Discord right now, and what the guild is recruiting. |
|
everyone |
Upcoming and past events. Members also see who signed up, and can sign up themselves. |
|
everyone |
How raid nights, SQL loot and attendance work. |
|
everyone |
Loot points for every raider, the loot rules, recent loot and boss kills. |
|
members |
The Hall of Heroes: Renown, raids attended, chat level and badges for each member. |
|
members |
The in-game roster, who can craft what, the guild bank log and guild news. |
|
everyone |
The addons the guild uses, with download links. |
|
anyone signed in |
Link Battle.net, get a calendar link, mark away dates, or delete your account. |
|
officers |
Events, addons, recruiting and attendance. Game Masters also get members, the activity log and moderation. |
|
officers |
Move a raid night from the addon to the site, and points back to the addon. |
|
everyone |
Guild rules, what the site keeps about you, and the terms. |
|
everyone |
The same API reference below, styled like the rest of the site. |
A member is anyone in the SideQuest Discord who has signed in. An officer holds Quest Master or Quest Guide, and a Game Master holds Quest Master. Roles are read at sign-in and kept up to date by the bot, so a promotion in Discord shows on the site within a few minutes.
Run it on your computer#
You need uv, mise and Docker. uv installs the right Python and mise installs the right Node.
cp .env.example .env # set API_KEY to any long random string
make install # Python and Node packages
make db-up migrate # start Postgres in Docker and create the tables
make dev # the site and the API at http://localhost:8000
make dev reloads as you edit. The site and the API share port 8000, so the sign-in cookie works on both.
Signing in needs a Discord app (see Sign-in). Without one, the public pages still work and the sign-in button returns an error.
To run the bot against your local API, clone the bot repo and set its API_URL to http://localhost:8000 and its
API_KEY to the same value as yours.
make help lists every command.
Before you push#
make ci runs every check GitHub runs: formatting, lint, type checks, the Python tests, the website checks, a build
and the browser tests. make fmt fixes formatting.
Python tests (
make test) start the real API with a fresh database for each test, so they don’t need Postgres.Browser tests (
make web-e2e; runmake web-e2e-installonce first) open every page in desktop Chrome, on an iPhone, an Android phone and a 320px phone. The API is faked fromwebsite/e2e/data.ts. A test fails if anything scrolls sideways, falls off the screen, doesn’t load or logs an error.npx playwright show-reportinwebsite/shows the screenshots.Type checks fail only on new errors. Known ones are listed in
pyrefly-baseline.json. After you fix some, runmake type-baseline.
After you change a database model, make migration MSG="add away dates" writes the migration and make migrate
applies it.
Where things are#
Folder |
What’s in it |
|---|---|
|
The API, built with Litestar. Each feature has a folder in |
|
The site, built with Astro. Pages are in |
|
The database models, the shape of every API request and reply, and the SQL loot rules. The bot uses it too. |
|
Python tests. |
|
Browser tests. |
With the API running, /schema lists every route with what it takes and returns; /api-reference/ on the site shows
the same thing, themed to match.
How the pieces talk#
The bot calls
/api/guilds/{guild_id}/...with the sharedAPI_KEY. About once a minute it asks what changed (new events, sign-ups, who should hold which role) and reports what happened in Discord (button clicks, voice channels, name and role changes). A change on either side reaches the other within about a minute.The addon records raid nights in game. An officer sends one to the site from
/officers/sync/by pasting aSQX1:string, uploading the addon’s save file, or runningsidequest-sync. The site sends points back as anSQI1:string for/sq import. Both formats are inpackages/sidequest-common/src/sidequest_common/schemas/(epgp.pyandguild_hub.py).The bot pins
sidequest-commonto a commit of this repo. If you change a schema the bot uses, keep the old fields working until the bot moves to the new commit.
How the features work#
Events. Each event on the site gets a Discord event and a thread in the events forum. The thread has Going, Maybe and Can’t buttons, so members can sign up from either place. Edits on the site reach Discord in about 15 seconds, and edits in Discord reach the site at once. A repeating event keeps its next 14 days filled in as ordinary events, each with its own sign-ups.
Two kinds of points. Renown is for the Hall of Heroes. Each event is worth some: a raid 10, a dungeon 5,
leveling and PvP 3, anything else 2, and an officer can change it for one event. EP and GP are the SQL loot
points. The addon records them and the site shows them on /ledger/. In the code and the API, Renown is still
called ep. The loot rules and prices are in schemas/epgp.py.
Attendance. Officers mark who came on the Officers page. Anyone who spent 30 minutes in voice during the event is
ticked already. Every number about who came (Hall of Heroes, badges, Renown, the attendance report) uses one rule,
attended_condition() in api/src/sidequest_api/lib/attendance.py. Use it for any new one too.
Hall of Heroes and badges. Seasons are listed in SEASONS in schemas/heroes.py. Season One starts on November
4, 2026. Each badge is also a Discord role, which the bot grants and announces.
Badge |
How to earn it |
|---|---|
First Steps |
Come to your first guild event. |
First Raid |
Come to your first raid. |
Raid Regular |
Come to 10 raids. |
Raid Veteran |
Come to 25 raids. |
Perfect Month |
Come to every raid in a month that had at least 4. |
Founding Member |
Be on the guild roster before launch. |
Chatterbox |
Reach chat level 10. |
Top Helper |
Come to the most dungeons and leveling groups last month. It moves to the next winner each month. |
Discord roles from the site. Signing in gives Signed the Ledger, and linking Battle.net gives Battle-Tagged. New people who haven’t signed in get At the Gate. When a Game Master confirms someone on the in-game roster, they get New Arrival, which opens the guild channels. The bot checks every member once a minute, and it never hands out a role with moderator powers or one above its own.
Calendar and away dates. Each member can get a private calendar link for Google or Apple Calendar with the events
they signed up for. Anyone with the link can read it, so “Make a new link” on /account/ turns the old one off.
Members can also mark dates they’ll be away. The bot gives them Away on a Quest for those days, and the raid chart
shows who is away.
Your data. If you start keeping something new about members, add it to /privacy/ and make delete_account in
api/src/sidequest_api/domain/members/services.py remove it.
MCP access for officers#
Officers can let Claude Code, omp or any other MCP client work on the guild for them: look up members, read and
post in channels, post announcements, and make, change or cancel events. Make a token on /officers/ under
Claude / MCP access, then run:
claude mcp add --transport http sidequest https://sidequest-guild.com/api/mcp --header "Authorization: Bearer <token>"
The site shows the token once and keeps only a scrambled copy. Revoke it on the same page.
It works only while you are an officer. A few tools, such as the attendance report, need a Game Master.
No tool can hand out or take away a role with moderator powers, a role another app manages, or a role above the bot’s own.
Everything a token does shows in the log on
/officers/#log, under your name.The site calls Discord with the bot’s token (
DISCORD_TOKEN). Without it, the Discord tools say so and the rest work.
The code is in api/src/sidequest_api/domain/mcp/.
Sign-in#
Discord. In the Discord Developer Portal, open the Game Master app, then OAuth2. Add the redirect
http://localhost:8000/auth/discord/callback (on the live site, https://sidequest-guild.com/auth/discord/callback).
Copy the client ID and secret into WEB_DISCORD_CLIENT_ID and WEB_DISCORD_CLIENT_SECRET, and set
WEB_SESSION_SECRET to a long random string. Without it, everyone is signed out each time the API restarts.
Battle.net. Make a client at develop.battle.net with the redirect
http://localhost:8000/auth/battlenet/callback, and copy its ID and secret into WEB_BATTLENET_CLIENT_ID and
WEB_BATTLENET_CLIENT_SECRET. The site asks only for the BattleTag. It can’t read characters yet, because Blizzard
hasn’t said whether Forever will be in its API.
Deploying#
Push to main. When CI passes, Cabotage builds the Docker image, runs the migrations, then starts the new version.
It takes about 13 minutes. Two things to know:
A push deploys only if its newest commit changes app files. A commit that touches only
.github/deploys nothing, even if older commits under it were never deployed.For a minute the old and new versions run side by side, and the migrations run first. So a migration must work with the old code too: give new columns a default or let them be empty.
After each deploy, the Deployed workflow clears Cloudflare’s cache and runs the browser tests against the live
site. gh run list --workflow deployed.yml shows how it went. Each deploy also starts two or three runs that skip
themselves, so look for the one that didn’t.
The Procfile has two processes from the same image: release runs the migrations, and web serves the API and the
site. The health check path is /health/live. Settings for the live site:
Setting |
Value |
|---|---|
|
|
|
the database’s |
|
a long random string, the same one the bot uses |
|
|
|
|
|
at least 32 characters |
|
the Discord app |
|
the Battle.net client |
|
only to change who counts as an officer or Game Master |
To run the whole thing in Docker on your computer, use make docker-up, and make docker-down to stop it.
Cloudflare#
Cloudflare answers almost every request itself, so the server rarely sees a visitor.
What |
How long Cloudflare keeps it |
|---|---|
|
a year. The file names change with every build. |
Pages |
a minute. After that it shows the copy it has while it fetches a new one. |
|
15 seconds. Everyone sees the same counts. |
Everything else in |
never kept |
A route is kept only if it sends a Cloudflare-CDN-Cache-Control header and skips the session, so it can never hand
one person’s cookie to another. The cache rule is “Pages and live counts at the edge” on the sidequest-guild.com
zone. It ignores the query string, so every /events/detail/?id=... is one page. Smart Tiered Cache, 0-RTT and Early
Hints are on.
Page scripts and the offline copy#
Links change the page without a full reload. A page script must start with onPage from
website/src/scripts/page.ts, or it runs only on the first page a visitor opens.
After the first visit, a service worker (website/service-worker/sw.js) keeps every page, script and font on the
device, so pages open at once and work offline. Public data shows from the device first and updates when the fresh
copy arrives. The saved data is deleted on every sign-up, edit, sign-in and sign-out, so nobody sees another
person’s data. To turn the service worker off, set SERVICE_WORKER = false in website/astro.config.mjs and deploy.
It deletes its copies and removes itself.