Last verified: 2026-03-03 Target:
apps/helloCompanion:packages/app-hello
Hello App Architecture
Overview
The Hello app is a self-service onboarding wizard that guides users through creating a video testimonial flow. It extracts organization data from websites using LLM, generates interview questions, and allows customization before publishing.
Tech Stack:
- Next.js 15 (App Router, Server Components, Server Actions)
- Zustand (DraftStore for state management)
- Redis (OnboardingDraft persistence)
- OpenRouter (LLM provider for extraction, generation, translation)
@repo/organization-extraction(website scraping + LLM enrichment pipeline)
Integration Map
High-Level Flow
Root page decision tree (apps/hello/app/page.tsx):
?demoquery param → redirect/start?demo=1(skips welcome-back, draft resume, dev dialog)- Anonymous user → redirect
/start - Authenticated, has orgs → render
WelcomeBackClient(go to dashboard or create new) - Authenticated, no orgs → redirect
/start
Onboarding Steps
| Step | URL | Purpose | LLM Interaction |
|---|---|---|---|
| Website | /d/[draftId]/website | Collect website URL | Triggers background extraction |
| Objective | /d/[draftId]/objective | Select testimonial type | None |
| Company | /d/[draftId]/plan-interview | Review/edit extracted data | Wait for extraction |
| Studio | /d/[draftId]/studio | Customize the flow | Optional question generation |
| Publish | /d/[draftId]/publish | Publish and share | None (converts draft to flow) |
Data Model
OnboardingDraft (Redis)
The OnboardingDraft is the single source of truth for all onboarding data:
interface OnboardingDraft {
// Metadata
id: string;
currentStep: DraftStep;
createdAt: number;
updatedAt: number;
// Organization data (from LLM extraction)
org: {
source: "extracted" | "existing";
profile: DraftOrgProfile | null;
logoUrl: string | null;
availableUsps: string[];
extractionStatus: "pending" | "in_progress" | "completed" | "failed";
extractionError?: string;
};
// User inputs (kickoff context)
kickoff: {
website: string;
objective: string;
selectedUsps: string[];
targetAudience: string;
customNotes: string;
};
// Flow configuration
flow: {
name: string;
questions: DraftQuestion[];
rewards: DraftReward[];
rewardsEnabled: boolean;
branding: DraftBranding | null;
supportedLanguages: Language[];
};
// AI-generated/translatable content
generated: {
welcomeTitle: Record<Language, string>;
welcomeSubtitle: Record<Language, string>;
tipsTitle: Record<Language, string>;
tipsDescriptions: Array<Record<Language, string>>;
rewardsTitle: Record<Language, string>;
confirmTitle: Record<Language, string>;
// ...
};
// Publish state
publish: {
flowId: string | null;
flowUrl: string | null;
publishedAt: number | null;
};
}
Complete Data Flow Diagram
LLM Interaction Flows
1. Organization Extraction
Triggered when user submits website URL. Runs in background via waitUntil().
2. Question Generation
Triggered when user clicks "Generate Questions" in Studio.
3. Translation (Add Language)
Triggered when user adds a new language in Studio. The translation pipeline extracts all translatable content, sends it to an LLM for translation, and applies the results back to the DraftStore.
For the detailed translation data flow, component responsibilities, known structural problems, and target architecture, see Interview Editor Architecture — Translation Flow.
4. Publish Flow
Converts draft to published FlowConfig.
Generation Snapshots (Analytics)
Generation Snapshots capture what the backend AI/LLM generates for analytics and training purposes. They track the original AI output separately from user-edited draft data.
Purpose
- Analytics/Training: Compare AI-generated content vs. user edits
- Not for restoration: Snapshots are never used to restore content
- Non-blocking: Snapshot failures don't affect main user flows
- Encapsulated: Snapshot storage is hidden inside LLM wrapper functions (callers don't need to know about snapshots)
Data Model
interface GenerationSnapshot {
draftId: string;
organizationExtraction?: OrganizationExtractionSnapshot; // Single (overwrites)
questionGenerationHistory: QuestionGenerationSnapshot[]; // Array (appends)
translationHistory?: TranslationSnapshot[]; // Array (appends)
createdAt: number;
updatedAt: number;
}
interface OrganizationExtractionSnapshot {
profile: ExtractedOrganizationProfile; // name, description, website, USPs, etc.
logoUrl: string | null;
capturedAt: number;
metrics?: { durationMs?: number; costUsd?: number };
}
interface QuestionGenerationSnapshot {
type: "initial" | "regenerate" | "generate_more";
questions: GeneratedQuestion[];
welcomeTitle?: string;
welcomeSubtitle?: string;
generationInput: QuestionGenerationSnapshotInput;
capturedAt: number;
metrics?: { durationMs?: number; costUsd?: number; modelId?: string };
}
interface TranslationSnapshot {
sourceLanguage: Language;
targetLanguage: Language;
input: TranslationContent;
output: TranslationContent;
capturedAt: number;
metrics?: { durationMs?: number; costUsd?: number; modelId?: string };
}
Storage: Redis KV with key pattern hello:generation-snapshot:{draftId} (same Redis as drafts via PROCESSED_VIDEO_DATA_KV_URL)
Architecture
Snapshot storage is encapsulated inside the LLM wrapper functions in packages/app-hello/src/server/. The caller passes an optional snapshotRegistry dependency, and the wrapper handles storage as a non-blocking side effect.
LLM Wrapper Functions
The wrapper functions encapsulate LLM calls + snapshot storage as a single operation:
| Wrapper Function | Location | Snapshot Stored |
|---|---|---|
generateQuestionsWithSnapshot() | packages/app-hello/src/server/generate-questions.ts | Question generation |
translateContentWithSnapshot() | packages/app-hello/src/server/translate-content.ts | Translation |
extractOrganization() | packages/app-hello/src/server/extract-organization.ts | Organization extraction (via optional snapshotRegistry + draftId params) |
Dependency Injection Pattern: The snapshotRegistry is passed as an optional dependency. If not provided, snapshot storage is skipped (useful for tests or dry runs).
// Example: Question generation with snapshot
const result = await generateQuestionsWithSnapshot(input, {
model,
modelId: "moonshotai/kimi-k2",
logger,
draftId,
type: "initial",
snapshotRegistry, // Optional - skip snapshot if not provided
});
Read Operations (Admin Only)
Snapshots are read only in the admin app for debugging and analytics:
- Admin page:
apps/admin/app/hello-drafts/[draftId]/page.tsx - Component:
GenerationSnapshotsSectiondisplays:- Organization extraction details
- Question generation history (all events)
- Translation history (all events)
- Full JSON view
Note: Mock mode does not store snapshots (only real LLM operations).
File Organization
| File | Purpose |
|---|---|
packages/app-hello/src/server/generate-questions.ts | generateQuestionsWithSnapshot() - wraps LLM + stores snapshot |
packages/app-hello/src/server/translate-content.ts | translateContentWithSnapshot() - wraps translation + stores snapshot |
packages/app-hello/src/server/extract-organization.ts | extractOrganization() with optional snapshot params |
packages/app-hello/src/server/snapshot-mapping.ts | Mapping functions that transform LLM results to snapshot format |
packages/registries/src/server/generation-snapshot-registry.ts | Registry implementation (extends BaseStorage) |
packages/registries/src/server/generation-snapshot-types.ts | Type definitions and Zod schemas |
apps/admin/app/hello-drafts/[draftId]/page.tsx | Admin UI for viewing snapshots |
Frontend Architecture
Component Hierarchy
apps/hello/
├── app/
│ ├── d/[draftId]/
│ │ ├── layout.tsx # Draft layout with progress bar
│ │ ├── website/page.tsx # Server component - loads draft
│ │ ├── objective/page.tsx # Server component - loads draft
│ │ ├── plan-interview/page.tsx # Server component - loads draft
│ │ ├── studio/page.tsx # Server component - loads draft
│ │ └── publish/page.tsx # Server component - loads draft
├── components/steps/
│ ├── WebsiteStepForm.tsx # Client form - uses DraftStore
│ ├── ObjectiveStepForm.tsx # Client form - uses DraftStore
│ ├── CompanyStepForm.tsx # Client form - uses DraftStore
│ ├── StudioStepForm.tsx # Client form - uses DraftStore
│ └── ...
└── hooks/
├── useDraftPersistence.ts # Debounced save to backend
├── useQuestionGeneration.ts # LLM question generation
└── useDraftExtractionPolling.ts # Client-side extraction polling
State Management Pattern
Synchronous Store Initialization
Critical pattern to prevent blank preview on first render:
// In StudioStepForm.tsx
function StudioStepForm({ initialDraft, draftId }) {
// Initialize SYNCHRONOUSLY during first render
useState(() => {
const store = useDraftStore.getState();
if (store.draftId !== draftId) {
store.initialize(initialDraft, draftId);
}
});
// Now safe to read from store
const draft = useDraftStore((s) => s.draft);
const flowConfigForPreview = useDraftFlowConfigForPreview();
// ...
}
Reactive Update Pattern (Zustand Subscriptions)
When server actions return data (e.g., translations, generated questions), FlowEditClient applies changes to the DraftStore directly. Zustand's subscription mechanism automatically triggers re-renders for all components using that data.
No useEffect needed for reactive updates - this is pure subscription-based reactivity:
Key hooks and their subscriptions:
| Hook | Subscribes To | Re-renders When |
|---|---|---|
useDraftQuestions() | draft.flow.questions | Questions change |
useDraftWelcome() | draft.generated.welcomeTitle/Subtitle | Welcome text changes |
useDraftTips() | draft.generated.tipsTitle/Descriptions | Tips change |
useDraftRewards() | draft.flow.rewards, draft.generated.rewardsTitle | Rewards change |
useDraftFlowConfig() | draft (derived) | Any draft change |
useDraftDesign() | draft.flow.branding | Design settings change |
This pattern means:
- FlowEditClient applies server action results to the store directly — no data returned to callers
- Components re-render automatically via Zustand subscriptions
- No manual useEffect synchronization needed
- UI updates are instant and consistent
- All data manipulation (translation mapping, question extraction, background image operations) happens through store methods or registry utilities — never inline in components
Debounced Persistence Layer
The useDraftPersistence hook handles automatic saving of store changes to the backend:
Fatal errors that stop retries:
"Draft not found"- Draft expired or deleted from Redis"Unauthorized"- Auth cookie invalid or expired"Draft validation failed: ..."- Invalid draft data (Zod validation failure)
Key behaviors:
- Changes accumulate in
pendingUpdateduring debounce window - Single save call with all accumulated changes
- Draft data is validated before saving (prevents writing unreadable data)
- Generation-aware
markClean: captures_saveGenerationbefore save, skips clearing if the store was updated during the save (prevents race conditions) - Fatal errors stop all future save attempts for this draft
- Transient errors show toast but allow future saves
Translation Detection and Pre-Publish Check
The system detects missing translations and prompts users before publishing. Key components:
GlobalLanguageSwitcher— Shows warning badge on language dots, displays "Translate missing content" buttonTranslationMissingDialog— Pre-publish prompt offering auto-translate or manual reviewtranslateMissingContentAction— Server action that translates only missing content (not re-translating existing)
All translation detection derives from the TRANSLATABLE_FIELDS registry — see Interview Editor Architecture — Translation Pipeline for the full pipeline, including how buildFullLanguageTranslationUpdate applies translations atomically to the store.
Server Actions
All server actions are defined in apps/hello/app/actions/. They use HMAC-signed cookies for draft authorization.
Draft CRUD Actions
Source: draft-actions.ts
| Action | Purpose |
|---|---|
createDraftAction | Create new draft, set auth cookie |
createDraftAndRedirect | Create draft and redirect to first step |
getDraftAction | Load draft by ID |
getValidatedDraftAction | Load draft with auth validation |
saveDraftAction | Generic draft update (merges partial update) |
updateDraftStepAction | Update current step marker |
deleteDraftAction | Delete draft and clear cookie |
clearDraftCookieAction | Clear auth cookie only |
Step-Specific Save Actions
Source: draft-actions.ts
| Action | Step | Purpose |
|---|---|---|
saveWebsiteStepAction | Website | Save URL, trigger extraction |
saveObjectiveStepAction | Objective | Save objective type |
saveCompanyStepAction | Company | Save org profile edits |
saveDesignStepAction | Studio | Save branding/design |
saveTipsAction | Studio | Save tips content |
saveWelcomeAction | Studio | Save welcome screen content |
saveConfirmAction | Studio | Save confirm screen content |
saveQuestionsAction | Studio | Save question edits |
saveRewardsAction | Studio | Save rewards configuration |
saveSettingsAction | Studio | Save flow settings |
Publish Actions
Source: draft-actions.ts, publish-flow.ts
| Action | Purpose |
|---|---|
publishDraftAction | Convert draft to FlowConfig, publish to user's organization |
markDraftPublishedAction | Mark draft as published (internal) |
publishFlowAction | Low-level flow publish with org selection/creation |
Publish Flow Logic:
- If user has 0 organizations → auto-creates organization from draft data, then publishes
- If user has 1 organization → publishes directly to that organization
- If user has multiple organizations → returns
pendingOrgsfor client to showOrgPickerForm
LLM/AI Actions
Source: question-actions.ts, background-extraction.ts, translate-content.ts
| Action | Purpose | LLM Model |
|---|---|---|
runExtraction | Extract org from website | Gemini |
waitForExtractionComplete | Poll until extraction done | - |
generateQuestionsAction | Generate interview questions | Kimi-K2 |
generateMoreQuestionsAction | Append additional questions | Kimi-K2 |
addLanguageAction | Add language + translate all content | GPT-4o-mini |
removeLanguageAction | Remove language from draft | - |
addTipAction | Add tip + translate to all languages | GPT-4o-mini |
addCustomFieldAction | Add custom field + translate | GPT-4o-mini |
translateContentAction | Translate content batch | GPT-4o-mini |
Asset Actions
Source: asset-actions.ts, unsplash.ts, process-image.ts
| Action | Purpose |
|---|---|
generateImageAction | AI image generation (logo/background/reward) |
saveUploadedAssetAction | Process and save uploaded image |
removeAssetAction | Remove asset from draft |
reorderBackgroundsAction | Reorder background images |
selectUnsplashForDraftAction | Select Unsplash photo, download, save to draft |
searchUnsplashAction | Search Unsplash photos |
selectUnsplashPhotoAction | Download Unsplash photo to blob storage |
processImageAction | Resize/optimize image |
Start Actions
Source: start-actions.ts
| Action | Purpose |
|---|---|
createDraftWithWebsiteAction | Create draft with pre-filled website URL |
createDraftForExistingOrgAction | Create draft for existing org (authenticated) |
API Routes
API routes exist primarily for external access, client-side operations requiring REST endpoints, and health monitoring. Most UI interactions use server actions directly.
| Route | Method | Purpose | Used By |
|---|---|---|---|
/api/blob/upload | POST | Upload files to blob storage | Client uploads |
/api/correlation | GET | Get/set correlation ID | Analytics |
/api/create-draft | GET | Create draft + redirect to first step | StartStepForm (client-side redirect) |
/api/draft-extraction-status | GET | Poll extraction status | Client polling |
/api/extract-organization | POST | Trigger org extraction | External API |
/api/generate-image | POST | AI image generation | External API |
/api/generate-questions | POST | Generate questions | External API |
/api/info | GET | App info/health check | Monitoring |
File Organization
apps/hello/
├── app/
│ ├── _providers/ # Server/client providers
│ ├── actions/ # Server actions (call LLM wrappers)
│ │ ├── draft-actions.ts # CRUD for drafts
│ │ ├── background-extraction.ts # Calls extractOrganization with snapshot
│ │ ├── question-actions.ts # Calls generateQuestionsWithSnapshot, translateContentWithSnapshot
│ │ ├── translate-content.ts # Translation utilities
│ │ └── publish-flow.ts # Publish to flow registry
│ ├── api/ # API routes
│ └── d/[draftId]/ # Onboarding step pages
├── components/steps/ # Step form components
├── hooks/ # Custom hooks
│ ├── useDraftPersistence.ts
│ ├── useQuestionGeneration.ts
│ └── useDraftExtractionPolling.ts
└── lib/
├── env.ts # Environment config
├── logger.ts # Logging singleton
└── build-flow-config.ts # Draft → FlowConfig builder
packages/app-hello/src/
├── server/ # LLM wrappers with encapsulated snapshot storage
│ ├── index.ts # Server exports
│ ├── extract-organization.ts # Org extraction + optional snapshot
│ ├── generate-questions.ts # Question generation + snapshot
│ ├── translate-content.ts # Translation + optional snapshot
│ └── snapshot-mapping.ts # Mapping functions for snapshots
├── web/
│ ├── stores/
│ │ ├── draft-store.ts # DraftStore (single source of truth for all data)
│ │ ├── draft-hooks.ts # Convenience hooks
│ │ └── build-flow-config-from-draft.ts
│ ├── utils/
│ │ ├── translatable-field-registry.ts # TRANSLATABLE_FIELDS registry + all translation functions
│ │ └── translationUtils.ts # Backward-compatible re-exports
│ ├── hooks/
│ │ ├── use-auto-save.ts # Generation-aware auto-save
│ │ └── use-flow-edit-handlers.ts # Shared wrapper for library/admin
│ └── components/
│ ├── FlowEditClient.tsx # Translation + question generation orchestration
│ ├── FlowStudioEditor.tsx # Store init, auto-save, UI delegation
│ ├── StudioStep.tsx # Studio UI
│ ├── PlanInterviewStep.tsx # Company step UI
│ └── config-tabs/ # Tab components (read from store via hooks)
└── types/
└── draft.ts # Re-exports from registries
packages/registries/src/server/
├── generation-snapshot-registry.ts # Snapshot storage (extends BaseStorage)
└── generation-snapshot-types.ts # Snapshot types and Zod schemas
Plan Interview Loading Flow
The plan-interview page uses Next.js Suspense streaming for optimal loading UX. Understanding the component hierarchy is critical to avoid layout jumps.
Key patterns:
- Suspense fallback (
loading.tsx) and content (CompanyStepForm) use the same wrapper (OnboardingScreen) - This ensures consistent layout during the streaming transition
- The only visual change is the inner content (loading animation to form)
Component hierarchy:
| File | Wrapper | Purpose |
|---|---|---|
loading.tsx | OnboardingScreen | Suspense fallback - streams immediately |
CompanyStepForm.tsx | OnboardingScreen | Actual content - renders when data ready |
Anti-pattern to avoid: Using different wrapper components (e.g., SplitScreenBuilder for loading, OnboardingScreen for content) causes jarring layout shifts when the async content loads.
Studio Page Loading Flow (Server-Side Initial Generation)
The studio page uses the same Suspense streaming pattern as plan-interview, with an additional step: it runs initial question generation server-side before handing off to the client component. This eliminates the client-side flash that previously occurred when generated data replaced empty/default data.
Key behaviors:
- Initial generation is server-side only — eliminates the
useEffectflash from the old client-side approach - Generation failures are non-fatal: logs a warning, passes draft as-is (user can regenerate from UI)
- Guard: only generates if
draft.flow.questions.length === 0ANDdraft.org.profileexists - User-triggered regeneration and "generate more" stay client-side in
StudioStepForm
File: apps/hello/app/d/[draftId]/studio/_components/StudioContent.tsx
Key Principles
-
OnboardingDraft is Source of Truth
- Store structure mirrors database exactly
- No transformation layer between frontend and backend
-
FlowConfig is Derived
- Computed on-demand via
buildFlowConfigFromDraft - Never stored in state
- Used only for preview and publish
- Computed on-demand via
-
Background LLM Operations
- Extraction runs via
waitUntil()for non-blocking UX - Client can poll or server can wait
- Extraction runs via
-
Debounced Persistence
- Changes accumulate in
pendingUpdate - Single
saveDraftActioncall after debounce
- Changes accumulate in
-
Synchronous Store Initialization
- Use
useState()initializer, notuseEffect - Prevents blank preview on first render
- Use
-
Validate on Save, Not Read
- Draft data is Zod-validated before writing to storage
- Prevents saving data that can never be read back
- Returns specific validation errors (not generic "Draft not found")
- Relaxed validation during editing (e.g., empty questions allowed)
- Strict validation at publish time via
buildFlowConfigFromDraft
Anonymous User Flow (Deferred Publish)
Anonymous users can create and configure flows without logging in. Publishing is deferred until after authentication.
Flow Diagram
Key Design Decisions
-
Deferred Publish: Flows are NOT published until the user is authenticated. The draft is saved to Redis, and the
draftIdin the URL is the only state needed to resume. -
No Intermediate Organization: Previously, flows were published to a "hello org" and then "claimed" by the user. Now, publishing goes directly to the user's organization.
-
No Flow Cookies: The
hc_pending_flowcookie and related claim logic have been removed. ThedraftIdURL parameter is sufficient for state management. -
Automatic Org Creation: New signups without organizations get one created automatically using the company name and logo from the draft's
organizationProfile. -
Multi-Org Selection: Users with multiple organizations see an org picker UI before publishing.
Authentication Redirect Handling
Different auth methods have different redirect mechanisms back to the publish page:
| Auth Method | Redirect Mechanism | Key File |
|---|---|---|
| OAuth (Google, Microsoft) | sso-callback/page.tsx sets signUpForceRedirectUrl | apps/hello/app/d/[draftId]/publish/sso-callback/page.tsx |
| Email/Password | useAuth() watcher in AuthGateForm detects isSignedIn and does window.location.href | apps/hello/components/steps/AuthGateForm.tsx |
| Direct page load (already authenticated) | [...rest]/page.tsx catch-all redirects to /d/[draftId]/publish | apps/hello/app/d/[draftId]/publish/[...rest]/page.tsx |
Why email/password needs special handling: Clerk Elements (SignUp.Root, SignIn.Root) with routing="path" manage sub-step navigation (e.g., /publish/continue, /publish/verifications). After sign-up completes, Clerk redirects to the global NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL rather than back to the component's path prop. Clerk Elements does not support a per-component fallbackRedirectUrl prop. The AuthGateForm intercepts this by watching useAuth().isSignedIn and performing a full page load to the publish page when auth completes.
Component Responsibilities
| Component | Type | Role |
|---|---|---|
usePublishFlow | Client Hook | Checks auth, either publishes directly or navigates to /publish |
publishDraftAction | Server Action | Publishes draft to specified org, handles org creation if needed |
publishFlowAction | Server Action | Low-level publish with org creation logic |
PublishPage (page.tsx) | Server Component | Checks auth, determines org, publishes or shows picker/auth |
AuthGateForm | Client Component | Renders OAuth buttons and email/password forms; watches isSignedIn for post-auth redirect |
OrgPickerForm | Client Component | Renders org selection UI for multi-org users |
SSOCallbackPage | Client Component | Handles OAuth callback, redirects back to publish |
PublishSubStepPage ([...rest]/page.tsx) | Server Component | Catch-all for Clerk Elements sub-steps; redirects authenticated users to publish |
Data Flow
Publish Return Types
The publishDraftAction returns one of three result types:
type PublishDraftResult =
| { success: true; flowId: string; flowUrl: string; organizationId: string } // Published successfully
| { success: true; pendingOrgSelection: true; organizations: Array<{ id: string; name: string; imageUrl?: string }> } // Multi-org selection needed
| { success: false; error: string } // Error
Note: All three branches have success: true | false. The multi-org case is distinguished by the presence of pendingOrgSelection: true. The publish success case includes organizationId so callers can setActive on the correct Clerk org.
Slack notifications on publish (fire-and-forget):
- New signup (0 orgs → creates org): notified to
#happyclient-new-signups-notifications - Existing user (1+ org, creates new flow): notified to the interview agent channel
- Notifications fail silently via
.catch()— publish success is not blocked
Key Files
| File | Purpose |
|---|---|
apps/hello/hooks/usePublishFlow.ts | Client hook - checks auth, publishes or defers |
apps/hello/app/actions/publish-flow.ts | Server Action - publish flow with org creation |
apps/hello/app/actions/draft-actions.ts | Server Action - publishDraftAction wrapper with org resolution |
apps/hello/app/d/[draftId]/publish/page.tsx | Server Component - orchestrates auth/org selection/publish |
apps/hello/components/steps/AuthGateForm.tsx | Client Component - OAuth and email auth UI |
apps/hello/components/steps/OrgPickerForm.tsx | Client Component - multi-org selection UI |
apps/hello/app/d/[draftId]/publish/sso-callback/page.tsx | OAuth callback handler |
apps/hello/app/d/[draftId]/publish/[...rest]/page.tsx | Catch-all for Clerk Elements sub-steps (continue, verifications) |
packages/app-hello/src/web/components/OrgPickerDemo.tsx | Demo version of OrgPickerForm for demo app |
Related Documentation
- Interview Editor Architecture - Shared editor system across hello/admin/library, translation pipeline, known bugs, target architecture
- Hello Component Architecture - Layout wrappers (OnboardingScreen, WithFlowPreview, OnboardingPanel)
- FlowConfig Data Model Refactor - DraftStore architecture details
- Monorepo Organization - Package structure