Skip to content

Investor Interest Signal Strength

Overview

The Investor Interest Signal detects when a viewer is engaging with a pitch deck beyond a casual glance. It uses a combination of behavioral signals derived from existing analytics data. Signals are shown only to the founder — viewers never see them.

Signal Rules

SignalRuleLabel
RevisitedVisitor viewed the deck on ≥ 2 distinct calendar daysRevisited
Multiple viewsVisitor has ≥ 3 total slide view recordsViewed multiple times
Deep readVisitor spent ≥ 20 seconds on ≥ 2 different slidesSpent time on key slides
Quick returnVisitor returned within 3 days of their first visitReturned quickly
Long sessionVisitor's total time across all slides ≥ 60 secondsExtended viewing

Architecture

Data Flow

mermaid
sequenceDiagram
    participant V as Investor (Viewer)
    participant App as ImageDeckViewer
    participant DB as Supabase (deck_page_views)
    participant F as Founder Dashboard

    V->>App: Views slide for 25s
    App->>DB: record_deck_visit(deck, page, total_time, is_unique)
    DB-->>DB: Atomic stats increment + page view insert

    Note over V,DB: Investor returns 2 days later

    V->>App: Views slides again
    App->>DB: syncSlideStats(deck, page, 15)
    DB-->>DB: INSERT new row (new 24h window)

    F->>DB: getVisitorSignals(deckId)
    DB-->>F: Grouped rows by visitor_id
    F->>F: Compute signals client-side
    F->>F: Render badges on DeckAnalytics page

Database Schema Change

sql
-- Two column additions to existing table
ALTER TABLE deck_page_views
ADD COLUMN IF NOT EXISTS time_spent REAL DEFAULT 0;

ALTER TABLE deck_page_views
ADD COLUMN IF NOT EXISTS viewer_email TEXT;

The deck_page_views table now stores:

ColumnTypeDescription
idUUIDPrimary key
deck_idUUIDForeign key to decks
page_numberINTSlide number viewed
visitor_idTEXTPersistent anonymous visitor ID
viewer_emailTEXTEmail captured from AccessGate (nullable)
viewed_atTIMESTAMPWhen the view occurred
time_spentREALSeconds spent on this slide by this visitor
countryTEXTVisitor's country (via Vercel Edge)
cityTEXTVisitor's city (via Vercel Edge)
country_codeTEXTISO country code (e.g., "US")

Signal Computation

Signals are computed client-side, not stored as flags. This keeps the system maintainable, avoids stale data, and allows signal thresholds to be tweaked without migrations.

NOTE

Broadened Scope: Queries now include views where data_room_id is NULL. This allows for "Historical Affinity" tracking — if a visitor views a deck independently and then later views it inside a private Data Room, their full engagement history is combined to calculate the interest signal.

deck_page_views (grouped by visitor_id)


interestSignalService.getVisitorSignals(deckId) OR getRoomVisitorSignals(roomId)

        ├──► filter(Boolean) + empty-guard   → Prevent malformed queries
        ├──► Include null data_room_id       → Capture historical affinity
        ├──► Count distinct viewed_at dates  → "Revisited"
        ├──► Count total rows               → "Viewed multiple times"
        ├──► Filter slides with time_spent≥20 → "Spent time on key slides"
        ├──► Compare first/last visit dates  → "Returned quickly"
        └──► Sum all time_spent             → "Extended viewing"


    VisitorSignal[] (sorted by signal count)

File Map

FileRole
src/services/analyticsService.tsModified — syncSlideStats() now uses record_deck_visit RPC for secure, atomic updates.
src/services/interestSignalService.tsNew — queries deck_page_views, computes signals + slide breakdown per deck or room
src/components/dashboard/InterestSignalBadge.tsxNew — color-coded neutral badge component
src/pages/DeckAnalytics.tsxModified — "Visitor Engagement Signals" section with expandable slide time charts
src/pages/DataRoomDetail.tsxModified — "Visitor Signals" section aggregated across all assets in the room
src/components/dashboard/TopDecksCard.tsxModified — shows "N interested viewers" hint per deck
src/pages/Viewer.tsxModified — stores captured email, passes to ImageDeckViewer
src/components/ImageDeckViewer.tsxModified — accepts viewerEmail prop, passes to syncSlideStats

UI Design

  • DeckAnalytics page: A DashboardCard section titled "Visitor Engagement Signals" lists each visitor (email or anonymous) with their signal badges
  • Expandable slide breakdown: Click a visitor card to reveal a vertical bar chart showing per-slide time spent
  • TopDecksCard: Subtle "N interested viewer(s)" text below deck titles
  • Badge styles: Each signal type has a unique color — blue (revisit), indigo (multiple views), green (deep read), amber (quick return), violet (extended viewing)
  • Language: All labels use neutral, factual language (no "hot lead" or "high interest")
  • Email display: Shows actual email when captured (e.g. john@acme.com), falls back to "Visitor #N" for anonymous viewers

Future Enhancements

  • [x] Email-based visitor identity (when Require Email is enabled)
  • [x] Per-visitor slide time breakdown (expandable vertical bar chart)
  • [ ] "Viewed updated version" signal
  • [ ] Push notifications to founder when a new signal triggers
  • [ ] Signal strength score (weighted combination of all signals)

Built with ❤️ for Founders