Skip to main content

0003 — Shared, flavor-parameterized auth flow

Status: Accepted · Date: 2026-06-09

Context

Both apps (@enode/tracking — the Capacitor Pro app — and @enode/portal — the web Portal) had near-duplicate login pages that only did login via email + OTP. We needed registration, legacy-account migration, and per-deployment access rules (Pro app: Pro-only login; Portal: One + Pro login; registration Pro-only in both), plus the redirect-resolution UI the Portal entirely lacked. The risk was two diverging copies of an increasingly complex flow.

Decision

One flow, parameterized per deployment, split across the existing layers rather than a new package or per-app duplication:

  • The state machine is a headless hook useAuthFlow(config) in @enode/core (next to the existing auth-guard.tsx, the *Loader family, and the translation hooks — @enode/core already ships React intentionally). It owns the step sequence and the API calls and returns a discriminated view-state + action callbacks; it renders nothing.
  • The screens are presentational components in @enode/ui/auth (AuthSteps, the step components, the segmented OtpInput, the redirect dialogs). The redirect/error-resolution plumbing that previously lived only in the tracking app was promoted here so both apps share one implementation.
  • Each app supplies an AuthFlavorConfig (flavor, register license base, whether One may log in, post-auth navigation) and its own decorative chrome.

Sign-in and sign-up are separate flows (no /users/check probe) to avoid duplicate accounts from email typos; legacy-migration routing comes from the OTP response reason instead. Register collects the organization name first.

314 (AppFlavorMismatch) is a blocking disposition, not an action-table.ts redirect — it is terminal (no in-app resolution), so it must not enter the resume/retry loop. The access gate itself is enforced server-side from the api-key subject + device-type; the client only renders the message.

Consequences

  • The flow can't drift between apps — logic and screens are shared; only config and chrome differ.
  • The Portal gains the full redirect UX (310/311/312) and a slim error engine it previously lacked.
  • 314 plugs into the engine as a one-row table change, no new pattern (see ADR 0002 and Error handling).
  • See Authentication for the model and the open items (the exact 311 onboarding endpoint, the 311 tier signal, Capacitor token storage).