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
| Area | Implementation |
|---|---|
| Framework | Next.js 15 App Router, React 19, TypeScript strict |
| Authentication | NextAuth 5 beta, Keycloak Provider, JWT Session Strategy |
| Server State | TanStack Query 5 |
| Tables | TanStack Table 8 |
| Forms | React Hook Form, Zod, @hookform/resolvers |
| UI | shadcn/ui, Radix UI, Tailwind CSS v4, Lucide Icons |
| Visual Editors | @xyflow/react, Monaco Editor |
| Internationalization | next-intl, message files de.json and en.json |
| Logging | Pino, Pretty Transport in development only |
| Tests | Vitest, Testing Library, Playwright, Storybook |
| Package Manager | pnpm |
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:
SessionProviderfor NextAuth client access.SessionManagerfor activity-based session refresh.NextIntlClientProviderwith locale fromnext-intl.QueryProviderfor TanStack Query.- IBM Plex Sans and IBM Plex Mono as global fonts.
src/app/(main)/layout.tsx is the protected portal layout:
- Loads
currentUserserver-side viagetCurrentUser(). - Redirects to
/api/auth/signouton auth errors, and to/erroron other errors. - Hydrates
currentUserinto 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,datastructuresusers,groups,roles,permissionsdataspaces
List and detail pages often follow this pattern:
- The page file is a Server Component.
- The Server Component reads route or search parameters.
- Initial data is loaded via
serverFetch. - 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_idand optionallyid_token_hint.
src/middleware.ts protects portal routes:
- Unauthenticated users are redirected to
/login. /loginand/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: nosniffX-Frame-Options: DENYReferrer-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: trueselects APISIX atAPI_BASE_URL:API_PORT/v1.- Sets
cache: 'no-store'. - Normalizes backend responses with
content,totalElements, andtotalPages. - Throws
AuthErroron 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
hostandcookiebefore 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:
useDataQueryuseCreateMutationuseUpdateMutationuseDeleteMutation
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.
currentUseris 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:
pagesizesortqtabsubtab
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:
beforeunloadfor tab closing or hard URL changes.GuardedLinkfor internal navigation.requestNavigation()andrequestBack()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:
- Primitive UI components from
src/components/ui, generated or oriented toward shadcn/ui and Radix. - Reusable portal components, for example
PageContainer,PageHeader,DataTable,SearchHeader,ContentCard, modals, and form fields. - 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()combinesclsxandtailwind-merge.- Icons come from
lucide-react. - Toasts come from
sonner.
Tables:
DataTableencapsulates 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:
- Cookie
NEXT_LOCALE Accept-Languageheader- Default
de
Supported locales:
deen
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:
PipelineEditorWrapperprovidesReactFlowProvider.PipelineEditorLayoutassembles palette, canvas, toolbar, tabs, and inspector.PipelineEditorProviderComponentis the central context provider.usePipelineSession()manages multiple pipeline tabs.pipelineServicecontains the reducer and graph operations.validationServicecontains validation rules.payloadBuilderServicebuilds backend payloads.modelBuilderServicebuilds 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:
namedescriptionstyleswith viewport, node positions, nodes, and edges for frontend roundtrip.dataSourceIdsapispersistencesmodelas 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:
UmlModeleris the entry point and sets up read-only context.MultiSessionLayoutrenders tabs, toolbar, canvas, palette, and inspector.useMultiSessionManager()manages diagram sessions.ActiveDiagramProviderComponentconnects session state with diagram operations.diagramServicecontains 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
modelfield as dirty. - Selection and dimension changes should not create functional dirty state.
Persistence model:
modelUploadServicebuilds payloads withstylesandmodel.stylescontains viewport and node positions.modelis 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-alpinepnpm install --frozen-lockfile --prodin the deps stage- Runtime contains
.next,public,package.json, andnode_modules - npm/npx are removed
- Non-root user
nextjs PORT=80- Started via
next start
Important runtime variables:
NEXTAUTH_SECRETNEXT_SERVER_URLKEYCLOAK_CLIENT_IDKEYCLOAK_CLIENT_SECRETKEYCLOAK_ISSUERAPI_BASE_URLAPI_PORTLOG_LEVEL
Build-time variables with NEXT_PUBLIC_*:
NEXT_PUBLIC_TENANT_NAMENEXT_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}andauth.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,appletare 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:
- Create types and Zod schemas under
src/types/<domain>.ts. - Create API hooks under
src/app/services/api/<domain>/clientRequests.ts. - Create server requests under
serverRequests.tsif Server Components need to load initial data. - Create a route under
src/app/(main)/<domain>. - Map list URL state via
useQueryParams()oruseTableSearchParams(). - Account for permissions via
PERMISSION_NAMESandusePermissions().
New backend integration:
- 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 explicitbackendRequest()/mockRequest()functions. Until then: always set the header explicitly, never omit it. - Align response shape with
ApiResponse<T>or the Axios normalization. - Keep query keys stable and domain-oriented.
- Invalidate after successful mutations.
New form:
- Define a Zod form schema.
- Use
useFormwithzodResolver. - Build the API payload via a mapper or
pickDirtyValues(). - Register dirty state with
useRegisterUnsavedChanges()for navigation-critical forms.
New editor node in the Pipeline Editor:
- Extend node type and node data in
_types. - Add the node component and inspector panel.
- Extend the node registry and palette.
- Add validation rules.
- Review the payload/model builder.
Glossary
| Term | Meaning |
|---|---|
| Page | Server Component as the entry point of a route. Loads initial data, delegates rendering. |
| Component | Reusable UI building block: either a shadcn/Radix primitive, a portal layout shell, or a domain component. |
| Hook | Custom React function that encapsulates state, data access, or side effects. |
| Client Requests | React Query hook wrappers for browser-side API calls through the BFF proxy. |
| Server Requests | Async functions for direct backend access in Server Components via serverFetch. |
| Schema | Zod object that defines runtime validation and TypeScript type in one. |
| API Proxy / BFF Proxy | Next.js catch-all route (/api/[...path]) that attaches auth tokens to requests and forwards them to the correct target system. |
| Draft Schema | Permissive Zod schema for entities in draft status. Required fields may be absent. |
| Available Schema | Strict Zod schema that must be fully satisfied before an entity can be published. |
| Domain Hook | Entity-specific hook (e.g., useGetDatasets) as a thin wrapper around generic CRUD hooks. |
| TanStack Query | Server state library for caching, invalidation, and synchronization of backend data in the browser. |
| BFF | Backend for Frontend – here the Next.js internal proxy that protects the browser from direct backend access. |
| x-api-request | Routing 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. |