TidyQuest/API.md
root a7cd81fe81 Initial release: TidyQuest v0.1.0
TidyQuest is a self-hosted web application that gamifies household chores
using RPG mechanics. Complete tasks, earn coins, unlock achievements, and
compete with your family on the leaderboard.

Features:
- 🎯 Health-based task tracking with visual decay over time
- 💰 Coin system with customizable rewards
- 🔥 Daily/weekly streak tracking
- 🏆 Family leaderboard (week/month/quarter/year)
- 🎖️ Automatic achievement unlocking
- 📅 Calendar view for upcoming tasks
- 🌍 Multilingual support (EN/FR/DE/ES)
- 📱 Optional Telegram notifications
- 🏖️ Vacation mode (pause task decay)
- 🐳 Docker deployment (single container)

Tech Stack:
- Frontend: React 19 + Vite + TypeScript
- Backend: Node.js + Express + TypeScript
- Database: SQLite3 with WAL mode
- Auth: JWT + bcrypt
- Deployment: Docker Compose

Includes:
- 8 room types with 60+ predefined tasks
- 10 preset rewards
- 12 achievement types
- Complete API documentation
- Security best practices guide
- Comprehensive README with examples

License: GNU Affero General Public License v3.0 (AGPL-3.0)
This ensures TidyQuest remains free and open-source forever, preventing
proprietary SaaS forks. Any modified hosted version must share source code.
2026-02-17 18:40:09 +01:00

16 KiB

🌐 TidyQuest API Documentation

Complete reference for all TidyQuest REST API endpoints.

Base URL: http://localhost:3020/api

Authentication: All endpoints (except /auth/*) require a JWT token in the Authorization header:

Authorization: Bearer <your-jwt-token>

📑 Table of Contents


🔐 Authentication

Register

Create a new user account.

Endpoint: POST /api/auth/register

Request Body:

{
  "username": "john",
  "password": "securepass123",
  "displayName": "John Doe",
  "avatarColor": "#F97316",
  "language": "en"
}

Response (201):

{
  "user": {
    "id": 1,
    "username": "john",
    "displayName": "John Doe",
    "role": "admin",
    "avatarColor": "#F97316",
    "avatarType": "letter",
    "coins": 0,
    "currentStreak": 0,
    "language": "en",
    "createdAt": "2026-02-17T10:30:00.000Z"
  },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

cURL Example:

curl -X POST http://localhost:3020/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "john",
    "password": "securepass123",
    "displayName": "John Doe",
    "avatarColor": "#F97316",
    "language": "en"
  }'

Login

Authenticate and receive a JWT token.

Endpoint: POST /api/auth/login

Request Body:

{
  "username": "john",
  "password": "securepass123"
}

Response (200):

{
  "user": { ... },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

cURL Example:

curl -X POST http://localhost:3020/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "john", "password": "securepass123"}'

Get Current User

Get authenticated user's profile.

Endpoint: GET /api/auth/me

Headers: Authorization: Bearer <token>

Response (200):

{
  "id": 1,
  "username": "john",
  "displayName": "John Doe",
  "role": "admin",
  "avatarColor": "#F97316",
  "avatarType": "letter",
  "avatarPreset": null,
  "avatarPhotoUrl": null,
  "coins": 150,
  "currentStreak": 7,
  "goalCoins": 500,
  "goalStartAt": "2026-02-01T00:00:00.000Z",
  "goalEndAt": "2026-02-28T23:59:59.000Z",
  "isVacationMode": false,
  "language": "en",
  "createdAt": "2026-01-15T08:00:00.000Z"
}

cURL Example:

curl -X GET http://localhost:3020/api/auth/me \
  -H "Authorization: Bearer <your-token>"

🏠 Rooms

List All Rooms

Endpoint: GET /api/rooms

Response (200):

[
  {
    "id": 1,
    "name": "Kitchen",
    "roomType": "kitchen",
    "color": "#FFE4C4",
    "accentColor": "#FFA500",
    "photoUrl": null,
    "sortOrder": 1,
    "health": 75,
    "createdAt": "2026-02-10T12:00:00.000Z"
  }
]

cURL Example:

curl -X GET http://localhost:3020/api/rooms \
  -H "Authorization: Bearer <your-token>"

Get Room by ID

Endpoint: GET /api/rooms/:id

Response (200):

{
  "id": 1,
  "name": "Kitchen",
  "roomType": "kitchen",
  "color": "#FFE4C4",
  "accentColor": "#FFA500",
  "photoUrl": null,
  "sortOrder": 1,
  "createdAt": "2026-02-10T12:00:00.000Z"
}

Create Room

Endpoint: POST /api/rooms (Admin only)

Request Body:

{
  "name": "Master Bedroom",
  "roomType": "bedroom",
  "color": "#E6F3FF",
  "accentColor": "#4A90E2"
}

Response (201):

{
  "id": 5,
  "name": "Master Bedroom",
  "roomType": "bedroom",
  ...
}

cURL Example:

curl -X POST http://localhost:3020/api/rooms \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Master Bedroom",
    "roomType": "bedroom",
    "color": "#E6F3FF",
    "accentColor": "#4A90E2"
  }'

Update Room

Endpoint: PUT /api/rooms/:id

Request Body:

{
  "name": "Main Kitchen",
  "sortOrder": 2
}

Response (200):

{
  "id": 1,
  "name": "Main Kitchen",
  "sortOrder": 2,
  ...
}

Delete Room

Endpoint: DELETE /api/rooms/:id (Admin only)

Response (200):

{
  "success": true
}

Get Default Tasks for Room Type

Endpoint: GET /api/rooms/defaults/:roomType

Available room types: kitchen, bedroom, bathroom, living, office, garage, laundry, garden

Response (200):

[
  {
    "name": "Wash Dishes",
    "frequencyDays": 1,
    "effort": 2,
    "translationKey": "kitchen.wash_dishes",
    "iconKey": "wash_dishes"
  },
  {
    "name": "Clean Fridge",
    "frequencyDays": 14,
    "effort": 3,
    "translationKey": "kitchen.clean_fridge",
    "iconKey": "fridge"
  }
]

Tasks

List Tasks for a Room

Endpoint: GET /api/rooms/:roomId/tasks

Response (200):

[
  {
    "id": 1,
    "roomId": 1,
    "name": "Wash Dishes",
    "notes": "Use dishwasher for big loads",
    "frequencyDays": 1,
    "effort": 2,
    "health": 85,
    "lastCompletedAt": "2026-02-17T08:00:00.000Z",
    "translationKey": "kitchen.wash_dishes",
    "iconKey": "wash_dishes",
    "createdAt": "2026-02-10T12:00:00.000Z"
  }
]

cURL Example:

curl -X GET http://localhost:3020/api/rooms/1/tasks \
  -H "Authorization: Bearer <your-token>"

Create Task

Endpoint: POST /api/rooms/:roomId/tasks (Admin only)

Request Body:

{
  "name": "Deep Clean Oven",
  "notes": "Use oven cleaner spray",
  "frequencyDays": 30,
  "effort": 5,
  "translationKey": "kitchen.deep_clean_oven",
  "iconKey": "oven"
}

Response (201):

{
  "id": 15,
  "roomId": 1,
  "name": "Deep Clean Oven",
  ...
}

Update Task

Endpoint: PUT /api/tasks/:id (Admin only)

Request Body:

{
  "frequencyDays": 7,
  "effort": 3
}

Response (200):

{
  "id": 15,
  "frequencyDays": 7,
  "effort": 3,
  ...
}

Delete Task

Endpoint: DELETE /api/tasks/:id (Admin only)

Response (200):

{
  "success": true
}

Complete Task

Mark a task as completed, earn coins, update streak.

Endpoint: POST /api/tasks/:id/complete

Response (200):

{
  "completion": {
    "id": 123,
    "taskId": 1,
    "userId": 1,
    "completedAt": "2026-02-17T14:30:00.000Z",
    "coinsEarned": 10
  },
  "user": {
    "id": 1,
    "coins": 160,
    "currentStreak": 8,
    "lastActiveDate": "2026-02-17",
    ...
  },
  "newAchievements": [
    {
      "id": "streak_7",
      "name": "On Fire",
      "description": "Complete tasks for 7 days in a row",
      "icon": "🔥"
    }
  ]
}

cURL Example:

curl -X POST http://localhost:3020/api/tasks/1/complete \
  -H "Authorization: Bearer <your-token>"

📊 Dashboard

Get aggregated view: house health, today's quests, upcoming tasks, recent activity, goals, pending rewards.

Endpoint: GET /api/dashboard

Response (200):

{
  "houseHealth": 78,
  "todayQuests": {
    "completed": 5,
    "total": 12
  },
  "nextTasks": [
    {
      "id": 3,
      "name": "Vacuum Living Room",
      "roomName": "Living Room",
      "health": 45,
      "effort": 3,
      "dueIn": "2 hours"
    }
  ],
  "recentActivity": [
    {
      "id": 120,
      "taskName": "Wash Dishes",
      "userName": "John Doe",
      "completedAt": "2026-02-17T14:00:00.000Z",
      "coinsEarned": 10
    }
  ],
  "myGoals": [
    {
      "id": 1,
      "title": "February Goal",
      "goalCoins": 500,
      "currentCoins": 160,
      "progress": 32,
      "startAt": "2026-02-01T00:00:00.000Z",
      "endAt": "2026-02-28T23:59:59.000Z"
    }
  ],
  "pendingRewardRequests": 2
}

cURL Example:

curl -X GET http://localhost:3020/api/dashboard \
  -H "Authorization: Bearer <your-token>"

🏆 Leaderboard

Get family rankings by period.

Endpoint: GET /api/leaderboard?period=<period>

Query Params:

  • period: week, month, quarter, or year

Response (200):

{
  "period": "week",
  "leaderboard": [
    {
      "userId": 1,
      "displayName": "John Doe",
      "avatarColor": "#F97316",
      "avatarType": "letter",
      "avatarPreset": null,
      "avatarPhotoUrl": null,
      "coinsEarned": 150,
      "tasksCompleted": 25,
      "rank": 1
    },
    {
      "userId": 2,
      "displayName": "Jane Doe",
      "coinsEarned": 120,
      "tasksCompleted": 20,
      "rank": 2
    }
  ]
}

cURL Example:

curl -X GET "http://localhost:3020/api/leaderboard?period=week" \
  -H "Authorization: Bearer <your-token>"

📜 History

Get paginated activity log of all task completions.

Endpoint: GET /api/history?limit=<limit>&offset=<offset>

Query Params:

  • limit: Number of records (default: 20)
  • offset: Skip N records (default: 0)

Response (200):

{
  "history": [
    {
      "id": 120,
      "taskName": "Wash Dishes",
      "roomName": "Kitchen",
      "userName": "John Doe",
      "completedAt": "2026-02-17T14:00:00.000Z",
      "coinsEarned": 10
    }
  ],
  "total": 500,
  "limit": 20,
  "offset": 0
}

cURL Example:

curl -X GET "http://localhost:3020/api/history?limit=50&offset=100" \
  -H "Authorization: Bearer <your-token>"

👥 Users

List All Users

Endpoint: GET /api/users (Admin only)

Response (200):

[
  {
    "id": 1,
    "username": "john",
    "displayName": "John Doe",
    "role": "admin",
    "coins": 160,
    "currentStreak": 8,
    ...
  }
]

Create User

Endpoint: POST /api/users (Admin only)

Request Body:

{
  "username": "alice",
  "password": "alicepass",
  "displayName": "Alice",
  "role": "child",
  "avatarColor": "#FFB6C1",
  "language": "en"
}

Response (201):

{
  "id": 3,
  "username": "alice",
  "displayName": "Alice",
  "role": "child",
  ...
}

Update User Profile

Endpoint: PUT /api/users/:id/profile

Request Body:

{
  "displayName": "John Smith",
  "avatarType": "character",
  "avatarPreset": "cat",
  "language": "fr"
}

Response (200):

{
  "id": 1,
  "displayName": "John Smith",
  "avatarType": "character",
  "avatarPreset": "cat",
  ...
}

Upload Avatar Photo

Endpoint: POST /api/users/:id/avatar-upload

Headers:

  • Content-Type: multipart/form-data

Request Body:

FormData:
  - avatar: <file> (image/*, max 2MB)

Response (200):

{
  "id": 1,
  "avatarType": "photo",
  "avatarPhotoUrl": "/api/avatars/user-1-1708176000000.jpg",
  ...
}

cURL Example:

curl -X POST http://localhost:3020/api/users/1/avatar-upload \
  -H "Authorization: Bearer <your-token>" \
  -F "avatar=@/path/to/photo.jpg"

Update User Settings

Endpoint: PUT /api/users/:id/settings (Admin only, self only)

Request Body:

{
  "isVacationMode": true,
  "language": "es"
}

Response (200):

{
  "id": 1,
  "isVacationMode": true,
  "vacationStartDate": "2026-02-17T15:00:00.000Z",
  "language": "es",
  ...
}

Get Coins Configuration

Endpoint: GET /api/users/coins-config

Response (200):

{
  "coinsByEffort": {
    "1": 5,
    "2": 10,
    "3": 15,
    "4": 20,
    "5": 25
  }
}

Update Coins Configuration

Endpoint: PUT /api/users/coins-config (Admin only)

Request Body:

{
  "coinsByEffort": {
    "1": 10,
    "2": 20,
    "3": 30,
    "4": 40,
    "5": 50
  }
}

Or reset to defaults:

{
  "useDefault": true
}

Response (200):

{
  "coinsByEffort": { ... }
}

🎁 Rewards

List Rewards

Endpoint: GET /api/rewards

Response (200):

{
  "rewards": [
    {
      "id": 1,
      "title": "Movie Night Pick",
      "description": "Choose the family movie",
      "costCoins": 40,
      "isActive": true,
      "isPreset": true,
      "createdBy": null,
      "createdAt": "2026-02-10T00:00:00.000Z"
    }
  ],
  "myRedemptions": [
    {
      "id": 10,
      "rewardId": 1,
      "rewardTitle": "Movie Night Pick",
      "costCoins": 40,
      "status": "requested",
      "redeemedAt": "2026-02-17T18:00:00.000Z"
    }
  ]
}

Create Reward

Endpoint: POST /api/rewards (Admin only)

Request Body:

{
  "title": "Extra Dessert",
  "description": "Second serving at dinner",
  "costCoins": 25
}

Response (201):

{
  "id": 8,
  "title": "Extra Dessert",
  "costCoins": 25,
  "isActive": true,
  "createdBy": 1,
  ...
}

Redeem Reward

Endpoint: POST /api/rewards/:id/redeem

Response (200):

{
  "redemption": {
    "id": 12,
    "rewardId": 1,
    "userId": 2,
    "costCoins": 40,
    "status": "requested",
    "redeemedAt": "2026-02-17T19:00:00.000Z"
  },
  "newCoinsBalance": 110
}

Errors:

  • 400: Insufficient coins
  • 404: Reward not found or inactive

Approve/Reject Redemption

Endpoint: PUT /api/rewards/redemptions/:id (Admin only)

Request Body:

{
  "status": "approved"
}

Status options: approved, rejected

Response (200):

{
  "id": 12,
  "status": "approved",
  ...
}

Note: If rejected, coins are refunded to user.


Cancel Redemption

Endpoint: POST /api/rewards/redemptions/:id/cancel (Self only)

Response (200):

{
  "id": 12,
  "status": "cancelled",
  ...
}

Note: Coins are refunded.


🎖️ Achievements

Get Achievements

Endpoint: GET /api/achievements

Response (200):

{
  "myAchievements": [
    {
      "id": "tasks_10",
      "name": "Cleaner",
      "description": "Complete 10 tasks",
      "icon": "✨",
      "unlockedAt": "2026-02-15T10:00:00.000Z"
    }
  ],
  "allAchievements": [
    {
      "id": "tasks_1",
      "name": "Starter",
      "description": "Complete your first task",
      "icon": "🌟",
      "unlocked": true
    },
    {
      "id": "streak_30",
      "name": "Legend",
      "description": "Maintain a 30-day streak",
      "icon": "👑",
      "unlocked": false
    }
  ]
}

Note: allAchievements only returned for admins.

cURL Example:

curl -X GET http://localhost:3020/api/achievements \
  -H "Authorization: Bearer <your-token>"

📤 Data Export/Import

Export Data

Endpoint: GET /api/data/export (Admin only)

Response (200):

{
  "version": "1.0",
  "exportedAt": "2026-02-17T20:00:00.000Z",
  "users": [...],
  "rooms": [...],
  "tasks": [...],
  "taskCompletions": [...],
  "rewards": [...],
  "rewardRedemptions": [...],
  "appSettings": [...]
}

cURL Example:

curl -X GET http://localhost:3020/api/data/export \
  -H "Authorization: Bearer <admin-token>" \
  -o backup.json

Import Data

Endpoint: POST /api/data/import (Admin only)

Request Body: Full JSON export (same structure as export)

Response (200):

{
  "success": true,
  "imported": {
    "users": 3,
    "rooms": 5,
    "tasks": 42,
    "taskCompletions": 250,
    "rewards": 10,
    "rewardRedemptions": 15
  }
}

Warning: This will CLEAR and REPLACE all data.

cURL Example:

curl -X POST http://localhost:3020/api/data/import \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d @backup.json

Error Responses

All errors follow this format:

{
  "error": "Descriptive error message"
}

Common HTTP Status Codes:

Code Meaning Example
400 Bad Request Missing required field
401 Unauthorized Invalid or missing JWT token
403 Forbidden Not admin or not owner
404 Not Found Resource doesn't exist
409 Conflict Username already exists
500 Server Error Database or internal error

🔔 Rate Limiting

Currently no rate limiting is implemented. For production deployments, consider using a reverse proxy (Nginx, Caddy) with rate limiting configured.


📝 Notes

  • All timestamps are in ISO 8601 format (UTC)
  • JWT tokens expire after 30 days
  • Pagination uses limit and offset query parameters
  • File uploads (avatars) limited to 2MB
  • Accepted image types: image/* (MIME type check)

Need help? Open an issue on GitHub Issues