Gamifying backend using Node.js + Express + Prisma + PostgreSQL.
npm install
cp .env.example .env
npm run prisma:migrate -- --name init
npm run prisma:generate
npm run prisma:seed
npm startBase URL: http://localhost:3000
PORT=3000
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/gamifying?schema=public"
JWT_SECRET="super-secret-change-me"{ "success": true, "data": {}, "error": null }{ "success": false, "data": null, "error": "message" }Request:
{ "name": "John", "email": "john@mail.com", "password": "password123", "gymId": "gym_id" }Response:
{ "success": true, "data": { "token": "jwt", "user": { "id": "u1", "email": "john@mail.com" } }, "error": null }Request:
{ "email": "john@mail.com", "password": "password123" }Response:
{ "success": true, "data": { "token": "jwt", "user": { "id": "u1", "email": "john@mail.com" } }, "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": { "id": "u1", "xp": 500, "gymCoins": 200, "equippedCosmetics": [], "activeSupplements": [] }, "error": null }Headers: Authorization: Bearer <token>
Request:
{
"gender": "MALE",
"faceJawId": 1,
"faceCheeksId": 1,
"faceEyeShapeId": 1,
"faceEyeColorId": 1,
"faceNoseId": 1,
"faceHairStyleId": 1,
"faceHairColorId": 1,
"faceSkinToneId": 1,
"faceBeardId": 0,
"faceEyebrowId": 1
}Response:
{ "success": true, "data": { "id": "u1", "avatarGender": "MALE", "faceJawId": 1 }, "error": null }Headers: Authorization: Bearer <token>
Response:
{
"success": true,
"data": {
"xpEarned": 50,
"gcEarned": 10,
"muscleGained": 2,
"enduranceGained": 1,
"powerGained": 1,
"newBodyStage": 2,
"newClass": "FIGHTER",
"activeSupplements": ["Protein Shake"]
},
"error": null
}Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": [{ "id": "c1", "xpEarned": 50, "createdAt": "2026-02-25T00:00:00.000Z" }], "error": null }Headers: Authorization: Bearer <token>
Request:
{ "gymId": "gym_id", "amount": 20 }Response:
{ "success": true, "data": { "gcEarned": 100, "newGymCoinsTotal": 450 }, "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": [{ "id": "p1", "amount": 20, "gcEarned": 100 }], "error": null }Headers: Authorization: Bearer <token>
Request:
{ "referredEmail": "friend@mail.com" }Response:
{ "success": true, "data": { "xpEarned": 200, "gcEarned": 100 }, "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": { "SUPPLEMENT": [{ "name": "Protein Shake" }], "COSMETIC": [{ "name": "Red Tank Top" }] }, "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": { "success": true, "newGymCoinsTotal": 350, "item": { "id": "ui1" } }, "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": { "activeSupplements": [], "cosmetics": [] }, "error": null }Response:
{ "success": true, "data": { "jaw": [{ "id": 1, "label": "oval" }], "cheeks": [{ "id": 1, "label": "flat" }] }, "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": { "class": "FIGHTER", "bodyStage": 2, "equippedCosmetics": [], "activeSupplements": [] }, "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": [{ "id": "ui1", "isEquipped": true, "shopItem": { "category": "OUTFIT" } }], "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": { "challengerProbability": 0.55, "defenderProbability": 0.45, "winnerId": "u1", "gcEarned": 50, "xpEarned": 30 }, "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": [{ "result": "won", "opponentName": "Marcos", "probability": 0.55 }], "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": [{ "userId": "u1", "name": "Carlos", "wins": 5 }], "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": [{ "rank": 1, "name": "Carlos", "xp": 3400, "currentMonthXp": 600 }], "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": [{ "rank": 1, "name": "Carlos", "score": 94.5 }], "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": { "competition": { "status": "ACTIVE" }, "standings": [{ "rank": 1, "name": "Carlos" }] }, "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": [{ "month": 1, "year": 2026, "winner": { "name": "Carlos" } }], "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": { "winner": "Carlos", "second": "Ayu", "third": "Dimas", "prizesAwarded": [] }, "error": null }Headers: Authorization: Bearer <token>
Request:
{ "name": "Iron Temple", "location": "Bali" }Response:
{ "success": true, "data": { "gym": { "id": "g1", "name": "Iron Temple" }, "ownerToken": "jwt" }, "error": null }Headers: Authorization: Bearer <token>
Response:
{ "success": true, "data": { "name": "Obsidian Gym", "location": "Canggu Bali", "memberCount": 5, "currentCompetition": {}, "currentPrize": "Monthly Bodybuilding Crown" }, "error": null }- Daily at
00:00: avatar inactivity decay, supplement expiry, streak shield handling. - Monthly at
1st 00:01: close active competitions, create new competition per gym, resetcurrentMonthXp.
- XP and GymCoins are separate currencies.
- Check-in is limited to one per calendar day.
- Battle only inside same gym and cannot self-challenge.
- Cosmetic items cannot be re-purchased.
- Supplements can be re-purchased.
- Avatar class/body stage auto-recalculated after progression changes.
- All routes except
/api/auth/register,/api/auth/login,/api/avatar/face-optionsrequire JWT. - Avatar creation is required for app routes through middleware guard.
- 1 gym: Obsidian Gym, Canggu Bali
- 5 users: Carlos, Marcos, Ayu, Dimas, Sofia
- 10 shop items
- 20 check-ins, 10 purchases, 3 battles
- 1 active current-month competition