docs/architecture/demo-app-and-tier-architecture.md

Demo App and Tier Architecture

Last verified: 2026-03-02 Target: apps/library, apps/demo Companion: packages/app-library

This document describes the standalone demo app (apps/demo) and the tier-based package structure for @repo/app-library.

Overview

The demo app provides a unified demonstration experience that runs without environment variables or authentication. It consolidates demo pages from both the hello and library apps into a single, standalone application.

Architecture Diagram

Demo App (apps/demo)

Purpose

  • Standalone demos: Run without Clerk authentication or environment variables
  • Unified experience: Single app for all demo flows across the product
  • Development tool: Quick iteration on UI without full stack setup
  • Sales/Marketing: Shareable demo links without account requirements

Structure

apps/demo/
├── app/
│   ├── layout.tsx           # No auth, minimal providers
│   ├── page.tsx             # Landing with links to demos
│   ├── hello/               # Hello app demos
│   │   ├── page.tsx         # Hello onboarding flow
│   │   ├── objective/
│   │   ├── video-teaser/
│   │   └── ...
│   └── library/             # Library app demos
│       ├── page.tsx
│       ├── free/            # Free tier demo
│       └── paid/            # Paid tier demo
├── package.json
├── next.config.mjs
└── README.md

Key Characteristics

  1. No Authentication: No Clerk, no auth middleware
  2. No Environment Variables: Uses mock data throughout
  3. Shared Components: Imports from @repo/app-library, @repo/app-hello, @repo/demo-flow
  4. Mock Data: Uses @repo/registries/server/mocks for realistic data

Dependencies

{
  "dependencies": {
    "@repo/app-hello": "workspace:*",
    "@repo/app-library": "workspace:*",
    "@repo/demo-flow": "workspace:*",
    "@repo/design-system": "workspace:*"
  }
}

Package Structure: @repo/app-library

The @repo/app-library package uses a tier-based organization to cleanly separate free and paid functionality while sharing common code.

Folder Structure

packages/app-library/
├── src/
│   ├── index.ts              # Barrel for shared only
│   ├── shared/               # Layout, navigation, types, helpers — used by all tiers
│   │   ├── index.ts
│   │   ├── types.ts              # Shared types (VideoInfo, FlowInfo, etc.)
│   │   ├── transformers.ts       # toFlowInfo(), toVideoInfo(), toRespondentViewData()
│   │   ├── mocks.ts              # Shared mock data
│   │   ├── LibraryLayoutShell.tsx  # Main layout shell (collapsible sidebar + content)
│   │   ├── AppSidebar.tsx          # Collapsible sidebar navigation
│   │   ├── MobileNav.tsx           # Mobile bottom tab navigation
│   │   ├── ProfileMenu.tsx         # User profile/org menu (Clerk-free UI component)
│   │   ├── InterviewsView.tsx      # Unified interviews card grid (all tiers)
│   │   ├── InterviewsSkeleton.tsx  # Loading skeleton for interviews view
│   │   ├── ShareFlowDialog.tsx     # Share interview link dialog
│   │   ├── OnboardingNudges.tsx    # Empty-state nudge cards
│   │   ├── InviteTemplatesDialog.tsx # Invite email template picker
│   │   ├── AnimatedThumbnail.tsx   # Static JPEG → GIF crossfade thumbnail
│   │   ├── extract-preload-thumbnails.ts # Server-side image preloading
│   │   ├── video-helpers.ts        # getThumbnailUrl, getBestVideoSource, etc.
│   │   └── csv-export.ts           # CSV generation and download
│   ├── free/                 # Free tier specific
│   │   ├── index.ts          # Exports free components + re-exports shared
│   │   ├── FlowsListView.tsx
│   │   ├── FlowManageUI.tsx
│   │   └── OrganizationSelectorUI.tsx
│   └── paid/                 # Paid tier specific
│       ├── index.ts          # Exports paid components + re-exports shared
│       ├── RespondentViewUI.tsx
│       ├── UserRecentVideos.tsx
│       ├── VideosDashboardWithTabs.tsx
│       ├── VideoDashboardGrid.tsx
│       ├── FinalVideosSection.tsx
│       ├── FinalVideosEmptyState.tsx
│       ├── FlowSidebar.tsx
│       ├── MainNavUI.tsx
│       ├── MainNavLinksClient.tsx
│       ├── PaidLayoutHeader.tsx
│       └── useFinalVideoSelection.ts
└── package.json

Import Patterns

// Free tier pages - gets free components + shared
import { FlowsListView, VideoInfo } from "@repo/app-library/free";

// Paid tier pages - gets paid components + shared
import { VideosDashboard, VideoInfo } from "@repo/app-library/paid";

// Just shared stuff (optional convenience)
import { VideoInfo, toVideoInfo } from "@repo/app-library";
import { transformers } from "@repo/app-library/shared";

Package Exports

{
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./dist/index.js"
    },
    "./shared": {
      "types": "./src/shared/index.ts",
      "import": "./dist/shared/index.js"
    },
    "./free": {
      "types": "./src/free/index.ts",
      "import": "./dist/free/index.js"
    },
    "./paid": {
      "types": "./src/paid/index.ts",
      "import": "./dist/paid/index.js"
    }
  }
}

What Goes Where

LocationContents
/sharedLayout (LibraryLayoutShell, AppSidebar, MobileNav), navigation (ProfileMenu), unified views (InterviewsView), types, transformers, mocks, helpers
/freeFlowsListView, FlowManageUI, OrganizationSelectorUI
/paidRespondentViewUI, VideosDashboardWithTabs, VideoDashboardGrid, FinalVideosSection, FlowSidebar, nav components, hooks

When a component is needed by both tiers, move it to /shared.

Note (2026-02): The shared tier has grown significantly beyond types/transformers to include the full layout shell and navigation. The root library page (/) now uses InterviewsView from /shared for all users — plan-gating has moved to the respondent detail level (/dashboard/[flowId]/[respondentId]), not the root.

Component Patterns

User-Facing vs Admin Components

Some library app components mix admin features with user features. The demo architecture uses this separation strategy:

  1. Package contains user-facing components only:

    • No server actions requiring admin permissions
    • No delete/edit operations needing auth checks
    • Pure UI that works with props/mock data
  2. Admin components stay in apps/library:

    • Components with server actions remain in the app
    • Admin-specific functionality not extracted to packages
    • Will be separated when admin app is created

Component Naming Convention

  • *UI suffix indicates a presentation component (e.g., RespondentViewUI)
  • These components accept data via props
  • No direct data fetching or server actions
  • Can be used in both production and demo contexts

Example: RespondentViewUI

// packages/app-library/src/paid/RespondentViewUI.tsx
// User-facing component - no admin actions

interface RespondentViewUIProps {
  respondent: PersonData;
  videos: VideoInfo[];
  onVideoSelect?: (video: VideoInfo) => void;
}

export function RespondentViewUI({ 
  respondent, 
  videos, 
  onVideoSelect 
}: RespondentViewUIProps) {
  // Pure presentation logic
}
// apps/library - admin version with server actions
// Wraps RespondentViewUI with admin capabilities

import { RespondentViewUI } from "@repo/app-library/paid";

export function RespondentDashboard({ respondentId }) {
  // Data fetching, auth checks, admin actions
  return (
    <RespondentViewUI 
      respondent={data}
      videos={videos}
      onVideoSelect={handleAdminVideoAction}
    />
  );
}

Transformer Pattern

Transformers convert backend data models to frontend-friendly types:

// packages/app-library/src/shared/transformers.ts

export function toVideoInfo(video: Video): VideoInfo {
  return {
    id: video.id,
    thumbnailUrl: video.thumbnail_url,
    createdAt: new Date(video.created_at),
    // ... simplified, frontend-friendly structure
  };
}

export function toFlowInfo(flow: VideoFlow): FlowInfo {
  return {
    id: flow.id,
    name: flow.name,
    videoCount: flow.videos?.length ?? 0,
    // ... derived properties for UI
  };
}

Usage in Apps

// Server component in apps/library
import { toFlowInfo } from "@repo/app-library/shared";

export async function FlowsPage() {
  const flows = await fetchFlows();
  const flowInfos = flows.map(toFlowInfo);
  return <FlowsListView flows={flowInfos} />;
}
// Demo page - uses mock data directly
import { mockFlowInfos } from "@repo/app-library/shared";

export function DemoFlowsPage() {
  return <FlowsListView flows={mockFlowInfos} />;
}

Mock Data Strategy

Location

Mock data lives in the shared layer for reuse:

// packages/app-library/src/shared/mocks.ts

export const mockVideoInfo: VideoInfo = {
  id: "demo-video-1",
  thumbnailUrl: "/demo/thumbnail.jpg",
  // ...
};

export const mockFlowInfos: FlowInfo[] = [
  { id: "demo-flow-1", name: "Customer Testimonials", videoCount: 5 },
  // ...
];

Usage Patterns

  1. Demo App: Import mocks directly for demo pages
  2. Tests: Use same mocks for component testing
  3. Storybook: Consistent mock data across stories

Relationship to Production Apps

apps/library

The Library app imports heavily from @repo/app-library for layout and UI components:

// apps/library/app/(main)/layout.tsx — uses shared layout shell
import { LibraryLayoutShell } from "@repo/app-library/shared";

// apps/library/app/(main)/page.tsx — unified root for ALL tiers
import { InterviewsView, InterviewsSkeleton } from "@repo/app-library/shared";

// apps/library/app/(main)/dashboard/[videoFlowId]/[respondentId]/page.tsx
import { toRespondentViewData } from "@repo/app-library/paid";

Architecture note: As of 2026-02, the library root page (/) serves a unified InterviewsView for all users. The legacy (free)/onboarding route redirects to root and is kept only for backwards-compatible URL compatibility. Plan-gating has shifted from the root page to the respondent detail page, where unpaid users who don't own a video are redirected back to the flow dashboard.

apps/hello

The Hello app remains unchanged — it uses @repo/app-hello for its components.

apps/demo

The Demo app imports from both packages with mock data:

// apps/demo/app/library/free/page.tsx
import { FlowsListView } from "@repo/app-library/free";
import { mockFlowInfos } from "@repo/app-library/shared";

export default function DemoFreeTierPage() {
  return <FlowsListView flows={mockFlowInfos} />;
}

Benefits

  1. Clean Separation: Free/paid tiers clearly organized
  2. Reusable Components: Same components work in prod and demo
  3. Type Safety: Shared types ensure consistency
  4. No Auth Overhead: Demo runs standalone
  5. Transformer Pattern: Backend complexity hidden from UI
  6. Mock Consistency: Same mock data for demos and tests

Migration Notes

When adding new components:

  1. Determine tier: Free, paid, or shared?
  2. Create UI component: Props-driven, no server actions
  3. Add transformer: If backend model differs from UI needs
  4. Add mocks: For demo and testing
  5. Export from index: Add to appropriate tier's barrel file