Goose Games: From Badge Scan to Points#

At Hack the North, a badge scan at a workshop did two things: checked you in, and gave you special points to spend on prizes. That second part was Goose Games: an quest-reward system built to give hackers a low-stakes reason to step away from their projects, try a workshop, or meet someone new.

As Ivan put it in the Hack the North retrospective:

How do we make one badge scan count toward a quest without letting a second scan award points again? I had a blast figuring out what the backend needed to remember.

below: Hacker dashboard designed by Weinna Zheng. Developed by the frontend team.

Objectives#

We needed to support three things.

  • Track exactly what a player completed so a repeat scan earns nothing.
  • Trace every balance change to a quest reward, prize redemption, or manual adjustment so that every player can be audited under scrutiny
  • Keep attendance independent while saving each quest award as a single operation.

TL;DR. This is the schema we landed on. goose_game_quest_log tracks quest completions, goose_game_transactions tracks points, and check-in can succeed on its own. The sections below walk through each decision.

Database diagram linking events to their activities, attendees, quests, and store items. The quest log records the activity ID for scanned quests, and both logs link to the player.
Open full-size diagram

Prevent duplicate quest rewards#

A counter per player and quest could tell us someone attended two workshops. When the next scan came in, we still needed to know which two.

goose_game_quest_log is a join table between goose_game_players and goose_game_quests, with one row per completion. For workshops, the database allows only one row for each combination of attendee, quest, and activity. For networking quests, it uses the person scanned in place of the activity.

Counting rows takes more work than reading a counter, but the same rows tell us when each completion happened. Each player links to an event_attendees row, so returning hackers start with separate progress for each edition.

Trace every points change#

A workshop reward might add five points, and a prize might cost ten without changing quest progress. goose_game_transactions records each credit or debit and its reason. A reward links to the quest, and a redemption links to an item in goose_game_store_items. The dashboard reads the current balance from goose_game_players.

Saving the balance and ledger entry separately could leave them out of sync if one write failed. We save them in one database transaction and include the completion row when awarding a quest. For scanned quests, that PostgreSQL transaction runs at READ COMMITTED. The points increment and ledger insert share a savepoint inside the same transaction as the quest completion. If any write fails, all of them roll back.

Keep check-in separate from quest rewards#

The scanner writes to event_activity_scans first so check-in works even when no reward is due.

  1. Use the workshop’s event_activities row to find a quest for the same event and activity category, then check the player’s completion limit.
  2. For an eligible scan, save the balance, ledger entry, and completion together in one transaction.
  3. If the database rejects a duplicate completion, roll back the award.
An example player starts with 20 points
ActionWhat we recordBalance
First workshop scanWorkshop completion
+5 points in the ledger
25
Same workshop againNeither log changes25
Redeem a prize−10 points in the ledger15

A hacker could spend their points and still see the workshop count toward their quest. If a balance looked off, we could check what they completed and follow each credit and debit in the ledger.


Special thanks to Jackie Fung for having my back throughout development. I learned a ton working together on schema design and programming practices, and for always having the most encouraging and positive attitude regardless of where and when we were at. Couldn’t have shipped Goose Games without you. 🫡

← Back to all artifacts