Raxx · internal docs

internal · gated

ADR-0115 — Beta Phase 2: Join Token Design

Status: Accepted Date: 2026-06-12 UTC Deciders: software-architect agent, operator Scope: Raptor (/api/beta/join/*), Antlers (/beta/join/<token>), Console (/internal/api/beta-join/*), CF Access bypass set


Context

Phase 2 beta puts NDA'd testers into the real Raxx app via a token-gated signup. The system must:

  1. Let a single HMAC-signed token serve both Phase 1 (walkthrough activation) and Phase 2 (real account creation) without re-minting a new token type.
  2. Enforce NDA ack before account creation.
  3. Guarantee one account per invite (single-use join semantics).
  4. Keep the pre-launch CF Access gate intact for all non-invited visitors.

The walkthrough token type already exists (cohort='beta', verified via Console). The question is how to add join-specific semantics to it.


Decision

Reuse the existing walkthrough token; add a join_consumed_at column to beta_walkthrough_tokens in Console as the consumption gate.

A new Raptor endpoint (POST /api/beta/join/<token>/claim) verifies the token, enforces NDA and geo-blocks, then atomically sets join_consumed_at via a new Console internal endpoint (POST /internal/api/beta-join/consume). Only after Console confirms consumption does Raptor mint a beta-scoped bootstrap token. That bootstrap token carries a beta_cohort claim that triggers writing users.beta_cohort = 'beta' after passkey enrollment.

This design does not require a new token type, a new Console-to-Raptor auth scheme, or changes to the WebAuthn registration ceremony itself.


Language choice rationale

This ADR governs API contracts and data model changes to existing Tier 2 (Python) services (Raptor and Console). No new service is introduced.

Service: N/A (extending existing services)

Language tier: Tier 2 (Python) — existing services; no new process.


Alternatives considered

Alternative A: New dedicated join-token type

Mint a separate HMAC token with type='join' rather than reusing the walkthrough token.

Rejected because: Testers already hold walkthrough tokens. A separate join token requires a second Console admin action (mint + email), introduces a second token verification path, and complicates the single-token invite email. The reuse approach piggybacks on proven infrastructure.

Alternative B: Mark consumption on the existing revoked_at column

Set revoked_at when the token is consumed as a join token, preventing further walkthrough use.

Rejected because: A Phase 2 tester should still be able to complete the Phase 1 walkthrough activation flow after creating their account. Conflating revocation with consumption removes that ability and breaks the walkthrough if the account creation fails mid-flow.

Alternative C: Raptor-local consumption record

Store the join consumption state in Raptor's own DB (a new beta_join_consumed table) rather than in Console.

Rejected because: Console owns beta_walkthrough_tokens — it is the authoritative record for that token's lifecycle. Splitting ownership between Console and Raptor creates a split-brain risk (double-claim possible if Console and Raptor become inconsistent). The existing HMAC-authenticated internal call pattern keeps truth in one place.

Alternative D: consume-then-certify (ceremony first, consume after)

Run the WebAuthn ceremony before marking the token consumed, so a failed ceremony doesn't strand the tester on a consumed-but-no-account state.

Rejected because: This creates a TOCTOU window where the same token could be presented simultaneously on two devices and both pass the state check. Consuming the token atomically before the ceremony is the correct serialization point; failed ceremonies become support cases (rare, recoverable).


Consequences

Positive

Negative / risks

Neutral


Security / GDPR checklist


Revisit when