Raxx · internal docs

internal · gated

ADR-0114: WCB Tier Gate and User Ownership via Queue JWT

Status: Accepted Date: 2026-06-05 UTC Deciders: software-architect agent Scope: WCB feature (Raptor backend_v2/); tier-gate on GET /api/positions/<id>/what-could-have-been and PATCH /api/account/settings/wcb Refs: #1659, #1657; parent design doc docs/architecture/wcb-2026-06-05.md Supersedes: Tier-gate section of docs/architecture/what-couldve-been-design.md §7.2 (2026-05-12), which assumed Raptor-local session tier


Context

The original WCB design (PR #1787, 2026-05-12) assumed Raptor would read the user's subscription tier from a locally cached session (JWT claims set at login via Raptor's own auth layer). This assumption was correct at the time: Queue was not yet the JWT issuer.

Since then, project_queue_owns_customer_timeline has been locked: Queue is the single source of truth for customer records and subscription tier. Queue now issues the JWT that clients present to Raptor. The WCB tier gate must be updated to read tier from the Queue-issued JWT, not from a Raptor-local cache.

Two concrete questions this ADR resolves:

  1. Should WCB call Queue's customer API at request time to verify tier, or should it read from the JWT claim?
  2. What happens when the JWT is missing the tier claim?

Decision

WCB reads tier from the tier claim in the Queue-issued JWT. No additional Queue API call is made at WCB request time. The JWT is the authoritative tier signal.

If the JWT is missing the tier claim, WCB fails closed: returns 403 tier_insufficient. It never assumes free and never fails open.

The user_id used for ownership checks in WCB queries is the sub claim from the Queue-issued JWT. No separate user-lookup call to Queue is made.


Language choice rationale

This ADR governs a feature that extends an existing Tier 2 Python service (Raptor / backend_v2/). WCB is not a new service; it is a new endpoint and scheduled task in Raptor. No language classification is required for this ADR.


Consequences

Positive

Negative / risks

Neutral


Alternatives considered

Alternative A: Call Queue's customer API at WCB request time

At each WCB request, Raptor calls GET /api/v1/customers/{user_id} on Queue to retrieve the current tier.

Rejected because: adds a synchronous service-to-service call on the WCB hot path, increasing latency by 20–100 ms (intra-Heroku latency). The WCB endpoint is already potentially slow (bar data fetch + P&L computation without Redis). Adding a Queue call makes the p99 worse. The JWT TTL model provides adequate freshness for a display feature with no execution consequences.

Alternative B: Maintain a local tier cache in Raptor with a TTL

Raptor caches the tier in Redis (or in-process) keyed by user_id, refreshed on each JWT refresh or on a fixed TTL.

Rejected because: introduces a third source of tier truth (Queue DB, Queue JWT, Raptor cache). Cache invalidation on downgrade requires either Queue pushing an event to Raptor or Raptor polling Queue — both are more complex than reading from the JWT, which Queue already keeps fresh via its JWT refresh cycle. The JWT TTL is the right invalidation boundary for this use case.


Security / GDPR checklist


Revisit when