# Phase 4: Marketplace & Portal Navigation - Research **Researched:** 2026-06-22 **Domain:** Frontend Marketplace UI, Sidebar Navigation Enhancement, Tenant Context Switching **Confidence:** HIGH ## Summary Phase 4 builds a marketplace view for browsing and activating modules, enhances the sidebar with search/filter and individual module links, and adds a Super-Admin tenant context selector. The critical finding is that nearly all backend infrastructure already exists: the module registry API (GET /modules, GET /modules/active, POST activate/deactivate) is complete, the Prisma schema has Module and TenantModuleActivation models, and the TenantMiddleware already supports `x-tenant-id` header for Super-Admin tenant switching. The tenant list API (GET /tenants) is also available and restricted to SUPER_ADMIN role. This phase is primarily a frontend implementation phase. The backend needs only minor extensions: an endpoint to fetch modules with their activation status for a specific tenant (combined view for marketplace cards), and potentially a lightweight endpoint to get tenant list for the context selector (the existing GET /tenants already works). The frontend work involves creating the marketplace page with card grid, search, category filters, status tabs, activation/deactivation flows, a module detail page, and enhancing the existing sidebar with search functionality and active-state highlighting via `usePathname`. **Primary recommendation:** Leverage existing API endpoints and patterns. Focus frontend work on the marketplace page components (MarketplaceCard, search, filters, tenant selector) and sidebar enhancements (SidebarSearch, usePathname-based active state, individual module links). Use the existing admin/modules page pattern for activation toggle logic. All components are hand-rolled following established shadcn-compatible CSS variable conventions. ## User Constraints (from CONTEXT.md) ### Locked Decisions - **D-01:** Marketplace als Karten-Grid (wie VS Code Extensions / App Store). Jede Karte zeigt Icon, Name, Kurzbeschreibung, Kategorie-Badge, Aktivierungs-Status. - **D-02:** Suchleiste + Kategorie-Filter oben auf Marketplace-Seite. Live-Filter waehrend Tippen. Kategorie-Chips oder Dropdown. - **D-03:** Empty State: Freundliche Illustration + Text ("Noch keine Module verfuegbar"). - **D-04:** Super-Admin bekommt leichtgewichtigen Mandanten-Kontext-Selector im Marketplace-Bereich (Dropdown zur Mandanten-Auswahl). Aktivierung/Deaktivierung erfolgt aus Perspektive des gewaehlten Mandanten. Kein volles Impersonation-System — nur Kontext-Wechsel fuer Modul-Management (volle Impersonation bleibt v2: FEAT-V2-04). - **D-05:** Regulaerer Admin aktiviert Module direkt fuer seinen eigenen Mandanten (kein Kontext-Wechsel noetig). ### Claude's Discretion - Marketplace Filter/Status-Unterscheidung: Tab-Ansicht vs. Badge-System vs. Kombination — basierend auf UX Best Practices - Bestaetigungs-Dialog bei Aktivierung: Dialog vs. sofortige Aktion mit Undo-Toast — UX-Entscheidung - Visuelles Feedback nach Aktivierung: Toast + Badge-Update vs. Animation — passend zum existierenden Design - Sidebar Kategorien-Darstellung: Bestehendes Accordion beibehalten vs. flache Liste mit Gruppen-Header — basierend auf Modul-Anzahl und UX - Sidebar collapsed Modus: Modul-Icons anzeigen vs. nur Haupt-Navigation — basierend auf existierender Sidebar-Logik - Sidebar Suchfeld: Im Kategorien-Bereich vs. globale Header-Suche vs. beides — basierend auf PRTAL-05 und Komplexitaet - Modul-Detail-Ansicht Tiefe: Ausfuehrlich (App-Store-Stil) vs. kompakt — passend fuer v1 Modul-Anzahl - Marketplace-Detail vs. Modul-Nutzung Navigation: Separate Seiten vs. Tabs vs. anderes Pattern — basierend auf App Router Architektur ### Deferred Ideas (OUT OF SCOPE) - **FEAT-V2-04: Volle Admin-Impersonation** — Komplettes "Als Mandant agieren"-Feature kommt in v2. Phase 4 baut nur leichtgewichtigen Kontext-Selector fuer Modul-Management. ## Phase Requirements | ID | Description | Research Support | |----|-------------|------------------| | MRKT-01 | Uebersicht aller verfuegbaren Module mit Beschreibung und Kategorie | Marketplace page at `/marketplace` with card grid. GET /modules API exists. ModuleCard pattern reusable. UI-SPEC defines card anatomy with icon, name, description, category badge, status badge. | | MRKT-02 | Admin kann Module pro Mandant aktivieren und deaktivieren | POST /modules/:id/activate and /deactivate APIs exist. Admin/modules page has toggle pattern. Marketplace adds button-based activation on cards. TenantMiddleware supports x-tenant-id for Super-Admin context switch. | | MRKT-03 | Nur aktivierte Module erscheinen in der Seitenleiste des Mandanten | Sidebar already fetches GET /modules/active and groups by category. Need to add individual module links under each category and search/filter. | | MRKT-04 | Module sind in Kategorien gruppiert | Category data is part of Module model (category field). Sidebar already groups by category. Marketplace adds CategoryFilter chips. | | PRTAL-02 | Linke Seitenleiste zeigt Kategorien und aktivierte Module | Sidebar already shows categories and modules. Need to enhance with individual clickable module items under each category (currently links to category page, needs per-module links). | | PRTAL-03 | Ausgewaehltes Modul oeffnet sich im Hauptbereich (Mitte) | Module route `/modules/{category}/{slug}` exists with lazy-loaded component rendering in main area. Need active-state highlighting in sidebar via usePathname. | | PRTAL-05 | Module koennen in der Seitenleiste durchsucht und gefiltert werden | New SidebarSearch component needed between main nav and categories section. Client-side filtering of module names and category names. | ## Architectural Responsibility Map | Capability | Primary Tier | Secondary Tier | Rationale | |------------|-------------|----------------|-----------| | Module catalog display | Frontend (Client) | API (Backend) | GET /modules already returns all modules. Client renders grid with filtering. All filtering is client-side (small dataset, all modules loaded). | | Module activation/deactivation | API (Backend) | Frontend (Client) | Business logic (tenant-module association, upsert) lives in backend service. Frontend sends POST and updates UI optimistically. | | Tenant context switching (Super-Admin) | API (Backend) | Frontend (Client) | TenantMiddleware reads x-tenant-id header. Frontend dropdown sets header on subsequent requests. | | Sidebar module navigation | Frontend (Client) | -- | Pure client-side: fetches active modules, groups by category, renders links, handles search/filter. | | Sidebar active state | Frontend (Client) | -- | usePathname comparison against link hrefs. No backend involvement. | | Module detail view | Frontend (Client) | API (Backend) | Frontend renders detail page at /marketplace/[slug]. Module data from same GET /modules response. | | i18n for marketplace copy | Frontend (Client) | -- | next-intl with DE/EN translation files. All strings from UI-SPEC copywriting contract. | ## Standard Stack ### Core (Already Installed -- No New Dependencies) | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | Next.js | 15.3.x | Frontend framework, App Router | Already installed, provides routing, dynamic imports, SSR [VERIFIED: apps/web/package.json] | | React | 19.x | UI library | Already installed [VERIFIED: apps/web/package.json] | | Zustand | 5.0.x | Client state management | Already used for sidebar-store, auth-store [VERIFIED: apps/web/src/lib/stores/] | | next-intl | 4.13.x | Internationalization | Already used for all UI strings [VERIFIED: apps/web/src/messages/] | | next-themes | 0.4.x | Theme switching | Already installed [VERIFIED: apps/web/package.json] | | Tailwind CSS | 4.x | Styling | Already configured with OKLCH tokens [VERIFIED: apps/web/src/app/globals.css] | | NestJS | 11.x | Backend API | Already running with module-registry controller [VERIFIED: apps/api/package.json] | | Prisma | 6.x | ORM | Schema has Module + TenantModuleActivation [VERIFIED: apps/api/prisma/schema.prisma] | ### Supporting (No New Libraries Needed) This phase requires **zero new npm packages**. All functionality is achievable with existing dependencies: - **Search/filter:** Native string matching, `useState` for filter state - **Debounce:** Simple `setTimeout`/`clearTimeout` pattern (no lodash needed for one debounce) - **Toast notifications:** Hand-rolled toast component with CSS animations (no sonner/react-hot-toast needed -- project convention is hand-rolled components) - **Dialog:** Hand-rolled modal with focus trap (project convention, no @radix-ui/react-dialog) - **URL routing:** Next.js App Router dynamic routes (`/marketplace/[slug]`) - **Active state:** `usePathname()` from `next/navigation` ### Alternatives Considered | Instead of | Could Use | Tradeoff | |------------|-----------|----------| | Hand-rolled toast | sonner / react-hot-toast | Would introduce a new dependency. Project convention is hand-rolled components following shadcn CSS variable naming. Toast is simple enough to build. | | Hand-rolled dialog | @radix-ui/react-dialog | Same reasoning. Only one dialog needed (deactivation confirm). Focus trap can be done with a few lines of JS. | | Hand-rolled debounce | lodash.debounce | Adding lodash for a single 4-line debounce function is wasteful. | **Installation:** ```bash # No new packages needed for this phase ``` ## Package Legitimacy Audit > No new external packages are installed in this phase. All functionality uses existing project dependencies. | Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition | |---------|----------|-----|-----------|-------------|---------|-------------| | (none) | -- | -- | -- | -- | -- | No new packages | **Packages removed due to [SLOP] verdict:** none **Packages flagged as suspicious [SUS]:** none ## Architecture Patterns ### System Architecture Diagram ``` User Browser | v [Next.js App Router - (portal) route group] | +-- /marketplace ---------> MarketplacePage | | |-- MarketplaceSearch (debounced text input) | | |-- StatusFilter (tabs: All/Active/Available) | | |-- CategoryFilter (horizontal chips) | | |-- TenantContextSelector (Super-Admin only) | | |-- MarketplaceCard[] (grid) | | |-- Activate/Deactivate button | | |-- Link to /marketplace/[slug] | | | +-- /marketplace/[slug] -> ModuleDetailView | |-- Back link | |-- Full description, status, activation button | +-- Sidebar (enhanced) | |-- Dashboard link | |-- Marketplace link | |-- SidebarSearch (filters categories+modules) | |-- Categories accordion | | |-- Category header (link to /modules/{cat}) | | |-- Module items (link to /modules/{cat}/{slug}) | |-- Admin section (role-gated) | |-- Active state via usePathname() | +-- API calls (fetch with credentials: 'include') | v [NestJS API - existing endpoints] |-- GET /modules -> all registered modules |-- GET /modules/active -> tenant's active modules |-- POST /modules/:id/activate -> activate for tenant |-- POST /modules/:id/deactivate -> deactivate for tenant |-- GET /tenants -> all tenants (SUPER_ADMIN) | +-- TenantMiddleware |-- Reads x-tenant-id header for Super-Admin context switch |-- Falls back to JWT tenantId for regular users ``` ### Recommended Project Structure ``` apps/web/src/ ├── app/(portal)/ │ ├── marketplace/ │ │ ├── page.tsx # Marketplace grid page │ │ ├── [slug]/ │ │ │ └── page.tsx # Module detail page │ │ └── components/ │ │ ├── MarketplaceCard.tsx # Card with activation controls │ │ ├── MarketplaceSearch.tsx # Debounced search input │ │ ├── CategoryFilter.tsx # Category chip row │ │ ├── StatusFilter.tsx # Status tab bar │ │ ├── TenantContextSelector.tsx # Super-Admin tenant dropdown │ │ ├── ActivationDialog.tsx # Deactivation confirmation dialog │ │ └── Toast.tsx # Toast notification component │ └── ...existing routes... ├── components/layout/ │ ├── sidebar.tsx # MODIFIED: add SidebarSearch, usePathname, module links │ ├── sidebar-search.tsx # NEW: search input for sidebar │ └── ...existing layout components... ├── lib/stores/ │ └── marketplace-store.ts # NEW: Zustand store for marketplace state (optional) └── messages/ ├── de.json # MODIFIED: add marketplace translations └── en.json # MODIFIED: add marketplace translations ``` ### Pattern 1: Client-Side Filtering with Multiple Dimensions **What:** Marketplace loads all modules once, then filters client-side by search text, status tab, and category chip. Three filter dimensions compose together. **When to use:** Small dataset (modules are typically < 100), all data available from initial fetch, instant feedback needed. **Example:** ```typescript // Source: Codebase convention — client-side state filtering // All three filters compose: search AND status AND category const filteredModules = useMemo(() => { return allModules.filter((mod) => { // Text search: name + description const matchesSearch = !searchQuery || mod.name.toLowerCase().includes(searchQuery.toLowerCase()) || getLocalizedDescription(mod.description).toLowerCase().includes(searchQuery.toLowerCase()); // Status filter: 'all' | 'active' | 'available' const matchesStatus = statusFilter === 'all' || (statusFilter === 'active' && activationMap.has(mod.id)) || (statusFilter === 'available' && !activationMap.has(mod.id)); // Category filter: 'all' | specific category slug const matchesCategory = categoryFilter === 'all' || mod.category === categoryFilter; return matchesSearch && matchesStatus && matchesCategory; }); }, [allModules, searchQuery, statusFilter, categoryFilter, activationMap]); ``` ### Pattern 2: Tenant Context Header for Super-Admin **What:** Super-Admin switches tenant context via dropdown. All subsequent API calls include `x-tenant-id` header. The existing TenantMiddleware already reads this header. **When to use:** When Super-Admin needs to manage modules for a specific tenant. **Example:** ```typescript // Source: Existing pattern in apps/api/src/tenant/tenant.middleware.ts // Frontend sends x-tenant-id header for Super-Admin context switching const fetchModulesForTenant = async (tenantId: string) => { const [allRes, activeRes] = await Promise.all([ fetch(`${API_URL}/modules`, { credentials: 'include' }), fetch(`${API_URL}/modules/active`, { credentials: 'include', headers: tenantId ? { 'x-tenant-id': tenantId } : {}, }), ]); // ...process responses }; ``` ### Pattern 3: Sidebar Active State with usePathname **What:** Current page URL determines which sidebar item gets the active class. Uses `usePathname()` from `next/navigation`. **When to use:** Replace the hardcoded Dashboard active state with dynamic path matching. **Example:** ```typescript // Source: Next.js App Router convention import { usePathname } from 'next/navigation'; const pathname = usePathname(); const isActive = (href: string) => { if (href === '/') return pathname === '/'; return pathname.startsWith(href); }; // Usage in sidebar link: ``` ### Pattern 4: Hand-Rolled Toast System **What:** Lightweight toast notification system matching project convention (no external library). Uses a Zustand store or React context for state, CSS transitions for animations. **When to use:** Activation/deactivation feedback, error notifications. **Example:** ```typescript // Toast store pattern — minimal Zustand approach import { create } from 'zustand'; interface Toast { id: string; type: 'success' | 'error'; message: string; } interface ToastState { toasts: Toast[]; addToast: (type: Toast['type'], message: string) => void; removeToast: (id: string) => void; } export const useToastStore = create()((set) => ({ toasts: [], addToast: (type, message) => { const id = crypto.randomUUID(); set((s) => ({ toasts: [...s.toasts, { id, type, message }] })); setTimeout(() => { set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })); }, 4000); }, removeToast: (id) => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })), })); ``` ### Anti-Patterns to Avoid - **Fetching activation status per card individually:** All modules and their activation status should be fetched in parallel (one GET /modules + one GET /modules/active), then merged client-side. The admin/modules page already does this pattern with `Promise.all`. [VERIFIED: apps/web/src/app/(portal)/admin/modules/page.tsx] - **Storing marketplace filter state in URL search params:** For this MVP, local React state is sufficient. URL params add complexity (back/forward, sharing URLs with filters). Keep it simple. - **Building a full impersonation system:** CONTEXT.md explicitly defers this (FEAT-V2-04). The tenant context selector is ONLY a dropdown that changes which tenant's activation status is displayed/modified. - **Re-fetching sidebar modules on every navigation:** The sidebar should re-fetch active modules only after an activation/deactivation action, not on every route change. Use a callback or event pattern. - **Using `` tags instead of Next.js ``:** The current sidebar uses raw `` tags, which causes full page reloads. Phase 4 should migrate sidebar links to Next.js `` for client-side navigation. ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | Module registry API | New CRUD endpoints | Existing GET /modules, GET /modules/active, POST activate/deactivate | Fully implemented in Phase 3, tested and working [VERIFIED: module-registry.controller.ts] | | Tenant context switching | Custom auth/session override | Existing x-tenant-id header in TenantMiddleware | Already supports Super-Admin context switching [VERIFIED: tenant.middleware.ts L33] | | Tenant list for selector | New API endpoint | Existing GET /tenants (SUPER_ADMIN only) | Returns all tenants with user count [VERIFIED: tenant.controller.ts] | | Module lazy loading | New import system | Existing MODULE_REGISTRY + loadModuleComponent | Whitelist-based dynamic imports [VERIFIED: lib/module-loader.ts] | | Localized descriptions | Custom i18n for module data | Existing pattern: `description[locale]` fallback chain | Module.description is JSON with locale keys [VERIFIED: ModuleCard.tsx L73] | | Design token system | New CSS variables | Existing OKLCH tokens in globals.css | Complete light/dark mode token set [VERIFIED: globals.css] | **Key insight:** This phase is 90% frontend work. The backend APIs, data models, middleware, and patterns all exist from Phase 2 and Phase 3. The only new code needed is the marketplace UI components, sidebar enhancements, and i18n translations. ## Common Pitfalls ### Pitfall 1: Sidebar Uses Raw `` Tags (Full Page Reloads) **What goes wrong:** The current sidebar.tsx uses raw HTML `` tags for all navigation links. This causes full-page reloads instead of client-side navigation. **Why it happens:** Phase 1 implemented the sidebar skeleton without Next.js navigation patterns. **How to avoid:** Replace all `` with Next.js `` in the sidebar. This is required for the usePathname-based active state to work correctly (no page reload means pathname persists in React state). **Warning signs:** Page flickers on sidebar clicks; loading spinner appears on every navigation. ### Pitfall 2: Race Condition in Activation + Sidebar Refresh **What goes wrong:** After activating a module, the marketplace card updates optimistically but the sidebar re-fetch may return stale data if the activation hasn't fully propagated. **Why it happens:** POST activate returns the activation record, but GET /modules/active is a separate query. **How to avoid:** After successful POST activate/deactivate, wait for the response, then trigger sidebar re-fetch. The optimistic update on the marketplace card provides immediate feedback while sidebar refresh catches up. Consider a shared event bus or callback. **Warning signs:** Module shows as "Aktiviert" in marketplace but doesn't appear in sidebar. ### Pitfall 3: Tenant Context Not Applied to All API Calls **What goes wrong:** Super-Admin changes tenant in the selector but only the module list updates; activation still targets the Super-Admin's own tenant. **Why it happens:** The x-tenant-id header must be included in EVERY API call that should respect the selected tenant context (GET /modules/active AND POST activate/deactivate). **How to avoid:** Create a wrapper function for API calls that automatically includes the x-tenant-id header based on the selected tenant. Store the selected tenant in React state, pass it through to all fetch calls. **Warning signs:** Super-Admin activates module for "Firma A" but it activates for their own tenant. ### Pitfall 4: Missing Locale Handling for Module Descriptions **What goes wrong:** Module description shows as `[object Object]` or is blank. **Why it happens:** Module.description is a JSON field containing `{ "de": "...", "en": "..." }`. If rendered directly without locale extraction, it breaks. **How to avoid:** Follow existing ModuleCard pattern: extract `description[locale] || description.en || description.de || ''`. The locale can be detected from `document.documentElement.lang` (existing pattern). **Warning signs:** Cards show object notation or empty descriptions. ### Pitfall 5: Categories Accordion State Reset on Re-render **What goes wrong:** Sidebar search filters modules, but when the search is cleared, all category accordions collapse to their default state. **Why it happens:** The categories accordion state (open/closed) is local component state that resets when the component re-renders with new data. **How to avoid:** Track which categories are expanded in stable state (set or map keyed by category name). When search filter changes, don't reset the accordion state. **Warning signs:** User expands a category, types in search, clears search, and the category is collapsed again. ## Code Examples ### Verified Pattern: Parallel Module + Activation Fetch ```typescript // Source: apps/web/src/app/(portal)/admin/modules/page.tsx (existing pattern) // Fetch all modules and active modules in parallel, merge into activation map const [allRes, activeRes] = await Promise.all([ fetch(`${API_URL}/modules`, { credentials: 'include' }), fetch(`${API_URL}/modules/active`, { credentials: 'include', headers: selectedTenantId ? { 'x-tenant-id': selectedTenantId } : {}, }), ]); const allModules: Module[] = allRes.ok ? await allRes.json() : []; const activeModules: Module[] = activeRes.ok ? await activeRes.json() : []; // Build activation map: moduleId -> boolean const activationMap = new Map(); for (const mod of activeModules) { activationMap.set(mod.id, true); } ``` ### Verified Pattern: Module Card with Localized Description ```typescript // Source: apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx const locale = (typeof window !== 'undefined' && document.documentElement.lang) || 'de'; const localizedDescription = typeof description === 'string' ? description : description[locale] || description.en || description.de || ''; ``` ### Verified Pattern: Toggle Activation API Call ```typescript // Source: apps/web/src/app/(portal)/admin/modules/page.tsx const action = currentlyActive ? 'deactivate' : 'activate'; const res = await fetch(`${API_URL}/modules/${moduleId}/${action}`, { method: 'POST', credentials: 'include', headers: selectedTenantId ? { 'x-tenant-id': selectedTenantId } : {}, }); ``` ### Verified Pattern: Tenant List Fetch (Super-Admin) ```typescript // Source: apps/api/src/tenant/tenant.controller.ts — GET /tenants returns: // [{ id, name, slug, isActive, createdAt, userCount }] const fetchTenants = async (): Promise => { const res = await fetch(`${API_URL}/tenants`, { credentials: 'include' }); if (!res.ok) return []; return res.json(); }; ``` ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | Sidebar uses `` tags | Should use Next.js `` | Phase 4 (now) | Enables client-side navigation, usePathname active state | | Sidebar Dashboard always "active" | Dynamic active state via usePathname | Phase 4 (now) | Correct visual feedback for current page | | Category-only links in sidebar | Per-module links under each category | Phase 4 (now) | Direct module navigation from sidebar | | Admin modules page for activation | Marketplace page with richer UX | Phase 4 (now) | Better discovery, search, filtering for end users | **Deprecated/outdated:** - Admin modules page remains available but marketplace becomes the primary discovery/activation interface for users - Raw `` tags in sidebar should be replaced with `` for performance ## Assumptions Log > List all claims tagged `[ASSUMED]` in this research. The planner and discuss-phase use this > section to identify decisions that need user confirmation before execution. | # | Claim | Section | Risk if Wrong | |---|-------|---------|---------------| | A1 | Toast system can be implemented as a simple Zustand store + positioned div, no library needed | Architecture Patterns | Low -- toast is a simple UI pattern, worst case is slightly rougher animations than a library provides | | A2 | Deactivation confirmation dialog focus trap can be hand-rolled in ~10 lines | Architecture Patterns | Low -- if accessibility requirements demand more robust focus management, could add @radix-ui/react-dialog later | | A3 | All modules fit on one page without pagination | Architecture Patterns | Low -- v1 will have very few modules. If wrong, add client-side pagination later | **If this table is empty:** Most claims in this research were verified directly from the codebase -- minimal assumptions needed. ## Open Questions 1. **Sidebar Module Refresh Mechanism** - What we know: Sidebar fetches `/modules/active` on mount. Admin modules page updates state locally after toggle. - What's unclear: How to signal sidebar to re-fetch after marketplace activation. Options: (a) poll on interval, (b) event bus, (c) shared Zustand store that both marketplace and sidebar read, (d) callback passed via context. - Recommendation: Use a shared Zustand store or a simple `useCallback` passed through props. Zustand is already the state management pattern in this project. 2. **Marketplace Page vs. Admin Modules Page Overlap** - What we know: Admin modules page (admin/modules) already allows activation toggle. Marketplace adds richer UX. - What's unclear: Should admin modules page be kept as-is, deprecated, or redirect to marketplace? - Recommendation: Keep both. Admin modules page is a quick admin-only toggle view. Marketplace is the user-facing discovery interface. They serve different use cases. ## Environment Availability | Dependency | Required By | Available | Version | Fallback | |------------|------------|-----------|---------|----------| | Node.js | Next.js, NestJS | yes | 24.16.0 | -- | | pnpm | Package management | yes | 9.15.0 | -- | | Docker | Container runtime | yes | 29.5.3 | -- | | Docker Compose | Service orchestration | yes | 5.1.4 | -- | | Turborepo | Build orchestration | yes | 2.9.18 | -- | **Missing dependencies with no fallback:** none **Missing dependencies with fallback:** none ## Validation Architecture ### Test Framework | Property | Value | |----------|-------| | Framework | Vitest (specified in CLAUDE.md, not yet installed) | | Config file | none -- see Wave 0 | | Quick run command | `pnpm vitest run --reporter=verbose` | | Full suite command | `pnpm vitest run` | ### Phase Requirements to Test Map | Req ID | Behavior | Test Type | Automated Command | File Exists? | |--------|----------|-----------|-------------------|-------------| | MRKT-01 | Marketplace grid renders all modules with descriptions and categories | unit | `pnpm vitest run apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx -x` | -- Wave 0 | | MRKT-02 | Activation toggle calls correct API endpoint with tenant context | unit | `pnpm vitest run apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx -x` | -- Wave 0 | | MRKT-03 | Sidebar shows only active modules | unit | `pnpm vitest run apps/web/src/components/layout/sidebar.test.tsx -x` | -- Wave 0 | | MRKT-04 | Modules grouped by category in grid and sidebar | unit | `pnpm vitest run apps/web/src/app/(portal)/marketplace/marketplace.test.tsx -x` | -- Wave 0 | | PRTAL-02 | Sidebar displays categories and activated module links | unit | `pnpm vitest run apps/web/src/components/layout/sidebar.test.tsx -x` | -- Wave 0 | | PRTAL-03 | Module opens in main content area, sidebar active state updates | integration | Manual verification -- requires full Next.js routing | -- Manual | | PRTAL-05 | Sidebar search filters modules by name | unit | `pnpm vitest run apps/web/src/components/layout/sidebar-search.test.tsx -x` | -- Wave 0 | ### Sampling Rate - **Per task commit:** `pnpm vitest run --reporter=verbose` (if test infra exists) - **Per wave merge:** Full test suite - **Phase gate:** Full suite green + manual verification of navigation flows ### Wave 0 Gaps - [ ] Vitest installation: `pnpm add -D vitest @testing-library/react @testing-library/jest-dom jsdom` in apps/web - [ ] `apps/web/vitest.config.ts` -- Vitest config for Next.js with jsdom environment - [ ] `apps/web/src/test/setup.ts` -- Test setup with @testing-library/jest-dom matchers - [ ] Test utilities for mocking fetch, Zustand stores, and next-intl ## Security Domain ### Applicable ASVS Categories | ASVS Category | Applies | Standard Control | |---------------|---------|-----------------| | V2 Authentication | yes (existing) | JWT auth via global JwtAuthGuard -- no changes needed | | V3 Session Management | no | No new session handling in this phase | | V4 Access Control | yes | RolesGuard on activate/deactivate endpoints (ADMIN/SUPER_ADMIN). TenantMiddleware for tenant isolation. x-tenant-id only respected for SUPER_ADMIN role. [VERIFIED: module-registry.controller.ts, tenant.middleware.ts] | | V5 Input Validation | yes | Module ID validated as UUID by Prisma parameterized queries. Search input is client-side only (no server-side SQL). Category filter uses module data, not user input for DB queries. | | V6 Cryptography | no | No cryptographic operations in this phase | ### Known Threat Patterns for This Phase | Pattern | STRIDE | Standard Mitigation | |---------|--------|---------------------| | Tenant context spoofing via x-tenant-id header | Elevation of Privilege | TenantMiddleware checks `user.role === 'SUPER_ADMIN'` before reading header [VERIFIED: tenant.middleware.ts L33] | | Unauthorized module activation | Elevation of Privilege | RolesGuard requires ADMIN or SUPER_ADMIN [VERIFIED: module-registry.controller.ts] | | XSS via module description | Tampering | Module descriptions are rendered as text content (not innerHTML). React auto-escapes. | | CSRF on activation endpoints | Tampering | Cookie-based auth with `credentials: 'include'` + same-origin policy. POST endpoints require valid JWT. | ## Sources ### Primary (HIGH confidence) - `apps/api/src/module-registry/module-registry.controller.ts` -- Full module registry API with activate/deactivate - `apps/api/src/module-registry/module-registry.service.ts` -- Service with findAll, findActiveForTenant, activateForTenant, deactivateForTenant - `apps/api/src/tenant/tenant.middleware.ts` -- TenantMiddleware with x-tenant-id header support for SUPER_ADMIN - `apps/api/src/tenant/tenant.controller.ts` -- GET /tenants for Super-Admin tenant list - `apps/api/prisma/schema.prisma` -- Module + TenantModuleActivation models - `apps/web/src/components/layout/sidebar.tsx` -- Current sidebar implementation with category accordion - `apps/web/src/app/(portal)/admin/modules/page.tsx` -- Existing activation toggle pattern - `apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx` -- Module card pattern - `apps/web/src/lib/module-loader.ts` -- MODULE_REGISTRY and lazy loading - `apps/web/src/app/globals.css` -- OKLCH design tokens, layout variables - `apps/web/src/lib/stores/auth-store.ts` -- AuthUser with role and tenantId - `.planning/phases/04-marketplace-portal-navigation/04-UI-SPEC.md` -- UI design contract - `.planning/phases/04-marketplace-portal-navigation/04-CONTEXT.md` -- User decisions ### Secondary (MEDIUM confidence) - `.planning/phases/01-foundation-portal-shell/01-CONTEXT.md` -- Phase 1 design decisions (sidebar, header, color scheme) - `.planning/phases/02-authentication-multi-tenancy/02-CONTEXT.md` -- Auth/RBAC decisions (SUPER_ADMIN, ADMIN, USER roles) - `.planning/phases/03-module-system-domaincheck/03-CONTEXT.md` -- Module system decisions (SDK, registry, categories) ### Tertiary (LOW confidence) - None -- all critical findings verified from codebase ## Metadata **Confidence breakdown:** - Standard stack: HIGH -- all libraries verified from package.json, no new dependencies needed - Architecture: HIGH -- all backend APIs verified from source code, patterns established in previous phases - Pitfalls: HIGH -- identified from direct codebase analysis (raw `` tags, missing usePathname, race conditions) **Research date:** 2026-06-22 **Valid until:** 2026-07-22 (stable -- no external API changes, pure frontend work)