Files
tessera-ctl/.planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md
T
2026-06-22 14:06:42 +02:00

34 KiB

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>

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.

</user_constraints>

<phase_requirements>

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.

</phase_requirements>

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:

# 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
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:

// 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:

// 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:

// 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:
<a href={href} className={cn(
  'flex items-center gap-3 rounded-md px-2 py-2 text-sm transition-colors',
  isActive(href)
    ? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
    : 'text-sidebar-foreground hover:bg-muted'
)}>

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:

// 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<ToastState>()((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 <a> tags instead of Next.js <Link>: The current sidebar uses raw <a> tags, which causes full page reloads. Phase 4 should migrate sidebar links to Next.js <Link> 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 <a> Tags (Full Page Reloads)

What goes wrong: The current sidebar.tsx uses raw HTML <a> 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 <a href> with Next.js <Link href> 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

// 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<string, boolean>();
for (const mod of activeModules) {
  activationMap.set(mod.id, true);
}

Verified Pattern: Module Card with Localized Description

// 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

// 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)

// Source: apps/api/src/tenant/tenant.controller.ts — GET /tenants returns:
// [{ id, name, slug, isActive, createdAt, userCount }]
const fetchTenants = async (): Promise<Tenant[]> => {
  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 <a> tags Should use Next.js <Link> 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 <a> tags in sidebar should be replaced with <Link> 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 <a> tags, missing usePathname, race conditions)

Research date: 2026-06-22 Valid until: 2026-07-22 (stable -- no external API changes, pure frontend work)