Demo App and Tier Architecture
Last verified: 2026-03-02 Target:
apps/library,apps/demoCompanion: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
- No Authentication: No Clerk, no auth middleware
- No Environment Variables: Uses mock data throughout
- Shared Components: Imports from
@repo/app-library,@repo/app-hello,@repo/demo-flow - Mock Data: Uses
@repo/registries/server/mocksfor 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
| Location | Contents |
|---|---|
/shared | Layout (LibraryLayoutShell, AppSidebar, MobileNav), navigation (ProfileMenu), unified views (InterviewsView), types, transformers, mocks, helpers |
/free | FlowsListView, FlowManageUI, OrganizationSelectorUI |
/paid | RespondentViewUI, 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 usesInterviewsViewfrom/sharedfor 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:
-
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
-
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
*UIsuffix 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
- Demo App: Import mocks directly for demo pages
- Tests: Use same mocks for component testing
- 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 unifiedInterviewsViewfor all users. The legacy(free)/onboardingroute 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
- Clean Separation: Free/paid tiers clearly organized
- Reusable Components: Same components work in prod and demo
- Type Safety: Shared types ensure consistency
- No Auth Overhead: Demo runs standalone
- Transformer Pattern: Backend complexity hidden from UI
- Mock Consistency: Same mock data for demos and tests
Migration Notes
When adding new components:
- Determine tier: Free, paid, or shared?
- Create UI component: Props-driven, no server actions
- Add transformer: If backend model differs from UI needs
- Add mocks: For demo and testing
- Export from index: Add to appropriate tier's barrel file