Skip to content

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-AppCheck header 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.objective is 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

FieldValue
MethodPOST
Path/quest/{questId}/step/{stepNumber}/advance
AuthInternal session JWT (Bearer)
IdempotentNo — replay returns 422 STEP_OUT_OF_ORDER (the strict-order check guards)
Rate limit30 req/min/walker

Path parameters

ParamTypeConstraintsNotes
questIdstringnon-emptyMust match an id in data/src/content/quests/.
stepNumberinteger1 ≤ n ≤ quest.steps.length1-indexed step the walker is claiming to have completed (matches the wireframe copy “Step 3 of 5”).

Request

Headers

HeaderRequiredNotes
AuthorizationyesBearer <internal-session-JWT>

Body

The body is optional. When present, it accepts:

{
"clientStepEcho": 3
}
FieldTypeNotes
clientStepEchonumber?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)

  1. Request body schema valid (if present). → 400 VALIDATION_ERROR
  2. clientStepEcho matches URL stepNumber (if echo provided). → 400 VALIDATION_ERROR
  3. Quest exists in data layer. → 400 QUEST_NOT_FOUND
  4. Walker exists. → 404 NOT_FOUND (internal)
  5. QuestProgress row exists for (walkerId, questId). → 422 QUEST_NOT_IN_PROGRESS
  6. Quest is not already completed. → 409 QUEST_ALREADY_COMPLETED
  7. stepNumber in range [1, totalSteps]. → 422 STEP_OUT_OF_RANGE
  8. stepNumber equals walker’s current step number (1-indexed). → 422 STEP_OUT_OF_ORDER
  9. Persist + return 200.

Responses

200 OK

{
"questId": "quest.001-first-road",
"currentStepIndex": 3,
"totalSteps": 5,
"stepCompleted": 3,
"isFinalStep": false,
"nextStepHint": "delivery:pani-sczepiel-envelope"
}
FieldTypeNotes
questIdstringEcho of the URL :questId.
currentStepIndexnumber0-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.
totalStepsnumberquest.steps.length.
stepCompletednumber1-indexed step that was just completed (echo of URL :stepNumber).
isFinalStepbooleanTrue 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.
nextStepHintstring | nullMachine-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 — no QuestProgress row exists for (walkerId, questId). Client must call POST /quest/start first.
  • STEP_OUT_OF_ORDERstepNumber does not equal walker’s current 1-indexed step number. Strict order is enforced (no skipping, no replay).
  • STEP_OUT_OF_RANGEstepNumber < 1 or stepNumber > 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-code
if (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/advance with n = currentStepIndex + 1.
  • On 422 STEP_OUT_OF_ORDER → refresh quest state via GET /quest/available (server is the source of truth).
  • On isFinalStep: true → render “Complete Quest” CTA; on tap, call POST /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 from GET /quest/available and 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 nextStepHint field 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.