Skip to content

API: POST /walker/class

POST /walker/class

Completes the onboarding class-pick flow. The Walker is auto-created with classId=null at /auth/callback time (ADR-0006 §Mock auth detail); this endpoint sets the class so the mobile client can skip the class-pick screen on subsequent launches.

Status: draft contract — implementation in sub-phase 13-3 (2026-05-20). Related: D-016 (progression spec, ratified 2026-05-20), ADR-0006 (mock-auth Walker auto-create), ADR-0007 (Android network layer + DTO contract), GET /walker/profile, POST /auth/callback.

Endpoint

FieldValue
MethodPOST
Path/walker/class
AuthInternal session JWT (Bearer)
IdempotentYes (re-submitting the same classId is a no-op)
Rate limit12 req/min/walker (onboarding flow only)

Design choice — why mutate, not create

Two shapes were considered for class-pick:

  1. Option (a): /auth/callback creates only User (not Walker). Then POST /walker/create { classId } creates the Walker.
  2. Option (b) — RATIFIED: /auth/callback continues to auto-create Walker with classId=null (per ADR-0006 §5.a-5.d). POST /walker/class { classId } sets the class on the existing Walker.

Option (b) chosen because:

  • ADR-0006 §“Mock auth detail” Server-flow step 1 is load-bearing: “Look up User by email. If missing, create User + Walker + StreakState + FactionRep rows exactly as the production callback does (sections 5.a-5.d of auth-callback.mdx).” Switching to option (a) would mean re-authoring the production /auth/callback contract too — ADR-0006’s whole point is that the contract is the migration vehicle.
  • ADR-0007 §2 ships a Kotlin WalkerSummary DTO that the mobile client already deserializes from /auth/callback. Option (a) would null this out at register time, breaking the contract.
  • ADR-0006 also creates StreakState and 5x FactionRep rows in the same transaction. Splitting Walker creation off would either duplicate those rows in the new endpoint, or leave them dangling without a Walker FK target. Option (b) avoids both.

The mobile flow per option (b):

sign in -> /auth/callback (returns walker.classId=null)
-> mobile renders class-pick screen
-> user picks Cartographer
-> POST /walker/class { classId: "class.cartographer" }
-> returns full WalkerProfile (same shape as GET /walker/profile)
-> mobile lands on home screen

Subsequent app launches: GET /walker/profile returns classId != null, mobile skips the class-pick screen.

Request

Headers

HeaderRequiredNotes
AuthorizationyesBearer <internal-session-JWT>
Content-Typeyesapplication/json
X-Request-IdoptionalUUID v4. Server echoes in logs and error envelope. Per ADR-0007 §6 client-generated.

Body

{
"classId": "class.cartographer"
}
FieldTypeRequiredNotes
classIdstringyesReferences data/src/content/classes/ by id. Server validates against the data layer; unknown class ids return 422 INVALID_CLASS_ID.

Response

200 OK

Returns the full WalkerProfileResponse (same shape as GET /walker/profile). The mobile client lands on the home screen in a single round-trip.

{
"walker": {
"id": "01HM4...",
"displayName": "Wanderer-9f3a2c",
"level": 0,
"classId": "class.cartographer",
"totalLifetimeSteps": 0,
"treePointsBanked": 0,
"treePointsSpent": 0,
"currentRegionId": "region.plenny",
"createdAt": "2026-05-20T08:00:00Z",
"lastActiveAt": "2026-05-20T08:14:22Z"
},
"region": { /* ... */ },
"streak": { /* ... */ },
"activeQuests": [],
"factionRanks": [ /* 5 entries, neutral tier 0 */ ],
"subscription": { "tier": "none", "validUntil": null },
"flags": {
"isFirstLogin": false,
"hasPendingDelete": false,
"isInQuarantine": false,
"appUpgradeAvailable": null
}
}

Idempotent re-pick

If the walker already has the requested classId set, the server returns 200 with the current profile and does NOT touch the DB. Safe to call from a class-pick screen on every mount.

400 Bad Request

{
"error": "VALIDATION_ERROR",
"message": "Request body failed schema validation.",
"details": { /* Zod flatten output */ }
}

Body did not parse against SetClassRequestSchema (e.g. missing classId, wrong type, empty string).

401 Unauthorized

Session JWT invalid or missing.

404 Not Found

{
"error": "WALKER_NOT_FOUND",
"message": "Walker not found for the authenticated user."
}

Defensive — should be impossible under ADR-0006 because /auth/callback lazy-creates the Walker. Surfaced for forward-compatibility with the production-mode auth path where Walker creation is delegated to a separate worker.

409 Conflict

{
"error": "CLASS_ALREADY_SET",
"message": "Walker already has class 'class.X'; cannot change to 'class.Y'.",
"details": {
"currentClassId": "class.X",
"requestedClassId": "class.Y"
}
}

Walker already has a different non-null classId. Class re-pick (respec) is out of Phase 13 scope; a future “respec class” feature is tracked as a B-level decision for post-vertical-slice.

422 Unprocessable Entity

{
"error": "INVALID_CLASS_ID",
"message": "Class 'class.nonexistent-xyz' is not in the data layer.",
"details": {
"classId": "class.nonexistent-xyz",
"knownClassIds": ["class.cartographer"]
}
}

classId is not in data/src/content/classes/. knownClassIds returns the current authored set so the mobile client can pin against drift between the bundled class data and the server-side data layer.

Class id validation

Server-side validation against backend/src/common/game-content.ts classes array (the backend’s runtime adapter for data/src/content/classes/). The adapter is maintained in sync with the data layer manually until the ESM/CJS bundler resolution lands (see common/game-content.ts header). Phase 13-3 ships the Cartographer; subsequent classes append to both the data layer instance set AND the backend adapter.

Telemetry

info-level log with: walkerId, classId, idempotent (true if walker already had this classId), latencyMs. No PII.

Open follow-ups

  • Class re-pick (respec) feature. Post-Phase-13 product question. If ratified, this endpoint accepts a force: true flag and returns 200 even when classId is already set. Currently 409.
  • Faction-allegiance side effect. Classes may eventually carry a starter faction allegiance (e.g. Cartographer → faction.unfinished-guild +20 starting reputation). Phase 13-3 does NOT mutate FactionRep on class-pick — all 5 factions stay at tier 0 / rep 0 from /auth/callback. A future B-level decision wires class-bound starter rep.
  • currentRegionId retention. The walker’s currentRegionId is set to region.plenny at /auth/callback. class.startRegionId (class.cartographer.startRegionId = "region.plenny") is informational only; the endpoint does NOT update currentRegionId based on class. If a future non-Plenny-starter class is authored, this contract amends to set currentRegionId = class.startRegionId on first class-pick.