Case study
AFTL Raid Manager
A guild was running its entire weekly operation out of a shared Google Sheet: who's raiding, who's geared, who owes whom for loot. I replaced it with a web app and kept building on it, eventually well past what the sheet did — including a live tournament mode with drafting, spectating, and betting.
- Role
- Solo developer — design, frontend, database, serverless backend
- Timeline
- 2025 – present
- Status
- Live, actively maintained
The problem
Coordinating a guild raid means tracking who is available, what gear each of their characters has, which eight-player lineups are viable, and who is owed what after loot drops. All of it lived in one Google Sheet that several people edited simultaneously, with no validation and no history. Lineups broke silently, loot debts got forgotten, and only one person understood the sheet.
What I built
- A single-page app with a hand-rolled router and no UI framework — ten pages covering lineups, the player pool, a drag-and-drop lineup editor, gear enhancement planning, recruiting, and an admin console.
- A Postgres schema on Supabase behind a Discord OAuth login, with app-level roles deciding who edits lineups and who just views them.
- Discord OAuth as the only login: the guild already lives in Discord, so guild identity is the account.
- A loot ledger where players upload a screenshot and Claude's vision API extracts the items into structured rows, instead of someone typing them in by hand.
- An arena mode: a real-time PvP tournament with character drafting, turn-based match resolution, live spectating, and a gold betting market, all synced through Supabase Realtime.
Screenshots





Technical notes
Swapping the backend under a live app
The app originally read and wrote through the Google Sheets API. Moving to Postgres meant changing the data layer under a live app, so I built the Supabase client as a drop-in alongside the Sheets client behind a shared interface, ran both during cutover, and kept the Sheets path as a fallback until the new schema had proven itself under real traffic.
No framework, on purpose
The app is plain ES modules — a small router, explicit render functions, and direct DOM updates. It started as a learning constraint and stayed because the payload is tiny and there's nothing between me and the bug. It also meant that when I picked React back up, the problems it solves were ones I'd already run into by hand.
Keeping game logic server-side
Arena actions, draft picks, bet resolution, gold payouts, and the Discord OAuth token exchange all run in Netlify Functions with the service role key — one authoritative implementation of the game rules instead of logic scattered across whichever client submits first, and the OAuth client secret never reaches the browser.
Real-time multiplayer state
Arena matches have several people watching one authoritative game state at once. Supabase Realtime pushes each state transition to spectators, with server-side force-actions so an admin can unstick a match when someone disconnects mid-draft.
By the numbers
- 246
- Commits
- 16
- Screens
- 16
- Serverless functions
- 53
- Schema migrations
- 42k
- Lines of app code
Stack
Frontend
Backend
Integrations
What I'd do differently
- Fifty-three loose SQL migration files works until it doesn't. I'd use a proper migration tool with up/down pairs from day one.
- Several page modules grew past a thousand lines before I split them. I now break a screen apart the moment it has two unrelated reasons to change.
- Building it framework-free was the right call for learning and the wrong call for the arena — shared reactive state is exactly the problem React exists to solve, and I hand-rolled a worse version of it.