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.
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
- Rooms
- Tasks
- Dashboard
- Leaderboard
- History
- Users
- Rewards
- Achievements
- Data Export/Import
- Error Responses
🔐 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, oryear
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 coins404: 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
limitandoffsetquery parameters - File uploads (avatars) limited to 2MB
- Accepted image types:
image/*(MIME type check)
Need help? Open an issue on GitHub Issues