Skip to main content
Version: V2-Next

Frontend Architecture Developer Guide

As of: 2026-05-18

Purpose

This guide explains the detailed technical architecture of the Portal Frontend and how to extend it at code-level.

Quick Overview

portal-frontend is a Next.js app using the App Router. It implements the portal for CIVITAS/CORE V2 and provides administration interfaces for data, data sources, data structures, users, groups, roles, permissions, and data spaces.

Core architectural decisions:

  • Next.js App Router with Server Components for initial data loading and Client Components for interactive interfaces.
  • NextAuth/Auth.js with Keycloak as the OIDC provider.
  • BFF proxy at /api/[...path] so browser requests do not go directly to APISIX or the mock server.
  • TanStack Query for server state in the browser, TanStack Table for lists.
  • React Hook Form and Zod for form state and validation.
  • shadcn/ui, Radix UI, Tailwind CSS v4, and Lucide Icons as the UI foundation.
  • Standalone editor modules for the Pipeline Editor and UML Modeler, each with local reducer/context state.

System Context

Browser
|
| Next.js UI, NextAuth Session Cookie
v
portal-frontend
|
| /api/auth/*
v
Keycloak
```text

```text
portal-frontend
|
| /api/[...path] with Bearer Token
| Header x-api-request: true
v
APISIX Gateway /v1
|
v
Backend Services
portal-frontend
|
| /api/[...path] without x-api-request
v
JSON Server Mock

The internal proxy uses the header x-api-request: true to decide whether a request is routed to the real API gateway or the local JSON Server. The JSON Server is marked as a temporary development and testing path.

Technology Stack

AreaImplementation
FrameworkNext.js 15 App Router, React 19, TypeScript strict
AuthenticationNextAuth 5 beta, Keycloak Provider, JWT Session Strategy
Server StateTanStack Query 5
TablesTanStack Table 8
FormsReact Hook Form, Zod, @hookform/resolvers
UIshadcn/ui, Radix UI, Tailwind CSS v4, Lucide Icons
Visual Editors@xyflow/react, Monaco Editor
Internationalizationnext-intl, message files de.json and en.json
LoggingPino, Pretty Transport in development only
TestsVitest, Testing Library, Playwright, Storybook
Package Managerpnpm

Source Code Structure

portal-frontend/
auth.ts, auth.config.ts NextAuth/Keycloak setup
next.config.ts Next.js and next-intl plugin
src/
app/ App Router, layouts, routes, API routes
(main)/ Protected portal pages
api/[...path]/route.ts BFF proxy to backend or JSON Server
api/auth/[...nextauth]/ NextAuth route handler
services/api/ Domain API hooks and server requests
components/ Shared UI and feature components
ui/ shadcn/ui components
uml-modeler/ Reusable UML editor
contexts/ App-wide React contexts
hooks/ Reusable client hooks
i18n/ Locale configuration
lib/ Server fetch, logger, token utils, UI utils
messages/ Translations
types/ Domain types and Zod schemas
utils/ Mappers, request parameters, error/form helpers
e2e/ Playwright E2E tests
playwright/ Auth/setup helpers for E2E
json-server/ Local mock backend dataset

Layer Dependencies (strict, no inversion allowed):

Pages → Components → Hooks → API Services → Schemas/Types

No layer reaches upward. No cross-cutting between peer layers without a shared abstraction point. Utilities and src/lib are cross-cutting and may be used by all layers.

App Shell and Routing

The app uses the Next.js App Router under src/app.

src/app/layout.tsx is the root layout. It sets up global providers:

  • SessionProvider for NextAuth client access.
  • SessionManager for activity-based session refresh.
  • NextIntlClientProvider with locale from next-intl.
  • QueryProvider for TanStack Query.
  • IBM Plex Sans and IBM Plex Mono as global fonts.

src/app/(main)/layout.tsx is the protected portal layout:

  • Loads currentUser server-side via getCurrentUser().
  • Redirects to /api/auth/signout on auth errors, and to /error on other errors.
  • Hydrates currentUser into the TanStack Query cache.
  • Renders sidebar, header, content area, toaster, and UnsavedChangesProvider.

The domain pages are located under src/app/(main), for example:

  • datasets, datasources, datastructures
  • users, groups, roles, permissions
  • dataspaces

List and detail pages often follow this pattern:

  1. The page file is a Server Component.
  2. The Server Component reads route or search parameters.
  3. Initial data is loaded via serverFetch.
  4. A Client Component renders the table, actions, modals, and mutations.

Example: src/app/(main)/datasets/page.tsx loads datasets server-side and passes data to DatasetsList.

Authentication and Session

Auth is centralized in auth.ts and auth.config.ts.

Key points:

  • Keycloak is the only configured provider.
  • NextAuth uses session.strategy = 'jwt'.
  • Session max age is 10 hours.
  • Access token, refresh token, and ID token are held in the JWT.
  • Before the access token expires, a refresh is performed via the Keycloak token endpoint.
  • After three failed refresh attempts, the JWT callback chain is terminated.
  • On sign-out, the Keycloak logout endpoint is called with client_id and optionally id_token_hint.

src/middleware.ts protects portal routes:

  • Unauthenticated users are redirected to /login.
  • /login and /api/auth/* are public.
  • Static assets and API routes are excluded from the matcher.

The middleware also sets security headers:

  • Per-request CSP with nonces for scripts.
  • Stricter CSP in production than in development.
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: strict-origin-when-cross-origin

Decision: Security headers are in the middleware because CSP nonces must be generated per request.

Data Access

There are two request paths.

Server-Side Access

Server Components and Server Actions use src/lib/serverFetch.ts.

Characteristics:

  • Reads the NextAuth JWT server-side from the encrypted cookie via getToken.
  • Adds Authorization: Bearer <access_token>.
  • Can route to the JSON Server or the real API backend path.
  • isApiBackend: true selects APISIX at API_BASE_URL:API_PORT/v1.
  • Sets cache: 'no-store'.
  • Normalizes backend responses with content, totalElements, and totalPages.
  • Throws AuthError on missing session, missing access token, or backend 401.

Domain server requests are located at src/app/services/api/*/serverRequests.ts.

Client-Side Access

Client Components use domain hooks from src/app/services/api/*/clientRequests.ts.

The hooks call apiRequest(). apiRequest():

  • Builds URLs as /api${endpoint}.
  • Uses the central Axios client from src/app/services/api/client/client.ts.
  • Sets Cache-Control: no-store.
  • Normalizes responses via the Axios interceptor.
  • Returns { data, totalElements, totalPages }.

The Axios client has baseURL = process.env.NEXT_SERVER_URL and withCredentials = true. Browser requests therefore stay within the portal origin and go through the Next.js proxy.

BFF Proxy

src/app/api/[...path]/route.ts is the central proxy.

Responsibilities:

  • Reads the NextAuth JWT from the request.
  • Checks access token and refresh errors.
  • Adds Authorization: Bearer <access_token> for backend requests.
  • Removes host and cookie before forwarding requests.
  • Routes to the real API gateway based on x-api-request: true, otherwise to the JSON Server.
  • Streams non-JSON responses directly.
  • Removes problematic encoding/length headers for JSON responses.
  • Logs request context without sensitive headers or tokens.

Decision: The proxy keeps tokens and backend coordinates server-side. The browser only knows the portal URL and the NextAuth cookie.

API and Domain Modules

Each domain encapsulates its API access:

src/app/services/api/
datasets/
serverRequests.ts
clientRequests.ts
datasources/
datastructures/
groups/
roles/
users/
permissions/
assignments/
pipelines/

Typical client hooks:

  • useGet* for queries.
  • useCreate*, useUpdate*, usePatch*, useDelete* for mutations.
  • Status/transition hooks for datasets, datasources, and datastructures.

Shared hook building blocks:

  • useDataQuery
  • useCreateMutation
  • useUpdateMutation
  • useDeleteMutation

These building blocks standardize:

  • Query keys.
  • Endpoint construction.
  • Mutation invalidation.
  • Error logging.

Real backend requests (typically) set:

headers: { 'x-api-request': 'true' }

Decision: Domain components are unaware of proxy details. They work with domain hooks.

Server State, URL State, and Local State

Server State

TanStack Query is configured globally in src/app/providers/queryClient.tsx:

  • Default staleTime: 60 seconds.
  • On the server, a new query client is always created.
  • In the browser, a singleton query client is reused.
  • currentUser is pre-populated server-side in the main layout and hydrated.

useGetCurrentUser() uses its own staleTime of 5 minutes because permissions typically do not change within a running session.

URL State

Lists store pagination, sorting, search, and tabs in search params:

  • page
  • size
  • sort
  • q
  • tab
  • subtab

useQueryParams() and useTableSearchParams() map UI state to URL parameters. This makes lists stable across refreshes and URLs shareable.

Form State

Forms use React Hook Form with Zod resolvers. Domain types and Zod schemas are typically located in src/types/*.

Typical pattern:

  • API response schema.
  • API create/update/patch schema.
  • Form schema.
  • Mapper from API data to form data.
  • pickDirtyValues() for partial updates.

Status-dependent entities (Dataset, Datasource, Datastructure) distinguish two validation levels:

  • Draft Form Schema – Permissive validation for incomplete entities. Required fields may be absent.
  • Available Form Schema – Strict validation that must be satisfied before an entity can be published.

Both schemas are Zod objects whose TypeScript types are derived via z.infer. The same hook determines which schema is active for submit validation based on currentStatus.

Unsaved Changes

UnsavedChangesProvider protects navigation when there is dirty state:

  • beforeunload for tab closing or hard URL changes.
  • GuardedLink for internal navigation.
  • requestNavigation() and requestBack() show an exit warning modal when there is dirty state.
  • useRegisterUnsavedChanges() registers dirty state and optionally a save handler.

This pattern is used by form pages and the Pipeline Editor.

Permissions

Permissions come from CurrentUser.assignments.

usePermissions() provides:

  • hasPermission(permission)
  • hasAnyPermission(...permissions)
  • hasScopedPermission(permission, scopeType, scopeId)

Scoped assignments are filtered: non-TENANT scopes retain only permissions with a matching prefix, for example DATASOURCE_* for a DATASOURCE scope.

UI gating occurs for example at:

  • Sidebar navigation.
  • Create/Edit/Delete buttons.
  • Status and release actions.
  • Scoped editing of datasets, datasources, and datastructures.

Decision: The frontend hides UI actions based on permissions. Authoritative enforcement remains the responsibility of the backend.

UI Architecture

The UI layer consists of three levels:

  1. Primitive UI components from src/components/ui, generated or oriented toward shadcn/ui and Radix.
  2. Reusable portal components, for example PageContainer, PageHeader, DataTable, SearchHeader, ContentCard, modals, and form fields.
  3. Domain components under the respective route folders.

Styling:

  • Tailwind CSS v4 via src/app/globals.css.
  • Design tokens as CSS custom properties.
  • IBM Plex fonts in the root layout.
  • cn() combines clsx and tailwind-merge.
  • Icons come from lucide-react.
  • Toasts come from sonner.

Tables:

  • DataTable encapsulates TanStack Table rendering.
  • Pagination is in TablePagination.
  • Header sorting and ARIA sort are handled centrally.
  • Long cell content can be rendered with a tooltip.

Decision: shadcn/ui components are stored in the repository. This makes components customizable and not embedded as an external black box. The src/components/ui/** folder is excluded from ESLint.

Internationalization

next-intl is integrated via next.config.ts, src/i18n/*, and the root layout.

Locale detection:

  1. Cookie NEXT_LOCALE
  2. Accept-Language header
  3. Default de

Supported locales:

  • de
  • en

The language switcher in LanguageSelect sets NEXT_LOCALE for one year and calls router.refresh().

Decision: Routes are not locale-prefixed. The language is determined via cookie/header and provider.

Pipeline Editor

Path:

src/app/(main)/datasets/[datasetId]/data-flow/pipeline-editor/

The Pipeline Editor is a standalone feature module for visual data flows.

Building blocks:

  • PipelineEditorWrapper provides ReactFlowProvider.
  • PipelineEditorLayout assembles palette, canvas, toolbar, tabs, and inspector.
  • PipelineEditorProviderComponent is the central context provider.
  • usePipelineSession() manages multiple pipeline tabs.
  • pipelineService contains the reducer and graph operations.
  • validationService contains validation rules.
  • payloadBuilderService builds backend payloads.
  • modelBuilderService builds RedPandaConnect models from the graph.

State decision:

  • The editor uses local reducer/context state instead of global app state.
  • Session state and active pipeline operations are kept separate.
  • Dirty sessions are integrated into the global unsaved-changes mechanism.

Backend integration:

  • Pipelines are loaded, created, updated, and deleted under /datasets/{datasetId}/pipelines.
  • Requests go through the BFF proxy with x-api-request: true.
  • New pipelines initially have no backend ID; after creation, the ID is taken into the session.

Payload:

  • name
  • description
  • styles with viewport, node positions, nodes, and edges for frontend roundtrip.
  • dataSourceIds
  • apis
  • persistences
  • model as a RedPandaConnect model generated from the graph, or an empty object.

Validation:

  • Start node required.
  • End node required.
  • At least one functional node.
  • API request requires API response.
  • Configuration required for relevant nodes.
  • Quartz cron expression is validated.
  • Orphaned nodes are marked as errors.

Decision: Validation is explicitly required before saving. After changes, the last validation is invalidated.

UML Modeler

Path:

src/components/uml-modeler/

The UML Modeler is implemented as a reusable component module and is currently embedded in data structure versions.

Architecture:

  • UmlModeler is the entry point and sets up read-only context.
  • MultiSessionLayout renders tabs, toolbar, canvas, palette, and inspector.
  • useMultiSessionManager() manages diagram sessions.
  • ActiveDiagramProviderComponent connects session state with diagram operations.
  • diagramService contains the reducer, graph operations, and connection validation.
  • XMI import/export is in separate services.

State decision:

  • As with the Pipeline Editor, there is a two-layer structure:
    • Session management for tabs.
    • Active diagram operations for canvas, nodes, and edges.
  • Semantic diagram changes mark the model field as dirty.
  • Selection and dimension changes should not create functional dirty state.

Persistence model:

  • modelUploadService builds payloads with styles and model.
  • styles contains viewport and node positions.
  • model is XMI without layout information.

Decision: The functional model and display state are stored separately. This allows the backend to process the model while the frontend can restore the editor view.

Deployment and Runtime Configuration

The Dockerfile builds a lean runtime image:

  • node:22-alpine
  • pnpm install --frozen-lockfile --prod in the deps stage
  • Runtime contains .next, public, package.json, and node_modules
  • npm/npx are removed
  • Non-root user nextjs
  • PORT=80
  • Started via next start

Important runtime variables:

  • NEXTAUTH_SECRET
  • NEXT_SERVER_URL
  • KEYCLOAK_CLIENT_ID
  • KEYCLOAK_CLIENT_SECRET
  • KEYCLOAK_ISSUER
  • API_BASE_URL
  • API_PORT
  • LOG_LEVEL

Build-time variables with NEXT_PUBLIC_*:

  • NEXT_PUBLIC_TENANT_NAME
  • NEXT_PUBLIC_SESSION_IDLE_TIMEOUT_SECONDS

Decision: Secrets and backend coordinates remain server-side. NEXT_PUBLIC_* is embedded by Next.js into the client bundle and must therefore be fixed at build time.

Tests and Quality

Unit and component tests:

  • Vitest with happy-dom.
  • Testing Library for React components.
  • Coverage for src/**/*.{ts,tsx} and auth.config.ts.

E2E:

  • Playwright under e2e/.
  • Setup projects under playwright/ for sweep and auth.
  • Local dev server is automatically started with pnpm dev.
  • Chromium locally, Firefox/WebKit in CI only.

Storybook:

  • Stories and markdown documentation exist for individual components.
  • Storybook is available via pnpm storybook.

Linting:

  • ESLint flat config.
  • Next Core Web Vitals.
  • TypeScript recommended.
  • Import sorting via simple-import-sort.
  • Unused imports are errors.
  • Prettier is integrated, semicolons are disabled.
  • Legacy tags object, embed, applet are forbidden.

Architectural Decisions

1. Next.js as Full-Stack Frontend

The app uses Next.js not only for rendering but also for auth routes, BFF proxy, and server-side initial loading.

Benefits:

  • No direct browser access to backend URLs.
  • Server Components can load initial data with auth context.
  • Client Components remain focused on interaction.

2. BFF Proxy Instead of Direct API Access

Browser requests go to /api/[...path]. The proxy appends the access token and routes to the target system.

Benefits:

  • Tokens are not managed manually in the browser.
  • APISIX and JSON Server coordinates remain server-side.
  • A single point for auth checking, header sanitization, and logging.

Trade-off:

  • Every real backend request must follow the header convention x-api-request: true.

3. Server Components for Initial Data, Client Hooks for Mutations

Lists and detail pages load initially server-side. Interactions subsequently go through TanStack Query and domain hooks.

Benefits:

  • Fast initial page render with ready data.
  • Mutations can invalidate locally and then trigger router.refresh() if needed.
  • Domain hooks encapsulate recurring CRUD patterns.

4. URL as State for Lists

Pagination, search, sorting, and tabs are stored in query params.

Benefits:

  • Stable across reloads.
  • Shareable URLs.
  • Server Components can load directly from search params.

5. Permissions in the CurrentUser Cache

currentUser is loaded in the main layout and hydrated into the query cache.

Benefits:

  • Sidebar and action buttons can be rendered immediately based on permissions.
  • Components use the same usePermissions() hook.

Boundary:

  • UI gating is a convenience and protection against misuse. The backend must still authorize every action.

6. Local Editor State Instead of Global Store

Pipeline Editor and UML Modeler use reducer/context within the respective feature module.

Benefits:

  • High interaction frequency remains locally contained.
  • No global store dependency for isolated editor functions.
  • Session and diagram/pipeline operations remain testably separate.

7. Store Model and Display State Separately

Pipeline and UML modules store functional models separately from layout/viewport information.

Benefits:

  • Backend can evaluate functional models.
  • Frontend can reconstruct the editor view.
  • Roundtrip remains possible without mixing layout details into the functional model.

8. shadcn/ui as Source-Owned Design System

UI primitives reside as code in the repository.

Benefits:

  • Customizable to project conventions.
  • Composable with Tailwind tokens and Radix accessibility.
  • No runtime lock-in on an external component library.

Extension Conventions

New domain:

  1. Create types and Zod schemas under src/types/<domain>.ts.
  2. Create API hooks under src/app/services/api/<domain>/clientRequests.ts.
  3. Create server requests under serverRequests.ts if Server Components need to load initial data.
  4. Create a route under src/app/(main)/<domain>.
  5. Map list URL state via useQueryParams() or useTableSearchParams().
  6. Account for permissions via PERMISSION_NAMES and usePermissions().

New backend integration:

  1. Mark real backend requests with headers: { 'x-api-request': 'true' }. Note: This convention is known to be architecturally fragile (a missing header silently routes to the JSON Server). It will be replaced by explicit backendRequest()/mockRequest() functions. Until then: always set the header explicitly, never omit it.
  2. Align response shape with ApiResponse<T> or the Axios normalization.
  3. Keep query keys stable and domain-oriented.
  4. Invalidate after successful mutations.

New form:

  1. Define a Zod form schema.
  2. Use useForm with zodResolver.
  3. Build the API payload via a mapper or pickDirtyValues().
  4. Register dirty state with useRegisterUnsavedChanges() for navigation-critical forms.

New editor node in the Pipeline Editor:

  1. Extend node type and node data in _types.
  2. Add the node component and inspector panel.
  3. Extend the node registry and palette.
  4. Add validation rules.
  5. Review the payload/model builder.

Glossary

TermMeaning
PageServer Component as the entry point of a route. Loads initial data, delegates rendering.
ComponentReusable UI building block: either a shadcn/Radix primitive, a portal layout shell, or a domain component.
HookCustom React function that encapsulates state, data access, or side effects.
Client RequestsReact Query hook wrappers for browser-side API calls through the BFF proxy.
Server RequestsAsync functions for direct backend access in Server Components via serverFetch.
SchemaZod object that defines runtime validation and TypeScript type in one.
API Proxy / BFF ProxyNext.js catch-all route (/api/[...path]) that attaches auth tokens to requests and forwards them to the correct target system.
Draft SchemaPermissive Zod schema for entities in draft status. Required fields may be absent.
Available SchemaStrict Zod schema that must be fully satisfied before an entity can be published.
Domain HookEntity-specific hook (e.g., useGetDatasets) as a thin wrapper around generic CRUD hooks.
TanStack QueryServer state library for caching, invalidation, and synchronization of backend data in the browser.
BFFBackend for Frontend – here the Next.js internal proxy that protects the browser from direct backend access.
x-api-requestRouting header of the BFF proxy. Routes a request to the real API gateway instead of the JSON Server mock. Will be replaced by explicit functions in the future.