Skip to main content

The Verify dashboard: history surface, filters, and role access

The Outbound → Verify page is the OTP console surface: a per-verification history table, the analytics and fraud panes, and a header action row whose Send Test OTP lane triggers paid carrier sends. It is also a role-gated surface — the seat’s organization role must be owner, admin, or developer for the page to render at all. The guide-level walkthrough in Verify console teaches you which screens to click; this page explains the surface behind them, so you can predict what a given seat will see and what lanes the page exports. Everything on this page is a tenant-owned control. The history rows are the org’s own verification sessions, the filters run client-side on the current page of rows, the paid test-send dialog is bounded by the same fraud pre-flights and account-level pre-send gates as an API send, and the CSV export pins each row’s created_at to the operator’s local zone so the exported file matches the rendered column.

1. The page and the guard — routing layer

The page is a Next.js route: apps/web/src/app/[locale]/(dashboard)/verify/page.tsx. The whole content body is wrapped in a RoleGuard against the seats that are authorized to move money: the page surfaces recipient PII (live phone numbers in the history table) AND the dialog row can trigger a paid carrier send, so both reads and writes are kept away from read-only seats. The guard resolves the member’s role from Clerk’s active org membership, with the same DB-truth fallback the rest of the dashboard uses: a Clerk role the map does not recognise is defaulted to viewer (denied), and a Clerk fallback that would deny the page is re-checked once against the API’s /me role before the static access-denied screen renders. The Clerk role string is only a best-effort mirror of the DB truth, so an invited billing seat that accidentally decodes as deny isn’t pushed onto the denied screen while the backend would grant it. The allowed set is owner, admin, developer: The denial is whole-page: a non-allowed role sees the access-denied surface, not a gated subset. (Inbox-side supervisor routes, which open to supervisors and stay denied here, are a routed by a different lane; this page deliberately excludes them because OTP sends are never a supervisor action.)

2. The page surface — history rows, tabs, and the send lane

What the page renders, from top to bottom:
  • Header row. Title, subtitle, and the action cluster: Export (CSV of the filtered rows), Fraud Score (pre-send risk dip on a target), Fraud Gate (composite operator-fusion verdict), Check Verification (resolve a code against an in-flight session), Bulk send, and Send Test OTP — the dialog that triggers a paid carrier send through the same /send gate chain as an API-driven send.
  • The tabs. overview (history table + headline KPI tiles), configuration (verify profiles and templates), analytics (windowed aggregates), fraudGuard (fraud analytics), and voiceBiometrics (enrollment manage UI when the add-on is present). All tabs share the same guard; the per-tab filter bar persists in the URL.
  • The history table. Each renderable row is a Verification — id, recipient to, channel, status (pending / in_progress / verified / expired / failed), config name, attempts, created_at, optional verified_at and expires_at, and an is_test marker that disambiguates dashboard-initiated test sends from API-driven production rows. The verification.detail drawer tucks a richer per-row view: fallback execution timeline, masked code-attempts log, and per-hop status coding.
  • The stats row. The Overview KPI tiles resolve to either the org-wide GET /verify/analytics aggregate (the authoritative server-side COUNT(*)) or the filtered in-page reduction — which one the row uses depends on the active filters. The org-wide path is bounded to a rolling look-back window; the window-bounded label on the gauge card exists so “Total Sent” does not over-read as an all-time account count.
The filter bar (channel, status, date range, custom range, free-text search) is URL-persisted, so a filter survives a tab switch and can be deep-linked. The free-text search lane is deliberately a client-side substring match — the server-side aggregate cannot answer it, so while a search is active the tiles fall back to loaded-rows scope and say so.

3. Roles — read/write per seat

The RoleGuard above gates the whole page, but the read/write split still matters for which lane a seat can act on:
  • Read lanes (history rows, the per-row drawer, the fraud-score and fraud-gate dialogs, the analytics pane, CSV export) evaluate identically for the three allowed roles.
  • Write lanes are uniformly open across owner/admin/developer: the send dialog, bulk send, check-verification, fraud-score dip, and fraud-gate dip are the same writes to the same backend gates. The role split is read+send vs. read-only, not owner can send, admin cannot. (Tenant-owned controls here: the fraud gate fires only when the profile configures it; an unconfigured profile skips the dips, and the whole scoring path degrades fail-open when a network-signal source is unreachable.)
  • Outbound termination. The Send Test OTP lane runs a paid carrier send; outbound MT SMS (and voice OTP delivery) exits exclusively through Devotel’s wholesale softswitch, as everywhere else in the platform.
The same guard discipline sits on the configuration tab’s profile editing and the voice biometrics tab’s enrollment management: a viewer can still GET /verify rows via the API if another gate explicitly allows it — the frontend guard is the page’s gate, not the API’s gate, so read-side API exposure is unchanged. (Verify profile creation, template edits, and test sends on the API are owner/admin/developer routes too.)

4. Underlying data — profile, template, verdict, log

The rows the page renders fold a few underlying records into one view:
  • Passcode / verification profile (config_name on the row) — the profile that drove the send: ordered channel chain, code length, expiry, max attempts, velocity caps. The dashboard configuration tab edits these; the history row names which profile a send ran under.
  • Template — the OTP body when the tenant configured one; the send pipeline’s passcode templates surface as structured template objects rather than flat text on the history row. (See also the template lifecycle concept for how template rows are governed across channels.)
  • Risk verdict — when fraud scoring ran, the row’s context carries the composite score/verdict (allow / step_up / block) and the per-hop attempt log; the masked code-attempts log row and the fallback execution timeline live in the verification-detail drawer.
  • Event ledger. log events — the verification.check / verified / expired / failed family — are the analytical source under both the history list and the analytics window (see verification lifecycle); they write to the tenant’s audit stream even when the dashboard row is still in-flight.
This page sits between two sibling readers:
  • Verify console guide — the click-path walkthrough for the same screen; use this concept page to predict the surface, then open the guide for per-control interaction detail.
  • Verify fraud gate: score, verdict, fallback chain — the composite model the fraud-score and fraud-gate dialogs render, plus the per-hop attempts log the verification-detail drawer surfaces.
  • Verification lifecycle — the session state machine and fallback advance rules the history row’s status flips over.
  • Roles, teams, permissions — the built-in role vocabulary this page’s guard names.
  • Verify overview — the API-side reference for the recipient shape, endpoint pre-flights, and rate limits the Test-OTP lane inherits.

Scope of the guard

The guard here is strictly the dashboard surface gate. The POST /verify/send and /check routes guard their own seats server-side; a dashboard render-blocked role can still hold an API key with verify privileges. Use the page to operate; use the API to integrate. The page’s on-screen labels tell the operator which role the current session resolved as, so a mis-provisioned seat does not silently decode as viewer.