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 existingauth-guard.tsx, the*Loaderfamily, and the translation hooks —@enode/corealready 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 segmentedOtpInput, 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).