Skip to content
devinder-devPublic

About

Mitt Djur — gamified pet wellness app (Bun + Fastify + React)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mitt Djur

A habit-tracking app where a virtual pet's health is a real-time reflection of your real-life habits — skip your habits, and it can die.

Backend built solo across a school sprint — 30+ endpoints, JWT auth, cron-driven decay, XP/streak/mission systems, layered architecture end to end.

Bun Fastify TypeScript MongoDB React Docker


Note

This is a public mirror of a private school GitLab repo (Chas Academy, 2025–2026), pushed here so the code is browsable outside the school network. I was the sole backend developer — every endpoint, service, model, cron job, and CI/CD fix below is mine. Teammates built the React frontend UI and Figma design; DevOps ran the Kubernetes cluster. Secrets are replaced with placeholders.

What it does

A user adopts a pet (cat, dog, rabbit, or raccoon) that starts as an egg and evolves through levels as XP comes in from logged activity — walks, water, sleep, mood check-ins, gratitude entries, and 7-day mission streaks. Skip a day and a nightly cron job decays the pet's health; hit zero three times and the pet dies permanently, revivable only by spending coins on a fresh one. The technically interesting part isn't the pet skin — it's that XP, leveling, streak continuity, and health decay all live in pure, request-agnostic service functions, so the same rules apply whether they're triggered by an HTTP call or a scheduled job with no request object in sight.

How it works

The flow below is grounded in the actual code path for logging an activity — backend/src/services/activityService.ts, xpService.ts, and streakService.ts — plus the nightly decay job that runs independently of any request.

sequenceDiagram
    actor User
    participant API as Fastify API
    participant Auth as authenticate middleware
    participant Svc as activityService
    participant XP as xpService (pure)
    participant Streak as streakService
    participant DB as MongoDB

    User->>API: POST /api/activity/log { type: "walk" }
    API->>Auth: verify JWT access token
    Auth-->>API: userId
    API->>Svc: logActivity(db, userId, body)
    Svc->>DB: findAlivePetByUserId
    Svc->>DB: already logged "walk" today?
    alt not logged yet
        Svc->>XP: applyXp(pet.xp, pet.level, 20)
        XP-->>Svc: newXp, newLevel, newStage, leveledUp
        Svc->>DB: save activity log + updated pet
    else already logged
        Svc-->>Svc: xpEarned = 0 (no double-dipping)
    end
    Svc->>Streak: checkAndUpdateStreak(db, userId)
    Streak->>DB: lastStreakDate === yesterday?
    Streak->>DB: increment or reset streak
    Svc-->>API: { xpEarned, alreadyLogged, pet }
    API-->>User: 200 OK

    Note over API,DB: Independently, every night at 00:05 (Europe/Stockholm):<br/>a node-cron job decays health for inactive pets and applies<br/>the 3-strike death rule — no HTTP request involved.
Loading

Status

Verified against the actual code — nothing here is aspirational.

Area State
Auth — register/login/logout/refresh, JWT access + httpOnly refresh cookie ✅
Activity logging — walk/water/sleep/read, steps sync, mood, gratitude, daily notes ✅
XP/level/stage system — pure functions, 18 levels, 7 evolution stages ✅
Streak tracking — idempotent per day, breaks on a missed day ✅
Missions — 7-day streak cycles, triple reward on day 7, max 5 active ✅
Pet health decay + 3-strike death, cron job (node-cron, 00:05 Europe/Stockholm) ✅
Shop — buy/equip/unequip cosmetic accessories with coins ✅
Friends — add/accept/remove, XP leaderboard ✅
Notifications, achievements, weekly summary cron job ✅
HUD endpoint — aggregates pet/today/missions/streak/coins in one call ✅
Env validation (Zod, fail-fast), rate limiting, CORS ✅
React frontend (Vite) — all screens listed in structure ✅
Automated test coverage 🔨 2 unit test files (validate.test.ts, authSchemas.test.ts) — not a full suite
backend/docker-compose.yaml — all three services under services: ✅
CI (.gitlab-ci.yml) ✅ typecheck on every push; Docker build + push on develop/main

Quick start

Requires Bun and a MongoDB instance (Atlas or local).

git clone https://github.com/devinder-dev/mitt-djur.git
cd mitt-djur/backend
cp .env.example src/.env      # fill in MONGODB_URI + two 32-char JWT secrets
bun install
bun run dev                    # http://localhost:3000 — watch mode
curl localhost:3000/health
# {"status":"ok","timestamp":"...","environment":"development"}
cd frontend
bun install
echo "VITE_API_URL=http://localhost:3000/api" > .env
bun run dev                    # http://localhost:5173
The XP / level / stage system — pure functions, no I/O (click)

xpService.ts is deliberately kept free of database or HTTP concerns — it's just math, which is what lets every XP-awarding feature (steps, activities, gratitude, mood, missions) call the same applyXp() function and get identical, testable behavior:

export const applyXp = (currentXp: number, currentLevel: number, xpToAdd: number) => {
  const newXp = currentXp + xpToAdd;
  const newLevel = calculateLevel(newXp);   // walks 18 XP thresholds, 0 → 8500
  const newStage = calculateStage(newLevel); // maps level to egg → chick → ... → dragon
  const leveledUp = newLevel > currentLevel;
  return { newXp, newLevel, newStage, leveledUp };
};

Activity XP is anti-cheese by construction: logActivity checks whether today's log already contains that activity type before awarding XP, so spamming POST /api/activity/log for the same activity on the same day earns nothing after the first call. Mood check-ins go further and reject the second call outright (409 Conflict) rather than silently no-opping.

Streaks and the nightly decay job (click)

checkAndUpdateStreak is idempotent per calendar day: it compares lastStreakDate against today and yesterday to decide whether to increment, reset to 1, or do nothing if already counted today. It's called from every activity endpoint (log, steps, mood, gratitude, notes), so any single daily action keeps the streak alive.

Health decay runs on a completely separate path — node-cron at 5 0 * * * (00:05 Europe/Stockholm), operating in bulk MongoDB writes with no per-pet loop:

  1. Pets active today → mood set to happy
  2. Inactive pets, health won't hit zero → decay by 20
  3. Inactive pets hitting zero (1st/2nd time) → reset to 1 health, healthHitsZero incremented, pet "warned"
  4. Inactive pets hitting zero a 3rd time → pet dies (status: 'dead')

Reviving requires the current pet to be dead — the new pet starts back at level 0, egg stage, full health.

Security controls actually implemented (click)
Control Implementation
Password hashing bcrypt, 10 salt rounds
Access tokens Signed JWT, kept client-side in memory (not localStorage)
Refresh tokens httpOnly cookie; hash (bcrypt) stored server-side, not the raw token
Env validation Zod schema in config/env.ts — app exits immediately if a required var is missing or a JWT secret is under 32 chars
Input validation Zod on every route body
Rate limiting @fastify/rate-limit, applied globally
CORS Locked to FRONTEND_URL
XSS safeSrc() sanitizer in the frontend blocks non-relative/non-https image URLs
NoSQL injection Email regex validation added to the giveCoins.ts admin script after a review finding
Ownership checks completeMission rejects (403) if the mission's userId doesn't match the caller
Repository layout & tech stack (click)

Backend follows a strict layered architecture — each layer only calls the one below it:

Route → Controller → Service → Repository → Model
  • Route — registers the URL + middleware, calls the controller
  • Controller — validates input (Zod), calls the service, sends the HTTP response
  • Service — business logic only, no MongoDB access
  • Repository — all MongoDB queries, no business logic
  • Model — TypeScript interfaces for every document shape
backend/
├── src/
│   ├── config/         env.ts (Zod-validated), db.ts
│   ├── controllers/    one file per endpoint
│   ├── services/       activity, xp, streak, pet, mission, shop,
│   │                   friend, notification, achievement, auth, hud
│   ├── repositories/   MongoDB queries only
│   ├── models/         TypeScript document interfaces
│   ├── routes/         Fastify route registration per domain
│   ├── middleware/      authenticate.ts (JWT verification)
│   ├── plugins/         cors, rateLimit, mongodb, jwt, errorHandler
│   ├── jobs/             healthDecay.ts, weeklySummary.ts (node-cron)
│   ├── schemas/          Zod request schemas
│   └── data/              missions.json, shopItems.json
frontend/
├── src/
│   ├── api/             one client file per domain (auth, pet, shop, friends…)
│   ├── components/      Home, PetScreen, Missions, Shop, Friends,
│   │                    Notifications, Profile, Login/Register, PetSelect
│   └── lib/               petImage.js, useEquippedPreview.js
kubernetes/               namespace, deployment, service, ingress, secrets
.gitlab-ci.yml             typecheck → Docker build/push → deploy
Layer Choice
Runtime Bun
API Fastify 5
Language TypeScript
Database MongoDB (Atlas or local)
Auth jsonwebtoken + bcrypt, @fastify/cookie for the refresh cookie
Validation Zod
Scheduled jobs node-cron
Frontend React 19 + Vite, vanilla CSS, React Router
Infra Docker multi-stage builds, nginx (SPA routing), Kubernetes, GitLab CI

Devinder Singh — sole backend developer · Fullstack Developer (YH) student at Chas Academy, Stockholm

About

Mitt Djur — gamified pet wellness app (Bun + Fastify + React)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages