API: POST /quest/:id/step/:n/advance
POST /quest/:id/step/:n/advance
Advances an in-progress quest by one step. The URL carries the 1-indexed step number the walker is claiming to have just completed; the server enforces strict ordering against the stored QuestProgress.currentStepId state.
Status: draft contract — implementation in sub-phase 13-5.
Related: GET /quest/available, POST /quest/start, POST /quest/complete, Prisma model QuestProgress, D-016 (quest-complete grants only).
Test-phase mock mode
Per ADR-0006:
- No App Check token requirement.
X-Firebase-AppCheckheader is ignored. - No anti-cheat on step pace. Server trusts the client’s claim it completed the step. Production reconciliation worker is out of scope.
- No step-objective verification. The data layer’s
QuestStepSchema.objectiveis a tag; the server does NOT verify the walker actually performed the objective (e.g. visited the NPC, reached the checkpoint). That is a production-phase concern.
Endpoint
| Field | Value |
|---|---|
| Method | POST |
| Path | /quest/{questId}/step/{stepNumber}/advance |
| Auth | Internal session JWT (Bearer) |
| Idempotent | No — replay returns 422 STEP_OUT_OF_ORDER (the strict-order check guards) |
| Rate limit | 30 req/min/walker |
Path parameters
| Param | Type | Constraints | Notes |
|---|---|---|---|
questId | string | non-empty | Must match an id in data/src/content/quests/. |
stepNumber | integer | 1 ≤ n ≤ quest.steps.length | 1-indexed step the walker is claiming to have completed (matches the wireframe copy “Step 3 of 5”). |
Request
Headers
| Header | Required | Notes |
|---|---|---|
Authorization | yes | Bearer <internal-session-JWT> |
Body
The body is optional. When present, it accepts:
{ "clientStepEcho": 3}| Field | Type | Notes |
|---|---|---|
clientStepEcho | number? | Optional echo of the URL stepNumber for idempotency sanity-check. If present, MUST equal the URL :n. Sanity-checks the mobile client’s URL-construction against its own state. |
Validation order (fail-fast)
- Request body schema valid (if present). → 400
VALIDATION_ERROR clientStepEchomatches URLstepNumber(if echo provided). → 400VALIDATION_ERROR- Quest exists in data layer. → 400
QUEST_NOT_FOUND - Walker exists. → 404
NOT_FOUND(internal) - QuestProgress row exists for
(walkerId, questId). → 422QUEST_NOT_IN_PROGRESS - Quest is not already completed. → 409
QUEST_ALREADY_COMPLETED stepNumberin range [1,totalSteps]. → 422STEP_OUT_OF_RANGEstepNumberequals walker’s current step number (1-indexed). → 422STEP_OUT_OF_ORDER- Persist + return 200.
Responses
200 OK
{ "questId": "quest.001-first-road", "currentStepIndex": 3, "totalSteps": 5, "stepCompleted": 3, "isFinalStep": false, "nextStepHint": "delivery:pani-sczepiel-envelope"}| Field | Type | Notes |
|---|---|---|
questId | string | Echo of the URL :questId. |
currentStepIndex | number | 0-indexed step the walker is NOW on (after advancing). On final-step advance, stays at totalSteps - 1 — the walker is still on the last step; quest is NOT auto-completed. |
totalSteps | number | quest.steps.length. |
stepCompleted | number | 1-indexed step that was just completed (echo of URL :stepNumber). |
isFinalStep | boolean | True if the just-completed step was the final step. Client surfaces “Complete Quest” CTA on true and MUST call POST /quest/complete to grant the D-016 +1 tree point. |
nextStepHint | string | null | Machine-readable beat label for the next step, sourced from QuestStepSchema.objective (e.g. "checkpoint:domek-mostkowy-bridge"). null on final-step advance. |
400 BAD_REQUEST
VALIDATION_ERROR (body schema invalid OR clientStepEcho does not match URL stepNumber) OR QUEST_NOT_FOUND.
401 UNAUTHORIZED
Missing or invalid Bearer token.
404 NOT_FOUND
Walker not found for the authenticated user.
409 CONFLICT
QUEST_ALREADY_COMPLETED — quest is already marked completed; further step advances are rejected. Client should refresh state via GET /quest/available.
422 UNPROCESSABLE_ENTITY
QUEST_NOT_IN_PROGRESS— noQuestProgressrow exists for(walkerId, questId). Client must callPOST /quest/startfirst.STEP_OUT_OF_ORDER—stepNumberdoes not equal walker’s current 1-indexed step number. Strict order is enforced (no skipping, no replay).STEP_OUT_OF_RANGE—stepNumber < 1orstepNumber > totalSteps.
Details payload for STEP_OUT_OF_ORDER:
{ "error": "STEP_OUT_OF_ORDER", "message": "Walker is at step 1, cannot advance step 3.", "details": { "questId": "quest.001-first-road", "requestedStepNumber": 3, "currentStepNumber": 1, "totalSteps": 5 }}Semantics
D-016 separation of step grant from quest grant
The final step advance does NOT auto-complete the quest. The client MUST call POST /quest/complete to receive the +1 tree point. This separation is HARD per D-016:
Per-quest grant: flat 1 tree point on quest completion. Per-step beats grant faction reputation + lore unlocks; no tree points per step.
Rationale:
- Tree points are quest-complete grants only. Per-step grants would change the math (D-016’s ~144 points across ~180 nodes), break the build-identity design intent, and dissonate with D-013’s “linijka oddana do składu” framing (a step is an editorial commit, not an achievement).
- The mobile UI on the final-step advance surfaces a “Complete Quest” CTA per the 11e-2 wireframe (§3.3 worked example “Step 5 of 5” + outcome card).
- Auto-completion would couple step-state to grant-state, making it harder to add quest outcomes that branch on the final beat (Quest 002 already has three possible outcomes at beat 5; the explicit completion call carries the outcome choice in a future schema extension).
Strict order
stepNumber must equal currentStepNumber (1-indexed). The strict order matches the wireframe’s “current step + future steps locked” visual model (11e-2 §3.3). Skipping or replaying steps would require a re-design of the quest detail panel.
Per-step side effects (forward-compatible)
The endpoint is wired to fire per-step faction-rep deltas and lore unlocks when the data layer surfaces step-level rewards. Currently QuestStepSchema carries only id, description, and objective — no per-step rewards. When the schema extends with rewards?: { reputation?, lore? }, the hook activates automatically:
// quest.service.ts advanceStep() — pseudo-codeif (questData.steps[idx].rewards?.reputation) { await this.factionRepService.applyDelta(walkerId, delta, tx);}Per D-016 (HARD): NO tree points are awarded per step, regardless of what schema extensions may add. Tree points are quest-complete only.
Mobile contract reference
mobile-developer in sub-phase 13-5 wires the step-advance flow:
- On beat completion (e.g. arrived at a checkpoint, finished a dialogue) → call
POST /quest/:id/step/:n/advancewithn = currentStepIndex + 1. - On 422
STEP_OUT_OF_ORDER→ refresh quest state viaGET /quest/available(server is the source of truth). - On
isFinalStep: true→ render “Complete Quest” CTA; on tap, callPOST /quest/complete. - On
isFinalStep: false→ update progress bar(stepCompleted) / totalSteps, advance to the next step’s prose in the detail screen.
Offline queue (D-009 §2)
Mobile queues advance requests when offline (7-day cap per D-009 §2). On reconnect:
- Replay queued advances in original order.
- If a replay returns
STEP_OUT_OF_ORDER, the queue is corrupted — refresh fromGET /quest/availableand drop the queue. - If a replay returns
QUEST_ALREADY_COMPLETED, the queue is stale (server already completed the quest somehow — should not happen in single-device case) — drop the queue.
Forward compatibility
- New optional fields on the response are additive; clients should ignore unknown fields.
- The
nextStepHintfield will gain a structured{ objective, prose? }shape if step descriptions are added to the runtime adapter; current string shape is a forward-compatible subset.
Open follow-ups
- Per-step rewards schema extension — mechanics-designer (B-level if just adding the field; D-level if it changes the quest grant math).
- Step-objective verification — production reconciliation worker concern; out of scope for Phase 13.
- Outcome branching at final step (Quest 002 has three beat-5 outcomes) — schema extension + endpoint extension. Tracked as a follow-up to the 11c-2 tier system.