Cosmic Quest

A wellness game whose backend has to remember every step you took to reach the next planet.

Context
Backend coursework, individual
My part
Everything: schema, API, auth and the plain frontend
Stack
Node.js, Express, MySQL, JWT, bcrypt, Bootstrap
Year
2026
Cosmic Quest dashboard

The problem

The brief was a wellness-challenge API: users, challenges and completions. On its own that is a to-do list with points. To make people come back, I turned it into a game: complete real-life challenges, earn points and resources, unlock planets, build and upgrade colonies on them, collect badges and climb a leaderboard.

That moves the difficulty into the backend. A single tap on “complete” now has to record the completion, pay out points and resources, and maybe award a badge, and none of those can happen twice or half-way. Spending is the same in reverse: unlocking a planet or upgrading a colony must never leave points gone and nothing to show for them.

The game loop

  • ChallengesAnyone signed in can create one; only its creator can edit or delete it. Completing one pays its points and one stardust, plus one ore when it is worth 20 points or more.
  • PlanetsEach has an unlock cost in points. Challenges can be linked to planets as their missions.
  • ColoniesOne per planet per player once it is unlocked. Building costs 5 stardust and 2 ore; each upgrade costs 3 and 2, up to level 5, and can rename it too.
  • BadgesAwarded automatically at milestones: first completion, first planet, first colony, a level 2 colony, and creating a challenge.
  • LeaderboardRanks players by everything they’ve earned, including points they’ve since spent on planets.

A planet

Earth planet page: unlocked, with Ethan Prime Colony at level 2, upgrade and rename buttons, and two challenges on the planet
Unlocked, with a level 2 colony. The upgrade button carries its cost, and the navbar shows points, stardust and ore live.

The schema

Eight tables. Three came from the brief (users, challenges, completions) and five are the game: planets, each player’s unlocked planets with the colony stored on the same row, the links between challenges and planets, badges and earned badges. Progress is spread across join tables instead of counters on the user, so every rule can be read straight off the data.

Unique keys do the first line of defence: one completion per user per challenge, one unlock and colony per user per planet, one of each badge per user. A duplicate request fails at the database even if the code above it gets the order wrong.

Eight tables

Database diagram: User, WellnessChallenge, UserCompletion, Planet, UserPlanet, ChallengePlanet, Badge and UserBadge with their foreign keys
Every relationship cascades on delete, so removing a user or a challenge takes its completions, unlocks and badges with it.

Each endpoint as a chain

Express routes are only a list of middleware: every step validates, checks, writes or rewards, and passes on or answers with the right status. Unlocking a planet reads top to bottom as the rule it enforces.

router.post(
  "/:user_id/planets/:planet_id/unlock",
  jwtMiddleware.verifyToken,                         // 401
  checkOwnerMiddleware.checkOwnerMiddleware,         // 403 not you
  userPlanetController.checkUserExistMiddleware,     // 404
  planetController.checkPlanetExistMiddleware,       // 404
  userPlanetController.checkPlanetNotUnlockedMiddleware, // 409
  userPlanetController.checkSufficientPointsMiddleware,  // 400
  userPlanetController.unlockPlanetMiddleware,
  userPlanetController.deductPointsOrRollbackMiddleware,
  userPlanetController.returnUnlockStatsMiddleware,
  userPlanetController.awardPlanetBadgeMiddleware,
  userPlanetController.sendUnlockResponse
);
src/routes/usersRoutes.js, Endpoint 14 (abridged)

The API has 33 numbered endpoints across auth, users, challenges, completions, planets, colonies and badges. Registering hashes the password with bcrypt before insert; logging in accepts a username or an email; both end by issuing a JWT that the protected routes verify.

Tested by hand in Postman

Postman: POST /api/auth/login with an email identifier returns 200 OK, Login successful, and a token
Login by email returns a signed token for the protected routes.

Creator-only editing

Cosmic Quest challenge editor: the signed-in player’s own challenges on the left, a description, points between 5 and 30, and a planet assignment on the right
A creator edits their own challenges and links them to planets as missions. For anyone else, the API answers 403.

Decisions

  • Spend only if the balance covers it, in one statement

    Checking the balance in one query and deducting in the next leaves a gap two requests can race through. The deduction is a single UPDATE with the balance in its WHERE clause, so it either spends or changes nothing, and zero affected rows means “not enough”.

  • Undo by hand when there is no transaction

    The course used plain callbacks without transactions, so each multi-step spend has a compensating step. An unlock inserts the row first (the unique key stops doubles), then deducts; if the deduction fails, it deletes the row. A colony deducts first, then builds; if the build fails, it refunds.

  • Ownership comes from the token, not the URL

    Every route that changes a player’s data compares the user id in the verified token with the one in the path and answers 403 on a mismatch. Challenges and completions check their creator or owner the same way, so a valid login can’t edit someone else’s progress.

  • Rank what was earned, not what is left

    Points are also the currency for planets. Ranking by the current balance would punish playing the game, so the leaderboard adds back the cost of every unlocked planet and ranks on total earned.

Results

  • 33

    Numbered API endpoints, each a chain of small middleware steps

  • 8

    MySQL tables, three from the brief and five for the game

  • 5

    Milestone badges awarded automatically inside the same requests that earn them

Individual coursework, tested manually with Postman and through the frontend against seeded players at different stages of progress. There are no automated tests, and it was never deployed beyond localhost; the demo video walks through it running locally.

Limits

  • Compensating steps aren’t transactions: if the server dies between a deduction and its refund, the player loses the resources.
  • The rate limiter allows 5,000 requests per 15 minutes per IP, loose enough to be decoration.
  • The JWT sits in browser storage and there is no refresh or revocation: a token is good until it expires.
  • A seeded role column hints at admins, but there is no admin role or moderation of user-created challenges.
  • Each challenge can only be completed once per player, which suits milestones better than daily habits.