# ⚙️ Technisch Ontwerp — Mini‑ECD (v1.2) **Datum:** nov 2025 **Scope:** MVP voor LinkedIn Build in Public Serie + Demo (≤10 min) **Bronnen:** PRD (v1.2), FO (v1.2), UX/UI‑specificatie, Bouwplan --- ## 0) TL;DR Stack & Keuzes * **Framework**: **Next.js** (App Router) - single repo voor marketing + EPD * **UI**: **Tailwind CSS** (v4; fallback v3.4 bij frictie) + **lucide-react** iconen; lichte componentlaag (shadcn/ui of eigen + headless) * **Editor**: **TipTap** (ProseMirror) met StarterKit + BasicNodes * **Auth & Data**: **Supabase** (PostgreSQL + Auth + Storage) * **AI**: **Claude** (Anthropic) via API * **Hosting**: **Vercel** (Next.js) * **Onboarding**: **react-joyride** voor walkthroughs + custom tooltip system * **PDF (stretch)**: server‑side HTML→PDF via **Chromium (playwright/puppeteer)** of cloud‑functie * **Test**: **Vitest** (unit) + **Playwright** (e2e) > ✅ Past bij MVP: minimale libs, AI‑calls server‑side, EU‑dataregio (Supabase EU), TipTap voor rijke tekst, marketing website in dezelfde app voor SEO/speed. --- ## 1) Architectuur ### 1.1 Overzicht ``` Browser (UI) └─ Next.js (App Router) ├─ Marketing Site (/, /build-log, /demo, /contact) ├─ Protected EPD App (/clients/*, /dashboard) ├─ Supabase (PostgreSQL + Auth + Storage) ├─ Claude AI (Anthropic) └─ (Stretch) PDF service (Chromium in serverless) ``` * **Server‑side AI‑calls**: keys blijven op de server; UI krijgt alleen resultaten * **Single Next.js app**: Marketing en EPD in één repo voor efficiëntie * **Dataflow (kern)**: Intake (TipTap) → AI‑samenvat → AI‑extract → Probleemprofiel → AI‑plan → Plan (concept → publiceer) ### 1.2 Routing & lagen **Route Groepen (Next.js App Router):** ``` /app /(marketing) # Public routes /page.tsx # Landing page /build-log /page.tsx # Timeline overview /[week]/page.tsx # Week detail /demo/page.tsx # Demo info + credentials /how-it-works/page.tsx # Software on Demand explainer /contact/page.tsx # Lead capture form /layout.tsx # Marketing layout (geen sidebar) /(app) # Protected routes /layout.tsx # App layout (met sidebar nav) /clients /page.tsx # Client list /[id] /page.tsx # Client dashboard (tabs) /intakes/page.tsx # Intake module /profile/page.tsx # Problem profile /plan/page.tsx # Treatment plan /onboarding/page.tsx # First-time walkthrough /api # API routes (server‑side) /clients/route.ts # CRUD clients /intakes/route.ts # CRUD intakes /problem-profile/route.ts # CRUD profiles /treatment-plan/route.ts # CRUD plans /ai /summarize/route.ts # AI summarize /readability/route.ts # AI B1 rewrite /extract/route.ts # AI extract problems /generate-plan/route.ts # AI generate plan /build-metrics/route.ts # Build tracking (hours, costs) /leads/route.ts # Lead form submissions ``` **Routing Strategie:** - Marketing routes: public, geen auth check - App routes: protected met middleware auth check - API routes: server-side only, verschillende auth levels per endpoint - Shared components in `/components/shared/` - Marketing-specific in `/components/marketing/` - App-specific in `/components/app/` ### 1.3 State De state van de applicatie wordt beheerd met **React Context API** + **Zustand** (lichtgewicht state library). Dit is eenvoudig genoeg voor de MVP-scope. **State Stores:** * **`clientStore.ts` (Zustand)**: Client data state * `selectedClientId: string | null`: Actieve client ID * `clients: Client[]`: Lijst van alle clients * `currentClientDossier: Dossier | null`: Complete dossier data * **`uiStore.ts` (Zustand)**: Globale UI state * `toasts: ToastMessage[]`: Actieve toast notificaties * `onboardingCompleted: boolean`: Onboarding status * `tooltipsSeen: string[]`: Welke tooltips al gezien * `sidebarCollapsed: boolean`: Sidebar state * **`buildMetricsStore.ts` (Zustand)**: Build tracking (NIEUW) * `totalHours: number`: Totaal development uren * `totalCosts: number`: Totale kosten (infrastructure + AI) * `weeklyBreakdown: WeekMetrics[]`: Per-week data * `featuresCompleted: number`: Aantal features af * **Dataflow Patroon**: 1. UI update `selectedClientId` via Zustand action 2. Store triggert Supabase query om dossier op te halen 3. React componenten die subscribed zijn via `useClientStore()` updaten automatisch --- ## 2) Data‑model (Supabase / PostgreSQL) ### 2.1 Entiteiten **Bestaande tabellen:** * **clients** — basisgegevens * **intake_notes** — TipTap JSON + afgeleide velden * **problem_profiles** — DSM‑light categorie + severity * **treatment_plans** — JSONB plan (doelen/interventies/frequentie/meetmomenten), versie/status * **ai_events** — prompts/completions (telemetrie, debugging) **Nieuwe tabellen (v1.2):** * **onboarding_progress** — tracking per user welke stappen gedaan * **build_metrics** — wekelijkse uren/kosten voor transparantie * **leads** — contact form submissions van marketing site * **demo_users** — special demo accounts met read-only access ### 2.2 Relaties (PostgreSQL) ``` clients (id UUID PRIMARY KEY) ├── intake_notes (client_id → clients.id, FK) ├── problem_profiles (client_id → clients.id, FK) └── treatment_plans (client_id → clients.id, FK) ai_events (id UUID PRIMARY KEY, client_id + note_id als optionele FKs) onboarding_progress (user_id → auth.users.id, FK) build_metrics (id UUID PRIMARY KEY, week_number UNIQUE) leads (id UUID PRIMARY KEY) demo_users (id UUID PRIMARY KEY, user_id → auth.users.id, FK) ``` **Row Level Security (RLS)**: Alle tables hebben RLS policies voor auth.users(). ### 2.3 Tables (PostgreSQL schema) > **NB**: Bestaande tables blijven ongewijzigd, nieuwe tables voor v1.2 features. ```typescript // BESTAANDE TABLES (ongewijzigd) // clients, intake_notes, problem_profiles, treatment_plans, ai_events // Zie originele TO voor deze schemas // NIEUWE TABLES (v1.2) // onboarding_progress table interface OnboardingProgress { id: string; // UUID user_id: string; // FK → auth.users.id walkthrough_completed: boolean; // Main walkthrough done tooltips_seen: string[]; // Array of tooltip IDs seen last_step_completed: string; // Last completed step ID created_at: string; // TIMESTAMPTZ updated_at: string; // TIMESTAMPTZ } // build_metrics table (voor transparantie) interface BuildMetrics { id: string; // UUID week_number: number; // 1-4 (UNIQUE constraint) week_start_date: string; // DATE week_end_date: string; // DATE development_hours: number; // DECIMAL(5,2) infrastructure_cost: number; // DECIMAL(8,2) in EUR ai_api_cost: number; // DECIMAL(8,2) in EUR features_completed: string[]; // TEXT[] array of feature names blog_post_url?: string; // TEXT (LinkedIn post link) notes?: string; // TEXT (internal notes) created_at: string; // TIMESTAMPTZ updated_at: string; // TIMESTAMPTZ } // leads table interface Lead { id: string; // UUID name: string; // TEXT email: string; // TEXT company?: string; // TEXT message: string; // TEXT source: 'landing' | 'build-log' | 'demo' | 'contact'; // TEXT with CHECK status: 'new' | 'contacted' | 'qualified' | 'converted' | 'rejected'; // TEXT with CHECK created_at: string; // TIMESTAMPTZ updated_at: string; // TIMESTAMPTZ } // demo_users table interface DemoUser { id: string; // UUID user_id: string; // FK → auth.users.id (UNIQUE) access_level: 'read_only' | 'interactive'; // TEXT with CHECK expires_at?: string; // TIMESTAMPTZ (optional expiry) usage_count: number; // INTEGER (track demo usage) created_at: string; // TIMESTAMPTZ } ``` **SQL voorbeelden** voor nieuwe tables: ```sql -- onboarding_progress CREATE TABLE onboarding_progress ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL, walkthrough_completed BOOLEAN DEFAULT FALSE, tooltips_seen TEXT[] DEFAULT '{}', last_step_completed TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id) ); -- build_metrics CREATE TABLE build_metrics ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), week_number INTEGER UNIQUE CHECK (week_number BETWEEN 1 AND 4), week_start_date DATE NOT NULL, week_end_date DATE NOT NULL, development_hours DECIMAL(5,2) DEFAULT 0, infrastructure_cost DECIMAL(8,2) DEFAULT 0, ai_api_cost DECIMAL(8,2) DEFAULT 0, features_completed TEXT[] DEFAULT '{}', blog_post_url TEXT, notes TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- leads CREATE TABLE leads ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, email TEXT NOT NULL, company TEXT, message TEXT NOT NULL, source TEXT CHECK (source IN ('landing', 'build-log', 'demo', 'contact')), status TEXT DEFAULT 'new' CHECK (status IN ('new', 'contacted', 'qualified', 'converted', 'rejected')), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- demo_users CREATE TABLE demo_users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE UNIQUE NOT NULL, access_level TEXT DEFAULT 'read_only' CHECK (access_level IN ('read_only', 'interactive')), expires_at TIMESTAMPTZ, usage_count INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW() ); ``` ### 2.4 Row Level Security (basis) **Bestaande RLS policies blijven:** authenticated users voor EPD tables. **Nieuwe RLS policies:** ```sql -- onboarding_progress: users can only see/update their own CREATE POLICY "Users can manage own onboarding" ON onboarding_progress FOR ALL USING (auth.uid() = user_id); -- build_metrics: read-only for all (public transparency) CREATE POLICY "Anyone can read build metrics" ON build_metrics FOR SELECT USING (true); -- Only service role can insert/update build metrics CREATE POLICY "Service role can manage metrics" ON build_metrics FOR ALL USING (auth.jwt() ->> 'role' = 'service_role'); -- leads: insert for anonymous, service role can manage CREATE POLICY "Anyone can submit leads" ON leads FOR INSERT WITH CHECK (true); CREATE POLICY "Service role can manage leads" ON leads FOR ALL USING (auth.jwt() ->> 'role' = 'service_role'); -- demo_users: service role only CREATE POLICY "Service role manages demo users" ON demo_users FOR ALL USING (auth.jwt() ->> 'role' = 'service_role'); ``` --- ## 3) API & Endpoints (Next.js Route Handlers) ### 3.1 CRUD (bestaand, ongewijzigd) * `POST /api/clients` — create * `GET /api/clients?query=` — list/search * `GET /api/clients/:id` — detail * `PATCH /api/clients/:id` — update * `POST /api/intakes` — create intake * `GET /api/intakes?clientId=` — list * `GET /api/intakes/:id` — detail * `PATCH /api/intakes/:id` — update * `POST /api/problem-profile` — create/update current * `GET /api/problem-profile?clientId=` — latest * `POST /api/treatment-plan` — create (concept) * `PATCH /api/treatment-plan/:id/publish` — publish vN ### 3.2 AI‑acties (bestaand, ongewijzigd) * `POST /api/ai/summarize` — TipTap JSON → bullets * `POST /api/ai/readability` — TipTap JSON → B1 * `POST /api/ai/extract` — TipTap JSON → {category, severity, rationale} * `POST /api/ai/generate-plan` — {noteId | profile} → plan JSON ### 3.3 Nieuwe Endpoints (v1.2) **Onboarding:** * `GET /api/onboarding/progress` — get user onboarding state * `PATCH /api/onboarding/progress` — update progress (complete step, mark tooltip seen) * `POST /api/onboarding/reset` — reset walkthrough (for testing) **Build Metrics:** * `GET /api/build-metrics` — get all weeks data (public, no auth) * `GET /api/build-metrics/current` — get current week stats * `POST /api/build-metrics` — create/update week data (service role only) **Leads:** * `POST /api/leads` — submit lead form (no auth required) * `GET /api/leads` — list all leads (service role only) * `PATCH /api/leads/:id` — update lead status (service role only) **Demo Access:** * `POST /api/demo/create-user` — generate demo credentials * `GET /api/demo/validate` — check if user is demo user * `POST /api/demo/track-usage` — increment demo usage counter **Patroon**: - Public endpoints (leads, build metrics GET) → no auth - User endpoints (onboarding) → auth.uid() check - Admin endpoints (leads management, metrics POST) → service role check --- ## 4) AI‑integratie (Claude / Anthropic) ### 4.1 Model & API (ongewijzigd) * **Model**: Claude 3.5 Sonnet (of nieuwere versie) via Anthropic API * **Regio**: Anthropic API is globally distributed; data blijft binnen EU waar mogelijk * **SDK**: `@anthropic-ai/sdk` (officiële Node.js SDK) ### 4.2 Prompt‑templates (ongewijzigd) Zie originele TO § 4.2 voor prompt details (Summarize, Readability, Extract, Plan). **Parameters (startwaarden):** - `model`: "claude-3-5-sonnet-20241022" (of nieuwer) - `temperature`: 0.3 (deterministischer) - `max_tokens`: passend per taak (samenvat 800‑1200, plan 1600‑2400) ### 4.3 Cost Tracking (NIEUW) Voor transparantie in de Build in Public serie: **Implementation:** ```typescript // lib/ai/cost-tracker.ts interface AICostTracker { trackCompletion(request: AIRequest, response: AIResponse): Promise getCurrentWeekCosts(): Promise getTotalCosts(): Promise } // Na elke AI call: const cost = calculateCost(response.usage) // based on token count await db.ai_events.create({ kind: 'summarize', request, response, cost_eur: cost, duration_ms: elapsed }) // Aggregate in build_metrics table wekelijks ``` **Cost Calculation:** - Input tokens: $3 / 1M tokens - Output tokens: $15 / 1M tokens - Convert USD → EUR based on current rate - Store in `ai_events.cost_eur` en aggregate naar `build_metrics.ai_api_cost` --- ## 5) Frontend implementatie ### 5.1 UI‑skelet (aangepast voor dual-site) **Marketing Layout:** ``` ┌─────────────────────────────────────────────────┐ │ Header: Logo | Nav (Build Log, Demo, Contact) │ ├─────────────────────────────────────────────────┤ │ │ │ Main Content Area │ │ (full width, no sidebar) │ │ │ ├─────────────────────────────────────────────────┤ │ Footer: Social Links | Cost Counter | CTA │ └─────────────────────────────────────────────────┘ ``` **App Layout (protected):** ``` ┌─────────────────────────────────────────────────┐ │ Topbar: Client Context | User Menu | Help │ ├───────────┬─────────────────────────────────────┤ │ Sidebar │ Main Content Area │ │ Nav │ (Dashboard/Intake/Profile/Plan) │ │ │ │ │ - Clients │ │ │ - Dashboard│ │ │ - Intake │ │ │ - Profile │ │ │ - Plan │ │ ├───────────┴─────────────────────────────────────┤ │ Toast Area (bottom right) │ └─────────────────────────────────────────────────┘ ``` ### 5.2 TipTap (ongewijzigd) Zie originele TO § 5.2 voor TipTap implementatie details. ### 5.3 AI Source Highlighting (ongewijzigd) Zie originele TO § 5.4 voor highlighting implementatie met TipTap Decorations API. ### 5.4 Onboarding System (NIEUW) **Architectuur:** ```typescript // components/onboarding/OnboardingProvider.tsx export function OnboardingProvider({ children }) { const { onboardingCompleted, tooltipsSeen } = useUiStore() const { mutate: updateProgress } = useOnboardingMutation() // Check if should show walkthrough useEffect(() => { if (!onboardingCompleted && isFirstTimeUser) { startWalkthrough() } }, []) return ( {children} ) } // components/onboarding/ContextualTooltip.tsx export function ContextualTooltip({ id, content, trigger = 'hover', showOnFirstUse = true }) { const { tooltipsSeen, markTooltipSeen } = useUiStore() const [isVisible, setIsVisible] = useState(false) useEffect(() => { if (showOnFirstUse && !tooltipsSeen.includes(id)) { setIsVisible(true) } }, []) const handleDismiss = () => { setIsVisible(false) markTooltipSeen(id) } return ( {content} } /> ) } // components/onboarding/HelpIcon.tsx export function HelpIcon({ topic }) { return ( {HELP_CONTENT[topic]} ) } ``` **Walkthrough Steps:** ```typescript const WALKTHROUGH_STEPS = [ { target: '.welcome-screen', content: 'Welkom bij Mini-ECD! AI bespaart je 50% administratietijd.', placement: 'center' }, { target: '.new-client-button', content: 'Start met een nieuwe cliënt aanmaken.', placement: 'bottom' }, { target: '.intake-editor', content: 'Schrijf je intake hier. Probeer de AI samenvatten knop!', placement: 'top' }, { target: '.dashboard-tiles', content: 'Je dashboard toont alle belangrijke info. Klik op tegels voor details.', placement: 'bottom' }, { target: '.help-menu', content: 'Je bent klaar! Help is altijd beschikbaar via dit menu.', placement: 'left' } ] ``` **Contextual Tooltips (voorbeelden):** ```typescript const CONTEXTUAL_TOOLTIPS = { 'first-ai-summarize': { trigger: 'first-use', content: 'AI kan je intake samenvatten in 5 seconden. Probeer het!' }, 'first-dsm-dropdown': { trigger: 'first-use', content: 'Laat AI een suggestie doen op basis van de intake' }, 'first-plan-publish': { trigger: 'first-use', content: 'Publiceren maakt het plan definitief en verhoogt versienummer' } } ``` **State Persistence:** ```typescript // localStorage backup for onboarding state const ONBOARDING_STORAGE_KEY = 'mini-ecd-onboarding' interface OnboardingState { completed: boolean lastStep: string tooltipsSeen: string[] version: string // voor migratie bij updates } // Sync met database maar fallback naar localStorage ``` **Dependencies:** - `react-joyride`: ^2.5.0 (walkthrough library) - Custom Tooltip component (shadcn/ui basis) - Custom HelpIcon component ### 5.5 Marketing Site Components (NIEUW) **Landing Page (`/app/(marketing)/page.tsx`):** ```typescript export default function LandingPage() { return (
{/* EPD build showcase */}
) } // components/marketing/LiveProofSection.tsx function LiveProofSection() { const { data: metrics } = useBuildMetrics() return (

Live Bewijs: Van €100k naar €50/mnd

Volg de volledige build →
) } // components/marketing/BuildMetricsDisplay.tsx function BuildMetricsDisplay({ metrics }) { const totalHours = metrics.reduce((sum, w) => sum + w.development_hours, 0) const totalCost = metrics.reduce((sum, w) => sum + w.infrastructure_cost + w.ai_api_cost, 0) return (
w.features_completed).length} />
) } ``` **Build Log (`/app/(marketing)/build-log/page.tsx`):** ```typescript export default function BuildLogPage() { const { data: weeks } = useBuildMetrics() return (

Build Log: 4 Weken naar Werkend EPD

{weeks.map(week => ( ))}
) } // components/marketing/WeekEntry.tsx function WeekEntry({ weekNumber, hours, cost, features, blogPostUrl, notes }) { return (

Week {weekNumber}

{hours}u development tijd
€{cost} kosten

Features Geleverd:

    {features.map(f =>
  • {f}
  • )}
{notes && (
Technische details {notes}
)} {blogPostUrl && ( )}
) } ``` **Lead Capture Form:** ```typescript // components/marketing/LeadCaptureForm.tsx export function LeadCaptureForm({ source }) { const { mutate: submitLead, isLoading } = useLeadMutation() const handleSubmit = async (data) => { await submitLead({ ...data, source, // 'landing' | 'build-log' | 'demo' | 'contact' }) // Track conversion trackEvent('lead_submitted', { source }) // Redirect to thank you page router.push('/thank-you') } return (