RBAC Antlers Surface Audit — Flag Inventory, Gap Report, and Denied-State Remediation
Status: Draft — awaiting operator review
Owner: software-architect
Date: 2026-05-14 UTC
Refs: #1449 (this card), #81 (SDLC epic), #970–#974 (RBAC phase cards), docs/architecture/rbac-design.md, docs/architecture/auth-unification-rbac-reconciliation.md
1. Context
Raxx today has no RBAC enforcement on Antlers (the customer-facing app). Every feature flag that controls a customer-facing Antlers surface is a global binary: ON for all authenticated users, or OFF for all authenticated users. As the platform moves toward a first external customer on 2026-05-23 UTC, three problems must be addressed before or shortly after launch:
- Global flags cannot express per-role access. The onboarding wizard, route guard, and passkey flows should be gated on authenticated
customerrole, not a global flag. - The "Coming soon" pattern is live in Settings. Two flag-backed surfaces (
options_backtest,live_mode_ring) display a visible "Coming soon" badge when the flag is OFF. This violates the locked invariant (2026-05-11 UTC): denied surfaces must be hidden, not grayed, badged, or "upgrade-to-unlock" framed. - No permission name exists for any Antlers surface. The role taxonomy in
rbac-design.mddefinesantlers-user,antlers-founders,antlers-protiers but no per-feature permission names. RBAC-aware flag resolution is Phase 2; this audit establishes the recommended permission names before Phase 2 implementation.
This document does not ship code. It is a pre-condition for dispatching implementation sub-cards.
2. Invariants (non-negotiable)
The following constraints govern every recommendation in this document:
- No single-user direct access. Permissions flow
user → group → role → permission. A check against a bare user ID must not exist. - Denied Antlers surfaces are hidden, not grayed. A surface the user's role does not grant is simply absent from the UI. No badge, no "Coming soon," no disabled button with a tooltip. (Memory anchor:
feedback_hide_dont_gray_unavailable_features, locked 2026-05-11 UTC.) - Superadmin is break-glass only. No feature access flows through superadmin as a working gate.
- Paper-first gating applies to all live-execution surfaces. Any flag that touches order entry or live-mode state must respect the paper-profitable-for-N-cycles gate.
- No stored credentials. Nothing in this model may store or replay an auth credential.
- GDPR by default. PII fields implied by role metadata follow the same retention and erasure rules as the rest of the platform.
3. Flag Inventory — Antlers Customer-Facing Surfaces
Scope: All flags where surface: antlers in feature_flags.yaml, plus flags on other surfaces (raptor, status-page) where the flag directly gates a customer-visible Antlers UI behavior. Console, Velvet, Queue, and CI flags are out of scope.
Total flags in file: 131 (as of 2026-05-14 UTC)
Flags with surface: antlers: 31
Flags on other surfaces with direct Antlers UI impact: 4 (noted below)
Flags audited: 35
3.1 Auth and Session Surfaces
| Flag | Surface / Route | Current Gate | Recommended Gate | Denied-State Treatment | Priority |
|---|---|---|---|---|---|
route_guard |
RouteGuard in App.js — enforces session-aware redirect table across all authenticated routes |
Global ON/OFF | Role: any authenticated session (antlers-user or above) — guard must fire on session presence, not flag presence. Role check is the auth session itself; no separate RBAC permission needed. |
N/A — guard is infrastructure, not a feature. Flag gates whether the guard runs at all. When OFF, routes are ungated. Must be ON for v1 launch. | v1 blocker |
passkey_signup_ui |
/signup — 5-step passkey enrollment + backup codes |
Global ON/OFF | Role: unauthenticated visitor (public route). No role gate needed. Flag gates route registration. | When OFF: route is not registered; /signup returns the catch-all redirect. Hidden by absence. |
v1 blocker |
passkey_login_ui |
/login — passkey assertion flow |
Global ON/OFF | Role: unauthenticated visitor (public route). No role gate needed. | When OFF: route is not registered. Hidden by absence. | v1 blocker |
email_verification_ui |
/verify-email/pending + /verify-email/confirm |
Global ON/OFF | Role: any authenticated or pre-auth session (public routes). No role gate needed. | When OFF: routes not registered. Hidden by absence. | v1 blocker |
onboarding_wizard_v1 |
/onboarding/* — multi-step onboarding wizard |
Global ON/OFF | Role: antlers-user (any authenticated new user). The wizard should not appear for users who have completed onboarding. Session state (onboarding_completed) is the gate, not a permission. Flag gates route registration; session state gates rendering. |
When OFF: route tree not registered; redirect to /setup or /dashboard. Hidden by absence. When ON but user already onboarded: redirect to /dashboard. |
v1 blocker |
quebec_geoblock |
EmailFormStep + POST /api/marketing/validate-region — blocks QC signups |
Global ON/OFF (default OFF, must flip before launch) | Role: N/A — geographic gate, not an RBAC gate. Remains global. | When flag ON + QC jurisdiction detected: waitlist card renders in place of signup form. When flag OFF: no province check. | v1 blocker |
signup_geoblock_eu |
POST /api/auth/register/options — blocks EU/EEA+CH signups |
Global ON/OFF (default ON) | Role: N/A — geographic gate, not an RBAC gate. Remains global. | When ON + EU/EEA detected: block card renders. | v1 blocker |
3.2 App Shell and Navigation Surfaces
| Flag | Surface / Route | Current Gate | Recommended Gate | Denied-State Treatment | Priority |
|---|---|---|---|---|---|
raxx_app_shell |
RaxxAppShell — full-bleed chrome wrapping all authenticated routes |
Global ON/OFF | Role: any authenticated session (antlers-user or above). Shell is infrastructure; not per-feature. No separate permission. |
When OFF: legacy Bootstrap shell renders instead. No user-visible "denied" state — fallback is another shell. | post-launch |
antlers_visual_port_v1 |
/dashboard → DashboardCE (Confidence Engine layout) |
Global ON/OFF | Role: any authenticated session. Global flag; no per-role branching needed. | When OFF: Dashboard.js renders instead. No denied state. |
post-launch |
help_drawer_v1 |
? icon in header tray → slide-over help drawer |
Global ON/OFF | Role: any authenticated session (antlers-user or above). No restriction needed. |
When OFF: ? icon absent from header tray. Hidden by absence. |
post-launch |
help_icon_v21 |
Replaces "Help" text button with moss-tinted ? tray icon |
Global ON/OFF | Depends on help_drawer_v1. Same role gate. |
When OFF: v2.0 icon renders (or nothing if help_drawer_v1 is also OFF). |
post-launch |
live_mode_ring |
LiveModeRing — breathing ring overlay when live trading is active |
Global ON/OFF | Role: any authenticated session with live-trading access. When ring is OFF, it should not appear — but current Settings page shows it as "Coming soon" (gray badge) regardless of flag state. DENIED-STATE GAP — see §5. | When OFF: ring overlay must not render AND the Settings → Sentinel ring nav item and badge must be hidden. | v1 blocker (denied-state gap must be remediated before launch) |
3.3 Trading Surfaces
| Flag | Surface / Route | Current Gate | Recommended Gate | Denied-State Treatment | Priority |
|---|---|---|---|---|---|
trade_window_v1 |
/trade — TradeWindow page (symbol search, order entry, positions) |
Global ON/OFF | Role: antlers-user minimum. Paper-first gate also applies — live order path is gated by paper-profitable-for-N-cycles invariant regardless of RBAC. |
When OFF: TradeWindow returns null (component already does this via isEnabled check). Route link in Header is conditionally rendered via isEnabled('trade_window_v1'). When OFF, link and page are both absent. Currently correct. |
post-launch |
3.4 Backtesting Surfaces
| Flag | Surface / Route | Current Gate | Recommended Gate | Denied-State Treatment | Priority |
|---|---|---|---|---|---|
options_backtest |
Settings → Platform Features tab — badge shows "Coming soon" when OFF | Global ON/OFF | Role: N/A — options_backtest is a backend licensing gate (data licensing in progress per #244). Current badge display is the denied-state violation. DENIED-STATE GAP — see §5. |
When OFF: the entire "Options Backtesting" row in Settings → Platform Features must be hidden, not badged "Coming soon." Route and endpoint remain gated via enable_options_backtest (backend). |
v1 blocker (denied-state gap) |
advanced_risk_metrics |
Backtesting results — Sortino, Calmar, VaR, CVaR, Ulcer Index | Global ON/OFF | Role: antlers-user or antlers-pro (to be decided; recommend antlers-pro if this is a paid-tier differentiator, otherwise antlers-user). |
When OFF: metrics section absent from results. Hidden. Confirm no gray-out exists in current Backtesting.js. |
post-launch |
walk_forward_validation |
Backtesting — walk-forward validation option | Global ON/OFF | Role: antlers-pro (advanced feature). |
When OFF: option absent from backtest form. Hidden. | post-launch |
custom_strategy_sandbox |
Backtesting — custom strategy editor | Global ON/OFF | Role: antlers-pro. |
When OFF: sandbox absent from UI. Hidden. | post-launch |
enable_options_backtest |
surface: raptor — HTTP 403 gate on options backtest endpoints |
Global ON/OFF | N/A — backend endpoint gate, not UI surface. Antlers impact: companion options_backtest flag governs UI. |
When OFF: 403 from backend. UI should not expose the path in the first place when options_backtest is OFF. |
post-launch |
3.5 AI Surfaces
| Flag | Surface / Route | Current Gate | Recommended Gate | Denied-State Treatment | Priority |
|---|---|---|---|---|---|
ai_proposer |
AI strategy proposer surface (exact UI location TBD) | Global ON/OFF | Role: antlers-pro (or explicit permission antlers-feature-ai-proposer). Per project memory: AI augments understanding, never autonomous order execution. |
When OFF: surface absent. Hidden. | post-launch |
3.6 Onboarding and First-Run Surfaces
| Flag | Surface / Route | Current Gate | Recommended Gate | Denied-State Treatment | Priority |
|---|---|---|---|---|---|
dashboard_first_run |
/dashboard — first-run welcome rail + empty-state widgets |
Global ON/OFF | Role: antlers-user — any authenticated new user. Detection via positions.length === 0 && settings.first_dashboard_seen !== true. Role gate is session presence, not an RBAC permission. |
When OFF: legacy empty-state Dashboard renders. No denied state. | post-launch |
3.7 Demo and Marketing Surfaces (pre-auth / public)
| Flag | Surface / Route | Current Gate | Recommended Gate | Denied-State Treatment | Priority |
|---|---|---|---|---|---|
demo_flow_ui |
/demo — 6-step visitor conversion flow (public, no auth) |
Global ON/OFF | Role: N/A — public route. No auth required. No RBAC gate needed. | When OFF: route not registered. Hidden by absence. | post-launch |
demo_founders_cta_variant |
DemoConversionOverlay — CTA variant text |
Global ON/OFF | Role: N/A — variant logic, not an access gate. | When OFF: standard CTA renders instead of Founders variant. No denied state. | post-launch |
demo_feasibility_readout |
/demo Step 2 — FeasibilityReadout component |
Global ON/OFF | Role: N/A — public route feature. | When OFF: readout absent from slider view. | post-launch |
demo_posthog_events |
Client-side PostHog capture on /demo |
Global ON/OFF | Role: N/A — analytics toggle. | When OFF: posthog.capture() is a no-op. |
post-launch |
demo_clarity |
Microsoft Clarity session replay on /demo |
Global ON/OFF | Role: N/A — analytics toggle. | When OFF: Clarity not initialized. | post-launch |
getraxx_waitlist |
WaitlistSection.js → POST /api/waitlist/subscribe |
Global ON/OFF | Role: N/A — public marketing surface. | When OFF: form is static no-op (existing behavior). | post-launch |
3.8 Privacy and Consent Surfaces
| Flag | Surface / Route | Current Gate | Recommended Gate | Denied-State Treatment | Priority |
|---|---|---|---|---|---|
antlers_obfuscate_mode |
Portfolio, positions, backtest, trade-history — hides $ values, shows % |
Global ON/OFF | Role: any authenticated session (antlers-user or above). Obfuscate mode is a user preference, not a tier gate. State is stored in localStorage. When ON globally, the toggle control is available to all authenticated users. No per-role restriction needed — operator decision: should all users have this? |
When OFF: toggle absent from Header/Settings. Dollar values always shown. Hidden toggle. | post-launch |
3.9 Observability and Auth Backend Flags with Antlers UI Impact
These flags live on surface: raptor or surface: antlers but directly govern what Antlers renders:
| Flag | Surface | Antlers UI Impact | Gate | Denied-State | Priority |
|---|---|---|---|---|---|
render_id_emission |
antlers | useRenderId() hook emits render events per page mount |
Global ON/OFF | When OFF: hook is no-op. No visible UI change to user. | post-launch |
auth_email_verification |
raptor | Backend email send/verify endpoints — companion to email_verification_ui |
Global ON/OFF | When OFF: /verify-email/* routes still register if email_verification_ui is ON, but API calls 404. Handle gracefully. |
v1 dependency (must match email_verification_ui) |
webauthn_registration |
raptor | Backend passkey registration endpoints — required before passkey_signup_ui is meaningful |
Global ON/OFF | When OFF: signup UI calls 404. Must be ON before passkey_signup_ui. |
v1 dependency |
auth_webauthn_login |
raptor | Backend passkey assertion endpoints — required before passkey_login_ui is meaningful |
Global ON/OFF | When OFF: login UI calls 404. Must be ON before passkey_login_ui. |
v1 dependency |
3.10 Flags Not Yet in App.js (UI not yet wired)
These flags are defined in feature_flags.yaml with surface: antlers but do not appear in current App.js routing or visible component files:
| Flag | Notes |
|---|---|
broker_fidelity |
Epic 1 — Fidelity broker integration (UI not yet built) |
options_chain |
Epic 1 — Options chain view (separate from options_backtest) |
roi_goals_tracker |
Epic 1 — ROI goals tracker (UI not yet built) |
sentiment_pipeline |
surface: raptor — backend-only, no Antlers UI yet |
customer_docs_theme |
surface: antlers, area: docs — CSS asset for docs.raxx.app, not the raxx.app SPA |
antlers_demo_mode |
Referenced in DemoContext.js but not present in feature_flags.yaml — unresolved flag: see §7 |
4. Recommended RBAC Permission Names
Where a flag will eventually need per-role enforcement, the following permission names are proposed following the <app>-<resource>-<level> convention from rbac-design.md:
| Surface / Feature | Proposed Permission Name | Minimum Role |
|---|---|---|
| Route guard (session presence) | N/A — gate is session existence, not a permission | antlers-user session |
| Onboarding wizard | N/A — gate is onboarding_completed state |
antlers-user session |
| Trade window (paper mode) | antlers-trade-paper |
antlers-user |
| Trade window (live mode) | antlers-trade-live |
antlers-founders or above, plus paper-first gate |
| Advanced risk metrics | antlers-backtest-advanced |
antlers-pro |
| Walk-forward validation | antlers-backtest-advanced (same) |
antlers-pro |
| Custom strategy sandbox | antlers-strategy-sandbox |
antlers-pro |
| AI proposer | antlers-feature-ai-proposer |
antlers-pro |
| Obfuscate mode toggle | N/A — user preference, not a permission | antlers-user |
| Help drawer | N/A — available to all authenticated | antlers-user |
| Options backtesting | antlers-backtest-options |
Blocked by licensing (#244), not RBAC; defer permission until licensing resolves |
Phase 2 note: These permission names are for planning purposes. Phase 2 (per-role flag resolution) must confirm B1 enforcement compatibility before any permission-conditional flag resolution ships.
5. Denied-State Audit — Current Gray/Disable Violations
This section inventories surfaces that currently show a grayed, badged, or "Coming soon" affordance when the controlling flag is OFF. Each is a violation of the locked invariant (2026-05-11 UTC) and requires a remediation card before v1 launch.
Gap 1: Settings → Platform Features — "Coming soon" badges (Settings.js, lines 617–638)
Location: frontend/trademaster_ui/src/pages/Settings.js, Settings → Platform Features tab
Current behavior:
- When options_backtest is OFF: "Options Backtesting" row renders with a gray secondary badge reading "Coming soon" and a tooltip explaining the licensing status.
- When live_mode_ring is OFF: "Live Mode Ring" row renders with a gray secondary badge reading "Coming soon."
Violation: Both rows are visible to all authenticated users regardless of flag state. The platform is communicating "this exists but you can't have it yet" — exactly the framing the invariant prohibits.
Required remediation:
- When options_backtest is OFF: hide the entire "Options Backtesting" row entirely. No badge, no tooltip, no row.
- When live_mode_ring is OFF: hide the entire "Live Mode Ring" row, AND hide the "Sentinel ring" nav item in the Settings left nav (line 348, already conditionally rendered — confirm it is gated correctly).
- The introductory copy ("Features shown as 'Coming soon' are in active development...") must also be removed or revised once the rows are hidden, since it describes the pattern being eliminated.
Remediation card: See sub-card recommendation in §6 (Gap-1 card).
Gap 2: console_drawer_actions flag — "Re-check disabled + tooltip for roles below ops"
Location: backend_v2/api/feature_flags.yaml line 555, flag console_drawer_actions
Surface: surface: console — this is a console flag, NOT an Antlers customer-facing flag.
Assessment: Out of scope for this audit. The console drawer actions gray pattern is an operator-facing surface. The hide-don't-gray rule applies to customer-facing Antlers surfaces; console surfaces for operators have different UX conventions. No remediation required under this audit.
Gap 3: antlers_demo_mode — referenced in code but absent from feature_flags.yaml
Location: frontend/trademaster_ui/src/context/DemoContext.js line 28
Current behavior: DemoContext.js calls isEnabled('antlers_demo_mode'). This flag name does not exist in feature_flags.yaml. When window.__FLAGS__ does not define it, the flag silently defaults to false — demo mode is globally disabled.
Risk: Undefined flag behavior. If demo mode is intended to be active, it may silently not work. If it is intentionally not in the YAML (because it is set some other way), that should be documented.
Assessment: Not a denied-state violation, but a flag inventory gap. See §7 (open questions).
6. v1 RBAC Gaps — Must-Fix Before 2026-05-23 UTC Launch
These are the items that must be resolved before first external user:
| # | Gap | Flag(s) | Required Action | Blocker? |
|---|---|---|---|---|
| G-1 | "Coming soon" badges in Settings | options_backtest, live_mode_ring |
Hide both rows when flags are OFF. Remove associated introductory copy. | Yes — denied-state invariant violation |
| G-2 | Route guard must be ON | route_guard |
Flag must be flipped ON before launch. Requires passkey_login_ui also ON. No code change needed if guard logic is complete; this is an ops flip. |
Yes — no auth gate without it |
| G-3 | Passkey auth backend + UI flags must be ON | passkey_signup_ui, passkey_login_ui, webauthn_registration, auth_webauthn_login |
All four must be ON for a working auth flow. Backend flags gate the API; UI flags gate the routes. Sequencing: backend first, then UI. | Yes — no user auth without these |
| G-4 | Email verification flags must be consistent | email_verification_ui, auth_email_verification |
Both must be ON together, or both OFF together. Mismatched state (UI ON, backend OFF) will produce 404 errors on the verification API. | Yes — UX breaks without parity |
| G-5 | Onboarding wizard gating | onboarding_wizard_v1 |
Wizard routes must require an authenticated session (via route_guard). Current implementation relies on FLAG_ROUTE_GUARD for session gating — confirm that wizard routes are nested inside the guarded route tree, not publicly accessible. |
Yes — security: unauthenticated users must not reach onboarding routes |
| G-6 | Quebec geoblock must be ON | quebec_geoblock |
Must flip to ON before launch. CAD $30,000/day OQLF exposure (Bill 96). Hard deadline 2026-05-23 UTC. | Yes — legal |
v1 RBAC gap count: 6 (G-1 through G-6)
Note: G-2 through G-5 are operational flips, not code changes, assuming the underlying feature work is complete. G-1 requires a code change in Settings.js. G-6 is an operational flip.
7. Open Questions (require operator decision before sub-cards are claimed)
-
antlers_demo_modeflag registration.DemoContext.jsreferencesisEnabled('antlers_demo_mode')but the flag is absent fromfeature_flags.yaml. Is this flag intentionally managed outside the YAML (e.g., derived fromdemo_flow_ui), or is it a missing entry? Operator must decide: add a YAML entry or remove theisEnabledcall and derive demo-mode state fromdemo_flow_uidirectly. -
options_backtestrow: hide entirely or hide until licensing resolves? The audit recommends hiding the row when the flag is OFF. However, if Kristerpher wants a waitlist-capture affordance for users interested in options backtesting (similar to the Quebec waitlist pattern), the treatment changes. Current recommendation: hide entirely; no waitlist row. Confirm. -
antlers_obfuscate_modescope. Currently global ON/OFF. Should it be available to all authenticated users (any tier), or restricted to a specific tier? Recommend: available to allantlers-user-and-above. If restricted toantlers-pro, a denied-state gap would be created (the toggle must be hidden for non-pro users). Operator must lock this before Phase 2 RBAC implementation. -
advanced_risk_metrics,walk_forward_validation,custom_strategy_sandboxtier assignment. Are theseantlers-pro-only or available to all authenticated users at v1? If pro-only, the denied-state treatment for non-pro users is: these options are simply absent from the backtest form. Operator must confirm tier assignment before permission names are finalized. -
ai_proposertier. Same question as above. Given the product thesis (AI augments understanding, never autonomous execution), this surface may be appropriate for all authenticated users or only pro. Confirm. -
B1 compatibility of role-conditional flag resolution. Phase 2 (per-role flag resolution) must confirm that adding role conditions to flag evaluation does not break B1 enforcement (every flag in
feature_flags.yamlmust have aconsole_flag_promotionsmigration row before prod flip). This is a design constraint on Phase 2, not Phase 1.
8. Rollout Compatibility
All flags audited here are currently global ON/OFF. No schema migrations are required to implement the denied-state remediations (G-1 is a React render change only). The RBAC phase is Phase 2 and is not a prerequisite for v1 launch, except where explicitly marked as a blocker above.
The flag reconciler (ADR-0085) is not affected by this audit. Flag values themselves are not changed by this document.
9. Security Considerations
- The route guard (
route_guardflag) is the sole client-side access gate for authenticated routes. When OFF, all authenticated routes are publicly accessible. This is acceptable pre-launch in development but must be ON before any customer receives a login link. - Geographic gates (
quebec_geoblock,signup_geoblock_eu) are defense-in-depth only. Server-side enforcement in Raptor is the authoritative gate; client-side is a UX layer. - No PII is stored or transmitted by any flag in this audit. The
antlers_obfuscate_modeflag operates on the render layer only; the backend always sends real values. - Session state (
antlers-userrole,onboarding_completed) must be derived from the session token issued by Queue/Raptor after passkey authentication, not from client-only localStorage. The route guard's reliance onlocalStorageas a fallback (when the session endpoint is not yet live) is a known gap tracked by the auth team.
10. Related Documents
docs/architecture/rbac-design.md— role taxonomy and data modeldocs/architecture/auth-unification-rbac-reconciliation.md— RBAC V2 phase mapdocs/architecture/auth.md— auth flow designdocs/architecture/adr/0020-rbac-groups-not-direct-roles.md— centralized identity decisiondocs/architecture/adr/0031-platform-auth-posture.md— WebAuthn-only gatedocs/architecture/adr/0032-customer-account-recovery-a-plus-b-only.md— recovery path constraint
Awaiting operator review. No code changes will be dispatched from this audit until Kristerpher approves.