Browse documentation

Frontend Specs

RankFlow AI — Frontend Specification (Part 1)

The RankFlow AI frontend is built on a token-driven design system named Quiet Signal. Every pixel, color, spacing value, and motion curve is derived from the canonical docs/brand/t

docs/specs/frontend/part-1.md
On this page

Version: 1.0.0 Date: 2026-06-13 Stack: Next.js 14 + React 18 + TypeScript + Tailwind CSS + tRPC + React Query + Better Auth Scope: Sections 1–4 — Global Foundations, Pre-Sales, Authentication, Onboarding Classification: Internal — Engineering Handoff


Section 1 — Design System & Global Foundations#

1.1 Overview#

The RankFlow AI frontend is built on a token-driven design system named Quiet Signal. Every pixel, color, spacing value, and motion curve is derived from the canonical docs/brand/tokens/design-tokens.json file. The system is engineered for medical professionals in India — low cognitive load, high readability, strict accessibility, and zero decorative noise.

All components are controlled React components with forwardRef support. The entire UI is responsive across three primary breakpoints: 1280px+ desktop, 768px tablet, and 375px mobile. The sidebar collapses to a hamburger menu on mobile and tablet.

1.2 Color Tokens#

The color system is anchored on three identity colors: Deep Ink, Paper White, and Ember Orange. All other colors are supporting accents or semantic mappings. Ember Orange is reserved exclusively for CTAs and conversion outcomes; it is never used for decoration, icons, or backgrounds unrelated to user action.

Token Name Hex Value Usage Rule Frontend Variable
color.brand.paper-white #F6F6F8 Primary canvas, editorial backgrounds, dashboard base --color-paper-white
color.brand.deep-ink #011821 Primary typography, dark surfaces, sidebar background, Rise signal bars --color-deep-ink
color.brand.ember-orange #EC652B CTAs only. Primary action buttons, focus rings, conversion outcomes, booked states. Never used for decoration or icons. --color-ember-orange
color.brand.card-white #FFFFFF Cards, dialogs, inputs, elevated surfaces --color-card-white
color.brand.carbon #12161E Dense technical surfaces, secondary dark panels --color-carbon
color.brand.deep-indigo #111A4A Charts, supporting dark accents --color-deep-indigo
color.brand.midnight-teal #023247 Technical panels, information foregrounds, dark data accents --color-midnight-teal
color.brand.forest-teal #167E6C Verified success, live state, positive evidence --color-forest-teal
color.brand.sky-blue #7EA7E9 Informational data series, supporting illustrations --color-sky-blue
color.brand.pale-cyan #C1E8EF Information backgrounds, selected states --color-pale-cyan
color.brand.mint #44B48B Secondary positive data, illustrations --color-mint
color.brand.lavender #9F7AEE Secondary data series only --color-lavender
color.brand.mist #E3E4E8 One-pixel borders, rules, separators --color-mist
color.brand.fog #A9ACB6 Disabled controls, non-essential decoration --color-fog
color.brand.slate #7C7F88 Tertiary labels, muted metadata --color-slate
color.brand.graphite #3B3E47 Accessible secondary text, icons --color-graphite
color.brand.error-red #C2442A Destructive actions, error states --color-error-red

Semantic Color Mapping Table:

Semantic Token Resolved Value Usage
text-primary #011821 All body text, headings, labels
text-secondary #3B3E47 Descriptions, secondary labels, helper text
text-tertiary #7C7F88 Metadata, timestamps, placeholders
text-disabled #A9ACB6 Disabled input text, inactive tab labels
text-inverse #F6F6F8 Text on dark surfaces (sidebar, dark buttons)
text-link #023247 Inline links, navigational anchors
text-link-hover #011821 Link hover state
surface-canvas #F6F6F8 Page background
surface-card #FFFFFF Card and dialog backgrounds
surface-elevated #FFFFFF Modals, dropdowns, popovers
surface-dark #011821 Sidebar, dark headers
surface-dark-secondary #12161E Admin panels, technical dark zones
border-default #E3E4E8 Input borders, card borders, table dividers
border-strong #A9ACB6 Focused input borders, active tab borders
action-primary #011821 Primary button background (non-CTA)
action-accent #EC652B CTA button background, focus indicators
action-accent-foreground #011821 Text on Ember Orange buttons
focus #EC652B Focus ring color, outline color
success #167E6C Success toasts, verified badges, live indicators
success-soft #44B48B Secondary success backgrounds
warning #EC652B Warning toasts, alert badges
error #C2442A Error toasts, invalid input borders, destructive buttons
info #023247 Info toasts, informational badges
info-soft #C1E8EF Info backgrounds, callout boxes

Data Visualization Colors:

Series Hex Usage
data-series-1 #167E6C Primary metric (e.g., citation health)
data-series-2 #7EA7E9 Secondary metric (e.g., GBP views)
data-series-3 #9F7AEE Tertiary metric (e.g., social engagement)
data-series-4 #44B48B Quaternary metric
data-series-5 #023247 Quinary metric
data-attention #EC652B Threshold breaches, anomaly markers
data-grid #E3E4E8 Chart grid lines, table dividers

Gradient Prohibition: No blue-purple gradients are permitted anywhere in the UI. The only permitted gradients are subtle rgba(1, 24, 33, 0.02) to transparent overlays for image masks on marketing pages.

1.3 Typography Tokens#

Font family is Suisse Intl with a strict hierarchy. No bold weight (700) is permitted anywhere in the UI. The maximum weight used is 600 (semibold). Fallback stack ensures cross-platform rendering.

Token Value Line Height Letter Spacing Usage
font-size-caption 12px 18px 0.01em Badges, timestamps, table footers
font-size-body-sm 14px 21px 0 Form labels, helper text, secondary body
font-size-body 16px 24px 0 Primary body text, input values
font-size-body-lg 18px 26px -0.01em Lead paragraphs, large descriptions
font-size-subheading 20px 28px -0.01em Card titles, section subheadings
font-size-heading-sm 24px 32px -0.02em Page titles in dashboard
font-size-heading 28px 36px -0.02em Hero titles, onboarding step titles
font-size-heading-lg 40px 44px -0.02em Landing page hero H1
font-size-display 60px 66px -0.03em Marketing homepage hero (desktop only)
Weight Token Value Allowed Context
font-weight-light 300 Large display headings, marketing hero
font-weight-regular 400 Body text, descriptions, inputs
font-weight-medium 500 Buttons, navigation labels, tab labels
font-weight-semibold 600 Card titles, data values, active states

Font Stack Declaration:

--font-primary: 'Suisse Intl', 'SuisseIntl', 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'Suisse Intl Mono', 'SuisseIntlMono', 'SFMono-Regular', Menlo, Monaco, Consolas, monospace;

1.4 Spacing & Layout Tokens#

Base unit is 4px. All spacing values are multiples of 4. This ensures perfect alignment and eliminates half-pixel anti-aliasing artifacts on non-retina displays.

Token Value Usage
space-0 0 Zero spacing
space-1 4px Tight gaps, icon padding
space-2 8px Inline element gaps, tight button padding
space-3 12px Standard element gaps, icon + text pairs
space-4 16px Card internal padding (small), form field gaps
space-5 20px Medium component gaps
space-6 24px Standard card padding, modal padding
space-8 32px Section internal gaps, large card padding
space-10 40px Page section vertical gaps
space-12 48px Large component separations
space-16 64px Marketing section gaps
space-18 72px Hero section padding
space-20 80px Major page sections
space-24 96px Full-page vertical rhythm
space-25 100px Marketing page maximum gaps
space-36 144px Extreme spacing (rare)

Layout Tokens:

Token Value Usage
page-max-width 1200px Maximum content width for all pages
section-gap 80px Vertical gap between major page sections
card-padding 24px Standard padding inside cards
element-gap 12px Gap between sibling elements in a form or list

1.5 Border, Radius & Shadow Tokens#

Token Value Usage
border-width-default 1px Standard borders (inputs, cards, tables)
border-width-strong 2px Active states, focus borders, error states
border-radius-none 0 Full-bleed images, data tables
border-radius-sm 2px Small tags, micro-buttons
border-radius-lg 8px Primary UI radius: buttons, cards, inputs, modals
border-radius-xl 12px Large cards, hero containers
border-radius-full 9999px Pills, badges, avatars

Shadow Tokens:

Token Value Usage
shadow-subtle 0 1px 3px rgba(17, 26, 74, 0.10), 0 1px 0 rgba(17, 26, 74, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.50), inset 0 1px 4px rgba(255, 255, 255, 0.50) Card resting state, input resting state
shadow-outline 0 0 0 1px rgba(87, 90, 100, 0.12) Subtle elevation, selected rows
shadow-inner inset 0 0 0 1px rgba(0, 0, 0, 0.05) Inset inputs, pressed states
shadow-sm 0 4px 8px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.10), 0 1px 1px rgba(0, 0, 0, 0.10) Dropdowns, hover-elevated cards
shadow-xl 0 40px 32px rgba(0, 0, 0, 0.02), 0 22px 18px rgba(0, 0, 0, 0.03), 0 12px 10px rgba(0, 0, 0, 0.03), 0 7px 5px rgba(0, 0, 0, 0.04), 0 3px 2px rgba(0, 0, 0, 0.07) Modals, dialogs
shadow-focus 0 0 0 3px rgba(236, 101, 43, 0.28) Focus ring for all interactive elements

1.6 Motion & Animation Tokens#

Token Value Usage
duration-instant 100ms Color transitions, opacity changes
duration-fast 150ms Button hover states, icon rotations
duration-base 200ms Card hover lifts, dropdown opens
duration-slow 300ms Modal enter/exit, page transitions
Easing Token Value Usage
easing-standard cubic-bezier(0.2, 0, 0, 1) Standard transitions
easing-enter cubic-bezier(0, 0, 0, 1) Elements entering viewport
easing-exit cubic-bezier(0.3, 0, 1, 1) Elements leaving viewport

1.7 Iconography#

  • Icon Family: Lucide
  • Stroke Width: 1.5px
  • Sizes: 16px (sm), 20px (md), 24px (lg)
  • Color Rule: Icons inherit text-primary or text-secondary via currentColor. Only the action-accent icons (CTA arrows, outcome indicators) use ember-orange.
  • Iconography Prohibition: No custom illustrations, no emoji, no SVG icons outside the Lucide set. All icons must be monochrome.

1.8 Global Component Library#

Every component is a controlled, typed React component with forwardRef support. All components use the design tokens above via CSS custom properties mapped through Tailwind config. No inline styles are permitted.

1.8.1 Button System#

Three variants are permitted: Primary, Accent (CTA), Ghost. No other button styles are allowed. No bold text in buttons. Maximum weight is medium (500).

Primary Button Specification:

Property Value Notes
Background action-primary (#011821)
Foreground text-inverse (#F6F6F8)
Border-radius 8px border-radius-lg
Padding 10px 16px space-2 vertical, space-4 horizontal
Font body-sm (14px/21px) Weight medium (500)
Hover Background midnight-teal (#023247) Duration 150ms, easing standard
Active Scale 0.98, shadow-inner applied
Disabled Opacity 0.5, cursor not-allowed, no hover transform
Loading Spinner icon replaces text, width preserved, aria-busy="true" Spinner: Loader2 from Lucide, 16px, text-inverse
Focus shadow-focus ring, offset 0

Accent Button (CTA Only) Specification:

Property Value Notes
Background action-accent (#EC652B)
Foreground action-accent-foreground (#011821)
Border-radius 8px
Padding 10px 16px
Font body-sm (14px/21px) Weight medium (500)
Hover Background darkens to #D95A23 Duration 150ms
Active Scale 0.98
Disabled Opacity 0.5
Loading Spinner with currentColor #011821
Focus shadow-focus ring
Constraint Only one Accent Button per viewport No decorative use. Never used for secondary actions.

Ghost Button Specification:

Property Value Notes
Background transparent
Foreground text-primary (#011821)
Border 1px solid border-default (#E3E4E8)
Border-radius 8px
Padding 10px 16px
Hover Background rgba(1, 24, 33, 0.04), border border-strong
Active shadow-inner
Disabled Opacity 0.5, border border-default
Focus shadow-focus ring

Button Size Matrix:

Size Height Padding Font Size Usage
sm 32px 6px 12px 12px Inline actions, table rows
md 40px 10px 16px 14px Standard forms, modals
lg 48px 12px 24px 16px Marketing CTAs, hero actions

Button Component Interface:

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant: "primary" | "accent" | "ghost";
  size?: "sm" | "md" | "lg";
  loading?: boolean;
  children: React.ReactNode;
}

1.8.2 Input System#

Text Input Specification:

Property Value Notes
Background surface-card (#FFFFFF)
Border 1px solid border-default (#E3E4E8)
Border-radius 8px
Padding 10px 12px
Font body (16px/24px) Weight regular (400), color text-primary
Placeholder text-secondary (#3B3E47)
Hover Border border-strong (#A9ACB6)
Focus Border focus (#EC652B), shadow-focus applied
Error Border error (#C2442A), 0 0 0 3px rgba(194, 68, 42, 0.28)
Disabled Background surface-canvas, color text-disabled, border border-default
Read-only Background surface-canvas, border border-default, no focus ring

Select Input Specification:

  • Same as text input with a 20px chevron icon (ChevronDown from Lucide) on the right, 12px padding from edge.
  • Dropdown menu: shadow-sm, border-radius 8px, background surface-card, max-height 240px, overflow-y auto, border 1px solid border-default.
  • Option hover: Background rgba(1, 24, 33, 0.04).
  • Option selected: Background pale-cyan (#C1E8EF), color text-primary.
  • Option font: body-sm (14px), weight regular.
  • Empty state: No options text, text-secondary, centered.

Textarea Specification:

  • Same as text input, min-height 96px (space-24), resize vertical only.
  • Max-height: 400px.
  • Line-height: 24px.

Date Input Specification:

  • Uses native <input type="date"> with custom styling via appearance-none.
  • Calendar icon: Calendar from Lucide, 20px, color text-secondary, positioned right 12px.
  • Value format (tRPC payload): YYYY-MM-DD (ISO).
  • Display format: DD MMM YYYY (e.g., 13 Jun 2026).
  • Min/max attributes supported via props.

Input Component Interface:

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  error?: string;
  hint?: string;
  iconLeft?: LucideIcon;
  iconRight?: LucideIcon;
}

1.8.3 Card System#

Three card types: Metric, Content, Action.

Metric Card Specification:

Property Value Notes
Background surface-card
Border 1px solid border-default
Border-radius 8px
Padding 24px
Shadow shadow-subtle
Layout Vertical stack, gap 12px
Header Label (font-size-caption, text-secondary) + optional info icon (Info, 16px)
Value font-size-heading-sm (24px), weight semibold (600), color text-primary
Delta font-size-caption, color forest-teal (positive) or error-red (negative), prefixed with or
Hover shadow-sm, translateY -1px, duration 200ms Only on dashboard grid

Content Card Specification:

Property Value Notes
Background surface-card
Border 1px solid border-default
Border-radius 8px
Padding 24px
Shadow shadow-subtle
Header Title (font-size-subheading, weight medium), optional subtitle (font-size-body-sm, text-secondary)
Body font-size-body, text-primary, gap 16px between paragraphs
Footer Optional border-top 1px solid border-default, padding-top 16px, aligned right

Action Card Specification:

  • Same as Content Card but with hover state suggesting clickability.
  • Hover: Border border-strong, shadow-sm, cursor pointer, translateY -1px.
  • Active: shadow-inner, scale 0.995.
  • Contains a primary action button in the footer or a clickable header with arrow-right icon.
  • Must have role="button" and tabIndex={0} if the entire card is clickable.

Card Component Interface:

interface CardProps {
  variant: "metric" | "content" | "action";
  title?: string;
  subtitle?: string;
  children: React.ReactNode;
  footer?: React.ReactNode;
  onClick?: () => void;
}

1.8.4 Table System#

Base Table Specification:

Property Value Notes
Wrapper Background surface-card
Wrapper Border 1px solid border-default
Wrapper Border-radius 8px Applied to wrapper, not cells
Header Row Background surface-canvas
Header Row Border-bottom 1px solid border-default
Header Text font-size-caption, weight medium (500), color text-secondary, uppercase, letter-spacing 0.05em
Body Row Background surface-card
Body Row Border-bottom 1px solid border-default Last row has no border
Body Text font-size-body-sm, color text-primary
Row Hover Background rgba(1, 24, 33, 0.02)
Row Selected Background pale-cyan (#C1E8EF)
Empty State Centered font-size-body-sm, text-secondary, with SearchX icon, padding 48px

Sortable Column Specification:

  • Header clickable, cursor pointer.
  • Sort indicator: ArrowUp / ArrowDown icon, 16px, text-secondary, toggles on click.
  • Default sort: None; first click ascending, second click descending, third click clears.
  • Active sort header: Color text-primary, icon text-primary.
  • Multi-sort: Not supported in v1.0; single column sort only.

Pagination Specification:

  • Position: Footer of table, right-aligned, padding 16px.
  • Components: "Previous" / "Next" ghost buttons, page numbers as ghost buttons (sm size).
  • Active page: Background surface-dark, color text-inverse, border-radius 8px.
  • Page size selector: 20 / 50 / 100 rows, select input, sm size.
  • Ellipsis: ... text, text-secondary, non-clickable.

Filters Specification:

  • Filter bar above table, horizontal row, gap 12px, padding 16px, border-bottom 1px solid border-default.
  • Each filter: sm size input or select, ghost button style, min-width: 120px.
  • Clear filters: Ghost button with X icon, appears when any filter is active.
  • Filter state: Synced to URL query parameters (?status=ACTIVE&sort=createdAt:desc).
  • Filter count badge: If filters active, show count in a full radius badge, background ember-orange, color text-inverse, font-size-caption.

Table Component Interface:

interface TableProps<T> {
  data: T[];
  columns: ColumnDef<T>[];
  sortable?: boolean;
  pagination?: { page: number; pageSize: number; total: number };
  filters?: FilterDef[];
  emptyState?: React.ReactNode;
  onRowClick?: (row: T) => void;
}

1.8.5 Modal System#

Three modal types: Confirmation, Form, Detail.

Modal Wrapper Specification:

Property Value Notes
Backdrop rgba(1, 24, 33, 0.60) z-index: 400, click to close (unless persistent)
Panel Background surface-card
Panel Border-radius 12px (xl)
Panel Padding 24px
Panel Max-width 560px (default), 720px (wide), 400px (narrow)
Shadow shadow-xl
Entry Animation Opacity 0 → 1, translateY 8px → 0, duration 300ms, easing enter
Exit Animation Reverse of entry
Focus Trap First focusable element auto-focused; Tab cycles within modal; Escape closes
Scroll Lock overflow: hidden on body when open

Confirmation Modal Specification:

  • Icon: AlertTriangle or CheckCircle, 24px, color warning or success, centered above title.
  • Title: font-size-heading-sm, weight medium, color text-primary, centered.
  • Description: font-size-body, text-secondary, centered, max-width 400px.
  • Footer: Two buttons side by side, gap 12px, centered. Left: Ghost "Cancel". Right: Accent "Confirm" (destructive actions use error-red background, text-inverse foreground).
  • Danger variant: Accent button uses error-red background. Icon is AlertTriangle.
  • Width: 400px (narrow).

Form Modal Specification:

  • Same wrapper, body contains a form with vertical stack gap 16px.
  • Footer: Ghost "Cancel" + Primary "Save" or Accent "Submit", right-aligned.
  • Validation: Inline errors appear below fields, modal does not close on invalid submit.
  • Submitting: Footer buttons disabled, primary button shows spinner.
  • Width: 560px (default).
  • Multi-step: Not supported in v1.0.

Detail Modal Specification:

  • Wide variant (720px).
  • Body: Scrollable if content exceeds 70vh, custom scrollbar (8px thumb, midnight-teal).
  • Header: Title (font-size-heading-sm) + close X icon (X, 20px), right-aligned.
  • Footer: Optional action buttons or "Close" ghost button.
  • No centering; left-aligned text.

Modal Component Interface:

interface ModalProps {
  type: "confirmation" | "form" | "detail";
  open: boolean;
  onClose: () => void;
  title: string;
  description?: string;
  children?: React.ReactNode;
  footer?: React.ReactNode;
  size?: "narrow" | "default" | "wide";
  persistent?: boolean;
}

1.8.6 Toast System#

Toast container: Fixed top-right, z-index: 800, gap 12px between toasts, max-width 400px, padding 16px from viewport edge.

Toast Variants:

Variant Background Border Icon Text Color Duration
Success forest-teal (#167E6C) 1px solid rgba(255,255,255,0.1) CheckCircle text-inverse 5000ms
Error error-red (#C2442A) 1px solid rgba(255,255,255,0.1) XCircle text-inverse 8000ms
Warning ember-orange (#EC652B) 1px solid rgba(255,255,255,0.1) AlertTriangle text-inverse 6000ms
Info midnight-teal (#023247) 1px solid rgba(255,255,255,0.1) Info text-inverse 5000ms

Toast Behavior:

  • Entry: Slide from right translateX(100%) → 0, opacity 0 → 1, duration 300ms, easing enter.
  • Exit: Slide right, opacity 1 → 0, duration 200ms, easing exit.
  • Progress bar: Thin line (2px) at bottom, color rgba(255,255,255,0.3), animates from full width to 0 over duration.
  • Hover: Pauses progress bar, resume on mouse leave.
  • Dismiss: Click X icon (16px, top-right) or swipe right on mobile (>50px swipe).
  • Max stack: 5 toasts; overflow pushes oldest out with exit animation.
  • Accessibility: role="status" (non-critical) or role="alert" (critical), aria-live="polite" or assertive.

Toast Component Interface:

interface ToastOptions {
  id: string;
  variant: "success" | "error" | "warning" | "info";
  title: string;
  description?: string;
  duration?: number;
  action?: { label: string; onClick: () => void };
}

1.8.7 Badge & Pill System#

Variant Background Foreground Border-radius Padding Usage
default surface-canvas text-secondary 9999px 4px 12px Neutral status
success forest-teal text-inverse 9999px 4px 12px Live, verified, active
warning ember-orange text-inverse 9999px 4px 12px Pending, trial, warning
error error-red text-inverse 9999px 4px 12px Failed, cancelled, expired
info pale-cyan text-primary 9999px 4px 12px Informational, draft
outline transparent text-secondary 9999px 4px 12px Tags, categories

Font: font-size-caption (12px), weight medium (500).

1.8.8 Sidebar Component#

Property Value Notes
Background surface-dark (#011821)
Width 240px width: 240px
Width Collapsed 64px Icon-only mode on large desktop (optional toggle)
Foreground text-inverse (#F6F6F8)
Foreground Muted fog (#A9ACB6) Inactive items
Item Hover rgba(246, 246, 248, 0.08)
Item Active rgba(246, 246, 248, 0.12)
Active Indicator ember-orange (#EC652B) 2px left border on active item
Font body-sm (14px) Weight medium (500)
Icon Size 20px Color inherits muted or inverse
Gap 12px Between icon and label
Section Gap 24px Between nav groups
Logo Area Height 64px, padding 16px 24px Logo 160px wide max

Mobile Sidebar Behavior:

  • md and below: Sidebar becomes full-screen overlay, 100vw, 100vh, background surface-dark, slides in from left with duration-slow (300ms), easing-enter.
  • Backdrop: rgba(1, 24, 33, 0.60), z-index: 399, tap to close.
  • Close trigger: Tap backdrop, swipe left (>50px), or tap X icon (24px, top-left).
  • Header: Logo + close button, padding 16px 24px.
  • No collapsed icon-only mode on mobile.

Sidebar Navigation Items:

Route Label Icon Roles Badge Logic
/dashboard Overview LayoutDashboard ADMIN, CLIENT, EDITOR, VIEWER None
/dashboard/gbp Google Business MapPin ADMIN, CLIENT, EDITOR, VIEWER Pending review count
/dashboard/social Social Media Share2 ADMIN, CLIENT, EDITOR Pending posts
/dashboard/citations Citations Link ADMIN, CLIENT, EDITOR, VIEWER NAP mismatch count
/dashboard/content Content FileText ADMIN, CLIENT, EDITOR Pending approval count
/dashboard/site Website Globe ADMIN, CLIENT, EDITOR, VIEWER None
/dashboard/reports Reports BarChart3 ADMIN, CLIENT, EDITOR, VIEWER New report indicator
/dashboard/leads Leads Inbox ADMIN, CLIENT, EDITOR New leads count
/dashboard/settings Settings Settings ADMIN, CLIENT None
/dashboard/billing Billing CreditCard ADMIN, CLIENT Past due indicator
/admin Admin Shield ADMIN Failed jobs count

1.9 Authentication & Session Patterns#

1.9.1 Better Auth Session Context#

The application wraps all pages in a SessionProvider from better-auth/react. Session data is fetched server-side on initial render via getServerSession in createTRPCContext. Client-side, the session is available via the useSession() hook.

Session Shape:

{
  "user": {
    "id": "usr_abc123",
    "email": "dr.smith@example.com",
    "name": "Dr. Smith",
    "image": "https://cdn.rankflow.ai/avatars/usr_abc123.png",
    "role": "CLIENT"
  },
  "session": {
    "id": "sess_xyz789",
    "expiresAt": "2026-06-20T06:06:20Z",
    "ipAddress": "192.168.1.1",
    "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
  }
}
  • Session refresh: Automatic via Better Auth updateAge: 60 * 60 * 24 (every 24 hours).
  • Idle timeout: No explicit idle timeout; session expires after 7 days (expiresIn: 60 * 60 * 24 * 7).
  • Multi-session: Supported via auth.listSessions and auth.revokeSession. UI shows active sessions in /dashboard/settings/security.
  • Session persistence: Remember me checkbox extends session to 30 days on the device; stored via localStorage flag rankflow_remember_me.

1.9.2 Role Checks#

Role is stored in User.role enum: ADMIN, CLIENT, EDITOR, VIEWER. Frontend role enforcement is defensive: every route checks session.user.role before rendering restricted UI.

Role-Based Component Wrapper:

interface WithRoleProps {
  allowedRoles: ("ADMIN" | "CLIENT" | "EDITOR" | "VIEWER")[];
  children: React.ReactNode;
  fallback?: React.ReactNode;
}

Redirect Rules:

Scenario Source Target Toast
Unauthenticated user accessing /dashboard/* Any /dashboard route /login?redirect=/dashboard None
VIEWER accessing /dashboard/settings /dashboard/settings /dashboard "Settings require editor access" (warning)
EDITOR accessing /dashboard/billing /dashboard/billing /dashboard "Billing is restricted to the practice owner" (warning)
CLIENT accessing /admin /admin /dashboard "Admin access required" (error)
ADMIN accessing /dashboard /dashboard /admin None (optional redirect)

1.9.3 tRPC Client Setup#

The tRPC client is initialized in src/lib/trpc.ts with httpBatchLink and superjson transformer.

Client Configuration:

export const trpcClient = trpc.createClient({
  links: [
    httpBatchLink({
      url: "/api/trpc",
      headers() {
        return {
          "x-request-id": crypto.randomUUID(),
        };
      },
    }),
  ],
  transformer: superjson,
});

Error Handling Mapping:

tRPC Code HTTP Frontend Behavior Toast Message
BAD_REQUEST 400 Show inline Zod errors; toast with "Please check your input" Inline + warning toast
UNAUTHORIZED 401 Redirect to /login; clear local query cache None (redirect)
FORBIDDEN 403 Redirect to /dashboard; toast permission denial Error toast
NOT_FOUND 404 Show empty state with "Not found" message Info toast
CONFLICT 409 Show inline error (e.g., duplicate email) Warning toast
TOO_MANY_REQUESTS 429 Toast "Too many requests. Please wait 60 seconds." Warning toast
INTERNAL_SERVER_ERROR 500 Toast "Something went wrong. Please try again."; log to Sentry Error toast

Zod Error Display:

// Example Zod error from tRPC
{
  "message": "Invalid input",
  "code": "BAD_REQUEST",
  "zodError": {
    "fieldErrors": {
      "email": ["Invalid email address"],
      "phone": ["Must be at least 10 characters"]
    },
    "formErrors": []
  }
}
  • Field errors map directly to input error props.
  • Form errors (non-field) display in a toast or an alert banner at the top of the form.

1.9.4 React Query Defaults#

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000,      // 60 seconds
      cacheTime: 5 * 60 * 1000,  // 5 minutes
      retry: 1,                  // one retry for network errors only
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
      suspense: false,
    },
    mutations: {
      retry: 0,
      onError: (error) => {
        // Global mutation error handler
        showToast({ variant: "error", title: "Action failed", description: error.message });
      },
    },
  },
});
  • Dashboard data: staleTime: 60s ensures freshness without excessive refetching.
  • Auth pages: refetchOnWindowFocus: false to prevent session polling on login pages.
  • Mutations: No retry; immediate feedback via toast.
  • Cache invalidation: After practice.update, location.create, billing.createSubscription, invalidate related queries with utils.practice.get.invalidate().

1.9.5 Error Boundaries#

Every major route segment has a ReactErrorBoundary. The fallback UI is a controlled, non-panicking state that preserves navigation.

Error Boundary Fallback UI:

  • Layout: Centered card, max-width: 400px, padding 24px, border-radius 8px, background surface-card, shadow shadow-sm.
  • Icon: XCircle, 32px, color error-red, centered.
  • Title: font-size-heading-sm, weight medium, "Something went wrong", centered, color text-primary.
  • Description: font-size-body-sm, text-secondary, "We've logged this error and are working on a fix.", centered.
  • Action: Ghost button "Reload page", md size, centered, onClick={() => window.location.reload()}.
  • Secondary action: Link "Go to dashboard", text-link, centered.

Logging: Errors are sent to Sentry with x-request-id, user ID, route path, and component stack trace.

1.10 Responsive Architecture#

Breakpoints are derived from design-tokens.json:

Breakpoint Width Layout Behavior
sm 640px Minor adjustments; single column
md 768px Tablet layout; sidebar collapses to hamburger; 2-column grid becomes 1-column
lg 1024px Small desktop; sidebar visible, content max-width 960px
xl 1280px Standard desktop; sidebar 240px, content max-width 1200px
2xl 1536px Large desktop; content max-width 1200px centered

Sidebar Behavior:

  • xl and above: Fixed left sidebar, 240px width, z-index: 200, always visible, position: fixed.
  • lg and below: Sidebar collapses to a hamburger menu (Menu icon, 24px) in the top-left of the header. Header height 64px.
  • Mobile (md and below): Sidebar becomes a full-screen overlay, 100vw, 100vh, background surface-dark, slides in from left with duration-slow (300ms), easing-enter.
  • Mobile overlay close: Tap backdrop, swipe left (>50px), or tap X icon (X, 24px, top-left, text-inverse).
  • Body scroll: Locked when overlay is open (overflow: hidden on html).

Content Grid:

  • Default: Single column with max-width: 1200px, centered, padding 0 24px.
  • Dashboard: Two-column layout on xl (sidebar 240px + main content fill), single column on lg and below.
  • Marketing pages: Full-width sections with internal max-width: 1200px containers, padding 0 24px.
  • Onboarding wizard: Centered card, max-width: 720px, padding 0 24px.

Touch Targets:

  • All interactive elements must be minimum 44px × 44px on touch devices.
  • Button heights on mobile: 48px minimum.
  • Input heights on mobile: 48px minimum.
  • Table rows on mobile: 56px minimum height.

1.11 Accessibility & Compliance#

1.11.1 WCAG 2.1 AA Compliance#

  • All text meets minimum contrast ratio 4.5:1 against its background.
  • Large text (18px+ or 24px+ headings) meets 3:1.
  • ember-orange on paper-white contrast ratio is 4.6:1 (passes AA).
  • deep-ink on paper-white contrast ratio is 16.2:1 (passes AAA).
  • graphite on paper-white contrast ratio is 7.8:1 (passes AA).
  • slate on paper-white contrast ratio is 4.5:1 (passes AA).
  • fog on paper-white contrast ratio is 3.0:1 (fails AA; only used for disabled states, which are exempt from contrast requirements under WCAG).

1.11.2 Focus Management#

  • Every interactive element has a visible focus ring: shadow-focus (0 0 0 3px rgba(236, 101, 43, 0.28)).
  • Focus ring is removed for mouse users via :focus-visible polyfill.
  • Modal focus trap: react-focus-lock or equivalent. Focus returns to trigger element on close.
  • Skip link: First focusable element on every page is a "Skip to content" link, visually hidden until focused (position: absolute, top: -40px, left: 0, background: deep-ink, color: paper-white, padding: 8px 16px, z-index: 999).
  • Focus order: Logical DOM order; no positive tabindex values.

1.11.3 ARIA Labels & Roles#

  • All icon-only buttons have aria-label describing the action (e.g., aria-label="Close modal").
  • Navigation menus use role="navigation", aria-label="Main navigation".
  • Tables use role="table", headers role="columnheader", rows role="row", cells role="cell".
  • Modals use role="dialog", aria-modal="true", aria-labelledby pointing to title element ID.
  • Toasts use role="status" (non-critical) or role="alert" (critical), aria-live="polite" or assertive.
  • Loading states: aria-busy="true" on forms and buttons during submission.
  • Error messages: aria-describedby on inputs pointing to error message element ID.
  • Current page: aria-current="page" on active sidebar nav item.
  • Breadcrumbs: aria-label="Breadcrumb", nav element.

1.11.4 Reduced Motion Support#

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0ms !important;
    scroll-behavior: auto !important;
  }
}
  • The prefers-reduced-motion flag is read via window.matchMedia and stored in a React context (MotionContext).
  • Components consuming MotionContext disable auto-scrolling carousels, auto-advancing wizards, and parallax effects.
  • Modal entry: Instant opacity change, no translate.
  • Toast entry: Instant, no slide.
  • Page transitions: None.
  • Button hover: Instant color change.

1.11.5 DPDPA 2023 Compliance (Frontend)#

  • Consent checkboxes are never pre-checked (opt-in by default).
  • Consent withdrawal link is present in footer of every authenticated page: "Manage data consent" → /dashboard/settings/privacy.
  • Data export request button is visible in /dashboard/settings for all roles. Triggers compliance/data-export Inngest event.
  • All forms that collect personal data display a "Required" or "Optional" label explicitly.
  • No third-party cookies are loaded before consent is granted (Google Analytics, Meta Pixel, etc.).
  • Cookie banner: Appears on first visit, minimal design, ghost buttons, ember-orange primary action "Accept all".

1.11.6 Screen Reader Support#

  • All images have descriptive alt text. Decorative images use alt="".
  • Charts have aria-label with summary text (e.g., "Citation health chart showing 85% consistency").
  • Form validation errors are announced via aria-live="polite" region.
  • Route changes are announced via aria-live="polite" region with page title.
  • Loading spinners have role="progressbar", aria-valuemin="0", aria-valuemax="100", aria-valuenow="indeterminate".