Gamified TaskFlow: managers post tasks with a credit reward to a market, workers claim and finish them, and managers approve the work to pay out credits and XP. Workers spend credits in their manager's rewards store or to clear penalties.
All routes live under /api. Every route needs Authorization: Bearer <accessToken>
except /auth/* and GET /users/managers. POST /restore/hourly instead requires the server's scheduler secret (not a user JWT).
-
Bodies and responses are JSON. Ids are MongoDB ObjectId strings in
_id. -
Errors are always
{ "message": "..." }with a status code:Status Meaning 400 Invalid input: validation failure, malformed id, bad query value 401 Missing, invalid or expired token; wrong login 403 Wrong role ( Only managers can do this/Only workers can do this) or not your resource404 Resource not found (or not in your team) 409 Not allowed in the current state: task not open/in review, already approved/cleared/fulfilled, Insufficient credits,This reward is out of stock, duplicate email -
User objects never contain
password.
GET /restore: authenticated capability check,{ "canRestore": true|false }; never mutates data.POST /restore: requires one of the four documented demo accounts' JWTs and exactly the JSON body{ "confirm": true }. Account/email/ID selectors are rejected. Restores all four demo accounts' dataset while preserving existing passwords, IDs and sessions.POST /restore/hourly: requiresAuthorization: Bearer <DEMO_RESTORE_SECRET>. Checks at most once per hour across all instances; calls the same restoration service as manual restore andnpm run seed. No user selection.
Success is { "restored": true, "reason": "restored" }, { "restored": false, "reason": "unchanged" }, or (scheduler only) { "restored": false, "reason": "not-due" }. Responses are not cacheable. Concurrent demo-affecting writes/restores return 409 while the dataset is busy. Mixed real/demo ownership also returns 409 without application writes rather than damaging shared data. See the README's hourly restoration section for scheduler setup and legacy sprint preservation.
roleismanagerorworker(defaultworker).- A worker's
reportsTois their manager. A manager's team is every user whosereportsTois that manager. - Workers see the market, store and leaderboard of the manager they report to. A worker without a manager sees empty lists.
- Credits are paid only when the posting manager approves a task: the
assignee earns exactly the task's
reward, once. - XP gained = task reward. XP never goes down (spending credits does not cost XP).
- Level =
floor(sqrt(xp / 100)) + 1, so level n needs100 * (n - 1)^2XP: level 2 at 100 XP, level 3 at 400, level 4 at 900.nextLevelXpin profiles is the XP needed for the next level. - Streak = consecutive calendar days (UTC) with at least one approved task.
Another approval the same day keeps it, one the next day extends it, a gap resets it
to 1. A streak whose last approval is older than yesterday is reported as
0. - Failed writes are rolled back. If a payout fails partway, the ledger entry is
removed and the task goes back to
in review, so the manager can approve again. A failed redemption or penalty clear refunds the credits and restores the stock or penalty. Concurrent approvals for one worker apply their XP, level and streak updates one after another, so none is lost. - Balances never go negative. Every debit is one conditional atomic update
(
credits >= cost). The ledger has one entry per task, redemption and penalty (uniquekind+ref), so nothing is paid or charged twice.
| Model | Fields |
|---|---|
| User | name, email, role, reportsTo, credits (cached balance), xp, level, streak, lastApprovedAt, created_at |
| Task | title (1-50 chars), type (bug/feature/story), description, reward (integer >= 0), createdBy, assignee, status, submissionNote, reviewNote, claimedAt, submittedAt, approvedAt, created_at, updated_at |
| CreditTransaction | user, amount (signed integer), kind (task_reward/reward_redeem/penalty_clear), ref (Task/Redemption/Penalty id), note, createdAt |
| Penalty | worker, issuedBy, reason, cost, status (active/cleared), clearedAt, createdAt, updatedAt |
| Reward | title, description, cost, stock (null = unlimited), createdBy, active, createdAt, updatedAt |
| Redemption | worker, reward, rewardTitle (snapshot), manager, cost, status (pending/fulfilled), fulfilledAt, createdAt, updatedAt |
open --claim--> in progress --submit--> in review --approve--> done (credits + XP paid)
^ |
+----reject (note)------+
Market tasks are the ones with a createdBy. Their status and assignee change only
through the lifecycle endpoints; only the posting manager may edit or delete them, and
they can only be deleted, and their reward changed, while the task is open
(otherwise 409), so a worker's claimed or in-review work never vanishes. Older tasks
without createdBy keep the legacy statuses (not started, in progress, completed,
closed) and the legacy board/sprint endpoints; they never appear in the market.
Body: { name, email, password, role?, reportsTo? }. role defaults to worker;
a worker's reportsTo must be a manager id (optional). Managers report to no one.
Credits/XP and other stats cannot be set. 201:
{
"token": { "tokenType": "Bearer", "accessToken": "...", "refreshToken": "...", "expiresIn": "..." },
"user": { "_id": "...", "name": "...", "email": "...", "role": "worker", "reportsTo": "...", "credits": 0, "xp": 0, "level": 1, "streak": 0 }
}Body: { email, password }. 200 with the same shape as register. Wrong credentials: 401.
Body: { email, refreshToken }. 200 with a new token object.
Managers for the signup picker: [{ "_id": "...", "name": "Rohit Sharma" }].
{
"_id": "...", "name": "Asha Verma", "email": "asha@taskflow.demo",
"role": "worker", "reportsTo": "...",
"credits": 60, "xp": 360, "level": 2, "nextLevelXp": 400,
"streak": 2, "lastApprovedAt": "...", "created_at": "...",
"manager": { "_id": "...", "name": "Rohit Sharma", "email": "rohit@taskflow.demo" }
}manager is null for managers and unassigned workers.
The manager's reports: each is the /me profile (without manager) plus
tasksDone, tasksInProgress, tasksInReview (tasks from this manager) and
activePenalties.
The older GET /users, GET /users/search?name=, GET /users/:id, POST /users,
PUT /users/:id and DELETE /users/:id remain. The reads only see your team (your
manager and everyone reporting to them; a worker with no manager sees only
themselves): other users are left out of lists and searches and GET /users/:id
returns 404 for them. GET /users/search returns up to 5 matches by name or email
with name and email only. PUT/DELETE only work on your own account (403
otherwise).
Open tasks of your team's manager (a manager sees their own open tasks), newest
first, with createdBy populated (name, email, role).
Body: { title, type?, description?, reward }. Creates an open market task owned by
you. 201 with the task.
Takes an open task from your manager's market: status -> in progress,
assignee -> you. 403 if the task is from another manager; 409 if it is no longer
open (only one concurrent claim wins).
Body: { note? }. in progress -> in review. 409 from any other status.
in review -> done, sets approvedAt, pays the reward to the assignee
(credits, XP, level, streak) and writes a task_reward ledger entry. A second
approve gets 409 Task is not awaiting review and pays nothing.
Body: { note } (required, 400 without it). in review -> in progress with
reviewNote set. The note is cleared on approval.
Workers: tasks assigned to you. Managers: tasks you posted. Any status, most recently
updated first, assignee and createdBy populated.
Your tasks in in review, oldest submission first, assignee populated.
GET /tasks, GET /tasks/search?name=, GET /tasks/:id, GET /tasks/user/:id,
PATCH /tasks/:id, POST /tasks/status/:id, POST /tasks/type/:id,
POST /tasks/assignee/:userId/:taskId, DELETE /tasks/:id and all /sprints
routes keep working (with a JWT). They are scoped to your team: you see your team
manager's market tasks plus legacy tasks (no createdBy) assigned to someone in your
team. Any other task is left out of lists and searches, and reading or changing it by
id returns 404. POST /tasks/assignee/... and an assignee sent to PATCH /tasks/:id
must be a member of your team, otherwise 404.
For market tasks: status/assignee changes there return 409, and edit/delete is
limited to the posting manager (403), and delete or a reward change returns 409
once the task has left open.
{
"balance": 60, "earned": 360, "spent": 300,
"transactions": [
{ "_id": "...", "user": "...", "amount": -300, "kind": "reward_redeem", "ref": "<redemption id>", "note": "Redeemed: Event ticket: DevFest 2026", "createdAt": "..." },
{ "_id": "...", "user": "...", "amount": 120, "kind": "task_reward", "ref": "<task id>", "note": "Task approved: Dark mode for dashboard", "createdAt": "..." }
]
}Newest first.
Workers: your manager's active rewards. Managers: every reward you stock (including inactive). Cheapest first.
Body: { title, description?, cost, stock?, active? }. stock omitted or null
means unlimited. 201 with the reward.
Any of title, description, cost, stock, active. 200 with the reward.
Deletes the item; past redemptions keep rewardTitle. 200 with the deleted reward.
(To hide an item but keep it, PATCH { "active": false }.)
Buys one unit from your manager's store: takes one unit of stock (if limited) and
cost credits, writes a reward_redeem ledger entry and creates a pending
redemption. 201 with the redemption. 409 Insufficient credits /
409 This reward is out of stock change nothing; 403 for another manager's store;
404 if missing or inactive.
Workers: your redemptions. Managers: redemptions of your rewards. Newest first,
worker (name, email) and reward (title, cost) populated (reward is null
if deleted; use rewardTitle).
pending -> fulfilled, sets fulfilledAt. 409 if already fulfilled.
Body: { worker, reason, cost }. worker must be in your team (404 otherwise).
201 with an active penalty.
Workers: your penalties. Managers: penalties you issued. Optional ?status=active|cleared.
Newest first, worker and issuedBy populated.
Pays cost credits: active -> cleared, sets clearedAt, writes a
penalty_clear ledger entry. 409 Insufficient credits keeps it active;
409 Penalty is already cleared charges nothing.
Ranks your team (your manager's reports, or a manager's own reports) by credits earned
from approved tasks: all (default) is all-time, week is the last 7 days.
Ties break by XP, then name. 400 for any other period.
{
"period": "week",
"entries": [
{ "rank": 1, "user": { "_id": "...", "name": "Asha Verma" }, "points": 210, "tasksDone": 2, "xp": 360, "level": 2, "streak": 2 }
]
}