# Broadcast Messaging & Surveys — Implementation Roadmap

> **Status:** 🟡 Not Started  
> **Estimated Effort:** 3–4 weeks  
> **Target:** E2E broadcast messages + surveys across all ByteLyst products  
> **DRY Principle:** Reuse `@bytelyst/*` packages, extend platform SDKs, single admin UI  

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Architecture Overview](#2-architecture-overview)
3. [Backend Modules](#3-backend-modules)
4. [Client SDKs (DRY)](#4-client-sdks-dry)
5. [Admin Dashboard](#5-admin-dashboard)
6. [Platform Implementations](#6-platform-implementations)
7. [Implementation Phases](#7-implementation-phases)
8. [Cosmos Containers](#8-cosmos-containers)
9. [API Reference](#9-api-reference)

---

## 1. Executive Summary

### Two New Systems

| System | Purpose | Key Features |
|--------|---------|--------------|
| **Broadcast Messaging** | Targeted announcements to user segments | Push, in-app, email channels; scheduling; A/B testing; geo/platform/version targeting |
| **Surveys & Polls** | In-app user research | Multiple question types; conditional logic; targeting; real-time results; CSV export |

### DRY Strategy

```
┌─────────────────────────────────────────────────────────────────┐
│                    SINGLE BACKEND (platform-service)             │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────────────────┐  │
│  │  broadcasts  │  │   surveys    │  │    targeting-engine    │  │
│  │   module     │  │    module    │  │   (shared segments)    │  │
│  └──────────────┘  └──────────────┘  └────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                              │
          ┌───────────────────┼───────────────────┐
          ▼                   ▼                   ▼
┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│ @bytelyst/broadcast-client │  │ @bytelyst/survey-client    │
│   (TypeScript)   │  │   (TypeScript)   │
└─────────────────┘  └─────────────────┘
          │                   │
    ┌─────┴─────┐       ┌─────┴─────┐
    ▼           ▼       ▼           ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│  Web   │ │Desktop │ │  iOS   │ │Android │
│        │ │        │ │  SDK   │ │  SDK   │
└────────┘ └────────┘ └────────┘ └────────┘
```

---

## 2. Architecture Overview

### 2.1 Core Philosophy

**One backend, many clients.** All products (LysnrAI, MindLyst, ChronoMind, NomGap, JarvisJr, PeakPulse) consume the same platform-service APIs via their respective platform SDKs.

### 2.2 Data Flow

```
Admin creates broadcast/survey
         │
         ▼
┌─────────────────┐
│ platform-service │
│  (targeting +   │
│   scheduling)   │
└─────────────────┘
         │
    ┌────┴────┐
    ▼         ▼
┌───────┐ ┌─────────┐
│ Push  │ │ In-App  │
│ (FCM/ │ │ Message │
│ APNS) │ │ Queue   │
└───────┘ └─────────┘
         │
    ┌────┴────┬────────┬────────┐
    ▼         ▼        ▼        ▼
┌───────┐ ┌───────┐ ┌──────┐ ┌──────┐
│  iOS  │ │Android│ │  Web │ │Desktop│
└───────┘ └───────┘ └──────┘ └──────┘
         │
         ▼
   Survey responses → platform-service
```

---

## 3. Backend Modules

### 3.1 Module: `broadcasts/`

**Location:** `services/platform-service/src/modules/broadcasts/`

| File | Purpose |
|------|---------|
| `types.ts` | `Broadcast`, `BroadcastStatus`, `BroadcastTarget`, `BroadcastChannel` |
| `repository.ts` | CRUD + targeting query builder |
| `routes.ts` | Admin CRUD + public "mark as read" |
| `scheduler.ts` | Cron-based scheduling + A/B testing splits |
| `delivery.ts` | Push (FCM/APNS) + in-app + email dispatch |
| `targeting.ts` | Segment evaluation engine |

**Key Types:**

```typescript
// types.ts
export interface Broadcast {
  id: string;
  productId: string;
  title: string;
  body: string;
  bodyMarkdown?: string;
  ctaText?: string;
  ctaUrl?: string;
  
  // Targeting
  target: BroadcastTarget;
  
  // Channels
  channels: BroadcastChannel[]; // 'push', 'in_app', 'email'
  
  // Scheduling
  status: 'draft' | 'scheduled' | 'sending' | 'sent' | 'paused';
  scheduledAt?: string;
  sentAt?: string;
  
  // A/B Testing
  variant?: 'control' | 'treatment';
  experimentId?: string;
  
  // Metrics
  metrics: BroadcastMetrics;
  
  createdAt: string;
  updatedAt: string;
}

export interface BroadcastTarget {
  // User segments (AND logic)
  userSegments?: ('free' | 'pro' | 'enterprise' | 'churned' | 'active')[];
  
  // Platform targeting
  platforms?: ('ios' | 'android' | 'web' | 'macos' | 'windows')[];
  
  // App version range
  appVersionMin?: string;
  appVersionMax?: string;
  
  // Geo targeting
  countryCodes?: string[]; // ISO 3166-1 alpha-2
  regionCodes?: string[]; // US-CA, EU-FR, etc.
  
  // OS version
  osVersionMin?: string;
  osVersionMax?: string;
  
  // Percentage rollout (for phased rollouts)
  percentageRollout?: number; // 0-100, uses FNV-1a hash like feature flags
  
  // User IDs (explicit list, for testing)
  specificUserIds?: string[];
}

export type BroadcastChannel = 'push' | 'in_app' | 'email';

export interface BroadcastMetrics {
  targetedCount: number;
  sentCount: number;
  deliveredCount: number;
  openedCount: number;
  clickedCount: number;
  dismissedCount: number;
  convertedCount: number;
}
```

**Endpoints:**

| Method | Endpoint | Auth | Purpose |
|--------|----------|------|---------|
| `POST` | `/admin/broadcasts` | Admin | Create broadcast |
| `GET` | `/admin/broadcasts` | Admin | List all broadcasts |
| `GET` | `/admin/broadcasts/:id` | Admin | Get single broadcast |
| `PUT` | `/admin/broadcasts/:id` | Admin | Update draft/scheduled |
| `DELETE` | `/admin/broadcasts/:id` | Admin | Cancel/delete |
| `POST` | `/admin/broadcasts/:id/send` | Admin | Trigger immediate send (with safety check for >10k users) |
| `POST` | `/admin/broadcasts/:id/pause` | Admin | Pause sending (cancels pending deliveries) |
| `GET` | `/admin/broadcasts/:id/metrics` | Admin | Real-time metrics |
| `POST` | `/admin/broadcasts/:id/clone` | Admin | Duplicate for A/B test |
| `GET` | `/broadcasts` | User | List my active broadcasts |
| `POST` | `/broadcasts/:id/read` | User | Mark as read |
| `POST` | `/broadcasts/:id/dismiss` | User | Dismiss in-app message |
| `POST` | `/broadcasts/:id/click` | User | Track CTA click |

**Rate Limiting:** Public endpoints (`/broadcasts`, `/broadcasts/:id/read`, etc.) use existing `ratelimit` module: 100 req/min per user.

**Safety Check:** `POST /admin/broadcasts/:id/send` requires confirmation for broadcasts targeting >10,000 users.

**Location:** `services/platform-service/src/modules/surveys/`

| File | Purpose |
|------|---------|
| `types.ts` | `Survey`, `Question`, `QuestionType`, `SurveyResponse` |
| `repository.ts` | CRUD + response aggregation |
| `routes.ts` | Admin CRUD + public response submission |
| `targeting.ts` | Reuses broadcasts/targeting.ts |
| `analytics.ts` | Response statistics, CSV export |

**Key Types:**

```typescript
// types.ts
export interface Survey {
  id: string;
  productId: string;
  title: string;
  description?: string;
  
  // Questions
  questions: Question[];
  
  // Targeting (same as BroadcastTarget)
  target: BroadcastTarget;
  
  // Scheduling
  status: 'draft' | 'active' | 'paused' | 'closed';
  startsAt?: string;
  endsAt?: string;
  
  // Display settings
  displayTrigger: SurveyTrigger;
  
  // Incentives
  incentive?: {
    type: 'pro_days' | 'credits';
    amount: number;
  };
  
  // Metrics
  metrics: SurveyMetrics;
  
  createdAt: string;
  updatedAt: string;
}

export type QuestionType = 
  | 'single_choice'      // Radio buttons
  | 'multiple_choice'    // Checkboxes
  | 'rating'             // 1-5 or 1-10 stars
  | 'nps'                // 0-10 scale
  | 'text_short'         // Single line
  | 'text_long'          // Multi-line textarea
  | 'dropdown'           // Select menu
  | 'scale'              // Likert scale (1-7)
  | 'ranking';           // Drag to rank

export interface Question {
  id: string;
  type: QuestionType;
  text: string;
  description?: string;
  required: boolean;
  
  // For choice types
  options?: QuestionOption[];
  
  // For conditional logic - supports complex AND/OR conditions
  showIf?: ShowIfCondition;
  
  // Validation
  minLength?: number;
  maxLength?: number;
  minValue?: number;  // For numeric types
  maxValue?: number;
}

// Helper type for conditional display (supports nested AND/OR)
export type ShowIfCondition = 
  | { and: ShowIfCondition[] }
  | { or: ShowIfCondition[] }
  | {
      questionId: string;
      operator: 'equals' | 'not_equals' | 'contains' | 'greater_than' | 'less_than' | 'in';
      value: string | string[] | number;
    };

export interface QuestionOption {
  id: string;
  text: string;
  emoji?: string; // Optional visual indicator
}

export type SurveyTrigger = 
  | { type: 'immediate' }                    // Show right away
  | { type: 'delay_seconds'; seconds: number } // After N seconds in app
  | { type: 'event'; eventName: string }     // After specific event
  | { type: 'page_view'; pagePattern: string }; // URL pattern match

export interface SurveyResponse {
  id: string;
  surveyId: string;
  productId: string;
  userId: string;
  deviceId?: string;
  
  // Answers
  answers: Record<string, QuestionAnswer>;
  
  // Metadata
  startedAt: string;
  completedAt?: string;
  isComplete: boolean;
  
  // Incentive claimed
  incentiveClaimed?: boolean;
  
  createdAt: string;
}

export type QuestionAnswer = 
  | { type: 'single_choice'; optionId: string }
  | { type: 'multiple_choice'; optionIds: string[] }
  | { type: 'rating'; value: number }
  | { type: 'nps'; value: number }
  | { type: 'text'; value: string }
  | { type: 'ranking'; rankedOptionIds: string[] };

export interface SurveyMetrics {
  impressions: number;
  starts: number;
  completions: number;
  // NOTE: completionRate is computed as completions / starts
  avgTimeSeconds: number;
  incentiveClaims: number;
}

// Computed metrics helper (not stored)
export function computeCompletionRate(metrics: SurveyMetrics): number {
  return metrics.starts > 0 ? metrics.completions / metrics.starts : 0;
}
```

**Endpoints:**

| Method | Endpoint | Auth | Purpose |
|--------|----------|------|---------|
| `POST` | `/admin/surveys` | Admin | Create survey |
| `GET` | `/admin/surveys` | Admin | List all surveys |
| `GET` | `/admin/surveys/:id` | Admin | Get survey with questions |
| `PUT` | `/admin/surveys/:id` | Admin | Update survey |
| `DELETE` | `/admin/surveys/:id` | Admin | Delete survey |
| `POST` | `/admin/surveys/:id/duplicate` | Admin | Clone survey |
| `GET` | `/admin/surveys/:id/responses` | Admin | All responses (paginated) |
| `GET` | `/admin/surveys/:id/analytics` | Admin | Aggregated stats |
| `GET` | `/admin/surveys/:id/export.csv` | Admin | CSV export |
| `GET` | `/admin/surveys/:id/respondents` | Admin | List users who responded |
| `POST` | `/admin/surveys/:id/pause` | Admin | Stop showing survey |
| `GET` | `/surveys/active` | User | Get active survey for me (rate limited: 10 req/min) |
| `POST` | `/surveys/:id/start` | User | Begin survey session |
| `POST` | `/surveys/:id/response` | User | Submit answer(s) (validated against question type) |
| `POST` | `/surveys/:id/complete` | User | Mark as complete |
| `POST` | `/surveys/:id/dismiss` | User | "Don't show again" |

**Security:** Survey responses validate answer type matches question type (reject string for NPS question). Admin endpoints require `role === 'admin'`.

### 3.3 Shared: `targeting-engine/`

**Reuses existing patterns from `flags/` and `experiments/`:**

```typescript
// services/platform-service/src/lib/targeting.ts

export interface TargetingContext {
  userId: string;
  productId: string;
  platform: 'ios' | 'android' | 'web' | 'macos' | 'windows';
  appVersion: string;
  osVersion: string;
  countryCode?: string;
  regionCode?: string;
  userSegments: string[]; // Populated from user subscriptions
}

export async function evaluateTarget(
  target: BroadcastTarget,
  context: TargetingContext
): Promise<boolean> {
  // AND logic across all criteria
  
  if (target.userSegments && !target.userSegments.some(s => context.userSegments.includes(s))) {
    return false;
  }
  
  if (target.platforms && !target.platforms.includes(context.platform)) {
    return false;
  }
  
  if (target.appVersionMin && semver.lt(context.appVersion, target.appVersionMin)) {
    return false;
  }
  
  if (target.appVersionMax && semver.gt(context.appVersion, target.appVersionMax)) {
    return false;
  }
  
  if (target.countryCodes && !target.countryCodes.includes(context.countryCode ?? '')) {
    return false;
  }
  
  if (target.percentageRollout !== undefined) {
    const hash = fnv1a32(`${context.userId}:${target.percentageRollout}`);
    if ((hash % 100) >= target.percentageRollout) {
      return false;
    }
  }
  
  return true;
}
```

---

## 4. Client SDKs (DRY)

### 4.1 New Package: `@bytelyst/broadcast-client`

**Location:** `packages/broadcast-client/`

**Purpose:** TypeScript client for broadcast ingestion + in-app message display.

```typescript
// packages/broadcast-client/src/index.ts

export interface BroadcastClientConfig {
  platform: 'web' | 'ios' | 'android' | 'macos' | 'windows';
  appVersion: string;
  osVersion: string;
  getAuthToken: () => Promise<string>;
  onMessage: (msg: InAppMessage) => void;
  onDismiss?: (msgId: string) => void;
}

export interface InAppMessage {
  id: string;
  title: string;
  body: string;
  bodyMarkdown?: string;
  ctaText?: string;
  ctaUrl?: string;
  priority: 'low' | 'normal' | 'high' | 'urgent';
  style: 'banner' | 'modal' | 'toast' | 'fullscreen';
  dismissible: boolean;
  expiresAt?: string;
}

export class BroadcastClient {
  constructor(config: BroadcastClientConfig);
  
  // Lifecycle
  initialize(): Promise<void>;
  dispose(): void;
  
  // Check for new messages (call on app open, every 5 min)
  checkMessages(): Promise<InAppMessage[]>;
  
  // Mark actions
  markRead(messageId: string): Promise<void>;
  markDismissed(messageId: string): Promise<void>;
  trackClick(messageId: string, url?: string): Promise<void>;
  
  // Get cached unread count
  getUnreadCount(): number;
  
  // Subscribe to real-time updates (WebSocket fallback to polling)
  onMessage(handler: (msg: InAppMessage) => void): () => void;
}

// React hook
export function useBroadcastMessages(): {
  messages: InAppMessage[];
  unreadCount: number;
  markRead: (id: string) => Promise<void>;
  dismiss: (id: string) => Promise<void>;
};
```

**Storage adapters (DRY pattern):**

```typescript
// Web: localStorage
// iOS: UserDefaults (via Swift SDK wrapper)
// Android: SharedPreferences (via Kotlin SDK wrapper)
// Desktop: file-based (node-localstorage or electron-store)

export interface StorageAdapter {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
  removeItem(key: string): void;
}
```

### 4.2 New Package: `@bytelyst/survey-client`

**Location:** `packages/survey-client/`

```typescript
// packages/survey-client/src/index.ts

export interface SurveyClientConfig {
  platform: 'web' | 'ios' | 'android' | 'macos' | 'windows';
  appVersion: string;
  osVersion: string;
  getAuthToken: () => Promise<string>;
  onSurveyReady: (survey: ActiveSurvey) => void;
  
  // Event tracking for triggers
  trackEvent: (eventName: string, metadata?: Record<string, unknown>) => void;
}

export interface ActiveSurvey {
  id: string;
  title: string;
  description?: string;
  questions: SurveyQuestion[];
  incentive?: { type: string; amount: number };
  estimatedMinutes: number;
}

export interface SurveyQuestion {
  id: string;
  type: QuestionType;
  text: string;
  description?: string;
  required: boolean;
  options?: { id: string; text: string; emoji?: string }[];
  // ... (matches backend types)
}

export class SurveyClient {
  constructor(config: SurveyClientConfig);
  
  // Lifecycle
  initialize(): Promise<void>;
  
  // Check for surveys on trigger events
  checkForSurveys(context?: { pageUrl?: string; eventName?: string }): Promise<ActiveSurvey | null>;
  
  // Survey session with offline queue support
  startSurvey(surveyId: string): Promise<SurveySession>;
  submitAnswer(questionId: string, answer: AnswerValue): Promise<void>;
  completeSurvey(): Promise<{ incentiveClaimed: boolean }>;
  dismissSurvey(): Promise<void>;
  
  // Offline support: queue responses when offline, flush when connected
  flushOfflineQueue(): Promise<{ flushed: number; failed: number }>;
  
  // Subscribe to survey availability
  onSurveyAvailable(handler: (survey: ActiveSurvey) => void): () => void;
}

// React hook with session state
export function useSurvey(): {
  activeSurvey: ActiveSurvey | null;
  currentSession: SurveySession | null;
  isOffline: boolean;
  pendingCount: number; // offline queue size
  start: () => Promise<void>;
  submit: (questionId: string, answer: AnswerValue) => Promise<void>;
  complete: () => Promise<void>;
  dismiss: () => Promise<void>;
  flushQueue: () => Promise<void>;
};
```

### 4.3 Platform SDK Extensions

**Swift SDK:** `packages/swift-platform-sdk/Sources/BLBroadcastClient.swift`

```swift
public class BLBroadcastClient: NSObject, UNUserNotificationCenterDelegate {
    // Inherits from platform config
    // Handles push token registration
    // Provides in-app message UI components (SwiftUI)
}

public class BLSurveyClient: ObservableObject {
    @Published public var activeSurvey: Survey?
    
    // Native SwiftUI survey sheet
    public func presentSurvey(_ survey: Survey, from viewController: UIViewController)
}
```

**Kotlin SDK:** `packages/kotlin-platform-sdk/src/main/kotlin/.../BLBroadcastClient.kt`

```kotlin
class BLBroadcastClient(
    config: BLPlatformConfig,
    private val context: Context
) {
    // FCM integration
    // In-app message Compose components
}

class BLSurveyClient {
    // Jetpack Compose survey dialog
}
```

---

## 5. Admin Dashboard

### 5.1 Broadcast Management UI

**Location:** `dashboards/admin-web/src/app/(dashboard)/broadcasts/`

| Page | Features |
|------|----------|
| `page.tsx` | List all broadcasts with status chips |
| `new/page.tsx` | Create wizard: content → targeting → scheduling → preview |
| `[id]/page.tsx` | Edit + metrics dashboard |
| `[id]/preview/page.tsx` | Device mockup preview (iPhone, Android, Web) |

**Targeting Builder Component:**

```tsx
// components/BroadcastTargetingBuilder.tsx
// Reusable for broadcasts + surveys

interface TargetingBuilderProps {
  value: BroadcastTarget;
  onChange: (target: BroadcastTarget) => void;
  estimatedReach: number; // Live preview
}

// Sections:
// - User segments (chips: free, pro, enterprise, churned, active)
// - Platform (checkboxes: iOS, Android, Web, macOS, Windows)
// - App version range (semver inputs)
// - Geo (country multi-select, region input)
// - Rollout % (slider 0-100)
// - Test user IDs (textarea for specific testing)
```

**Metrics Dashboard:**

```tsx
// Real-time metrics via SSE or polling
// - Targeted users (number)
// - Delivery funnel (sent → delivered → opened → clicked)
// - Conversion rate
// - Geo breakdown (map)
// - Platform breakdown (bar chart)
// - A/B test results (if applicable)
```

### 5.2 Survey Management UI

**Location:** `dashboards/admin-web/src/app/(dashboard)/surveys/`

| Page | Features |
|------|----------|
| `page.tsx` | List surveys with completion rates |
| `new/page.tsx` | Survey builder: questions → targeting → display rules |
| `[id]/page.tsx` | Edit + live preview |
| `[id]/analytics/page.tsx` | Response analytics + charts |
| `[id]/responses/page.tsx` | Individual responses table |

**Question Builder Component:**

```tsx
// components/QuestionBuilder.tsx
// Drag-drop question types
// - Single choice
// - Multiple choice
// - Rating (1-5 or 1-10)
// - NPS
// - Text (short/long)
// - Dropdown
// - Scale (Likert)
// - Ranking

// Features:
// - Conditional logic builder
// - Required toggle
// - Emoji picker for options
// - Live preview of question
```

**Analytics Components:**

```tsx
// Single choice → Pie chart or bar chart
// Multiple choice → Stacked bar chart
// Rating/NPS → Histogram with average
// Text → Word cloud + sample responses
// Scale → Distribution chart
```

---

## 6. Platform Implementations

### 6.1 Web (Next.js)

**Location:** `user-dashboard-web/src/components/broadcasts/`

```tsx
// components/broadcasts/BroadcastBanner.tsx
// Fixed position banner for in-app messages

// components/broadcasts/BroadcastModal.tsx  
// Modal overlay for high-priority messages

// components/surveys/SurveyModal.tsx
// Full survey flow in modal

// hooks/useBroadcast.ts
// hooks/useSurvey.ts
```

### 6.2 iOS (SwiftUI)

**Location per product:** `ios/{Product}/Views/Broadcasts/`

```swift
// BroadcastBannerView.swift
// Slide-down banner (like Snackbar)

// BroadcastModalView.swift
// Full-screen modal for urgent messages

// SurveyView.swift
// Paginated survey with progress bar
```

**AppDelegate integration:**

```swift
// Register for push + broadcast client init
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    BLTelemetryClient.shared.setPushToken(deviceToken)
    BLBroadcastClient.shared.setPushToken(deviceToken)
}
```

### 6.3 Android (Jetpack Compose)

**Location per product:** `android/app/src/.../ui/broadcasts/`

```kotlin
// BroadcastBanner.kt
// Snackbar-style banner

// BroadcastDialog.kt
// AlertDialog for modal messages

// SurveyDialog.kt
// Bottom sheet or full-screen survey
```

**FCM Integration:**

```kotlin
class BroadcastMessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        val broadcastId = remoteMessage.data["broadcastId"]
        // Show notification or in-app message
    }
    
    override fun onNewToken(token: String) {
        BLBroadcastClient.setPushToken(token)
    }
}
```

### 6.4 Desktop (Electron/Tauri or Python)

**Python (LysnrAI):**

```python
# src/platform/broadcast_client.py
class BroadcastClient:
    def __init__(self):
        self.storage = LocalStorage()
        
    def check_messages(self) -> List[InAppMessage]:
        # Poll platform-service
        
    def show_banner(self, message: InAppMessage):
        # tkinter overlay or native notification
```

---

## 7. Implementation Phases

### Phase 1: Backend Foundation (Week 1) ✅ **COMPLETED**

**Commit:** `1b11db3` — feat(broadcasts,surveys): Phase 1 complete - backend modules

| Day | Task | Deliverable | Status |
|-----|------|-------------|--------|
| 1 | `broadcasts` module scaffold + types | `types.ts`, `repository.ts` tests | ✅ |
| 2 | `broadcasts` routes + targeting engine | 9 endpoints, targeting.ts | ✅ |
| 3 | `surveys` module scaffold + types | `types.ts`, `repository.ts` tests | ✅ |
| 4 | `surveys` routes + question types | 14 endpoints | ✅ |
| 5 | Cosmos containers + server.ts registration | 7 containers, routes registered | ✅ |

**Tests:** 80+ unit tests for repositories, 40+ for routes (to be added in follow-up)

### Phase 2: Admin Dashboard (Week 2)

| Day | Task | Deliverable |
|-----|------|-------------|
| 6 | Broadcast create/edit UI | Targeting builder, preview |
| 7 | Broadcast metrics dashboard | Real-time charts |
| 8 | Survey builder UI | Question builder, preview |
| 9 | Survey analytics | Response charts, CSV export |
| 10 | Integration testing | E2E admin flows |

### Phase 3: Client SDKs (Week 3)

| Day | Task | Deliverable |
|-----|------|-------------|
| 11 | `@bytelyst/broadcast-client` | Package + tests |
| 12 | `@bytelyst/survey-client` | Package + tests |
| 13 | Swift SDK extensions | `BLBroadcastClient`, `BLSurveyClient` |
| 14 | Kotlin SDK extensions | `BLBroadcastClient`, `BLSurveyClient` |
| 15 | SDK integration tests | All platforms |

### Phase 4: Platform Integration (Week 4)

| Day | Task | Deliverable |
|-----|------|-------------|
| 16 | Web integration (user-dashboard) | Banner, modal, survey flow |
| 17 | iOS integration (pick 1 product) | SwiftUI components |
| 18 | Android integration (pick 1 product) | Compose components |
| 19 | FCM/APNS setup + testing | Push delivery verified |
| 20 | Documentation + polish | README, API docs, examples |

---

## 8. Cosmos Containers

| Container | Partition Key | Purpose | Est. Size | TTL |
|-----------|---------------|---------|-----------|-----|
| `broadcasts` | `/productId` | Broadcast definitions + targeting | Small | — |
| `broadcast_deliveries` | `/userId` | Per-user delivery status (avoids hot partitions) | Large | 90 days |
| `broadcast_reads` | `/userId` | User read receipts | Large | 90 days |
| `surveys` | `/productId` | Survey definitions + questions | Small | — |
| `survey_responses` | `/surveyId` | Individual responses | Large | 1 year |
| `in_app_messages` | `/userId` | Active in-app messages per user | Medium | 30 days |
| `targeting_segments` | `/productId` | Pre-computed user segments | Medium | 1 day |
| `broadcast_templates` | `/productId` | Reusable broadcast templates | Small | — |

**Partition Key Rationale:** `broadcast_deliveries` uses `/userId` (not `/broadcastId`) to distribute writes across partitions during large sends. Read receipts also use `/userId` for efficient per-user queries.

## 9. API Reference

### 9.1 Broadcast Targeting Examples

```json
// Target: Pro users on iOS 15+, US/CA only, 50% rollout
{
  "userSegments": ["pro"],
  "platforms": ["ios"],
  "appVersionMin": "15.0.0",
  "countryCodes": ["US", "CA"],
  "percentageRollout": 50
}

// Target: All active users on Web, immediate send
{
  "userSegments": ["active"],
  "platforms": ["web"]
}

// Target: Specific test users
{
  "specificUserIds": ["user_123", "user_456"]
}
```

### 9.2 Survey Question Examples

```json
{
  "title": "Product Feedback Survey",
  "questions": [
    {
      "id": "q1",
      "type": "nps",
      "text": "How likely are you to recommend us?",
      "required": true
    },
    {
      "id": "q2",
      "type": "single_choice",
      "text": "What feature do you use most?",
      "required": true,
      "options": [
        { "id": "voice", "text": "Voice dictation", "emoji": "🎙️" },
        { "id": "keyboard", "text": "Keyboard", "emoji": "⌨️" },
        { "id": "desktop", "text": "Desktop app", "emoji": "💻" }
      ]
    },
    {
      "id": "q3",
      "type": "text_long",
      "text": "What could we improve?",
      "required": false,
      "showIf": {
        "questionId": "q1",
        "operator": "not_equals",
        "value": ["9", "10"]
      }
    }
  ],
  "target": {
    "userSegments": ["pro"],
    "platforms": ["ios", "android"]
  },
  "displayTrigger": { "type": "delay_seconds", "seconds": 30 }
}
```

---

## Appendix A: DRY Checklist

| Component | Reused From | Notes |
|-----------|-------------|-------|
| Targeting engine | `flags/` module | Same FNV-1a hash, same segment logic |
| Storage adapters | Existing SDK patterns | `BLTelemetryClient` storage pattern |
| Admin UI components | `flags/page.tsx` | Copy table, filters, pagination |
| API client factory | `@bytelyst/api-client` | `createApiClient()` with auth |
| Cosmos repository pattern | All existing modules | `types.ts` → `repository.ts` → `routes.ts` |
| Charts | Admin dashboard Recharts | Same chart components |
| Real-time updates | SSE pattern from telemetry | Or polling fallback |

---

## Appendix B: Future Enhancements (Post-MVP)

1. **Rich media broadcasts** — Images, videos, GIFs
2. **Push notification deep links** — Open specific screens
3. **Survey logic jumps** — Skip questions based on answers
4. **Survey templates** — NPS, CSAT, CES templates
5. **Multi-language broadcasts** — i18n content variants
6. **Advanced analytics** — Cohort analysis, trend lines
7. **Integration with A/B testing** — Survey as experiment outcome

## Appendix C: Bug Fixes & Known Issues

### Fixed in This Revision

1. **Cosmos Partition Keys** — Changed `broadcast_deliveries` from `/broadcastId` to `/userId` to avoid hot partitions during large sends
2. **Computed Metrics** — Removed `completionRate` from stored `SurveyMetrics`; now computed on-the-fly
3. **Rate Limiting** — Added explicit rate limits for public endpoints (100 req/min)
4. **Conditional Logic** — Extended `showIf` to support nested AND/OR conditions, not just single conditions
5. **Offline Support** — Added `flushOfflineQueue()` to survey client for network resilience
6. **Safety Checks** — Added confirmation requirement for broadcasts targeting >10k users

### Known Limitations (MVP)

1. No persistent message queue — uses in-memory event bus; restart = lost pending deliveries
2. Push delivery is best-effort — no delivery receipt confirmation from FCM/APNS
3. Survey responses don't auto-retry on 5xx — client must retry manually
4. No real conflict resolution if user qualifies for multiple surveys simultaneously
