Zum Inhalt springen
Zurück

Die Architektur im Detail

Next.js, Payload CMS und die Macht der Composability

Lucke EDV~9 Min.

Die Architektur im Detail

Next.js, Payload CMS und die Macht der Composability

Teil 2 der Serie "Enterprise-Ready Multi-Tenant-Architekturen mit Next.js und Payload CMS"


Nachdem wir im ersten Artikel die geschäftlichen Vorteile moderner Architekturen beleuchtet haben, öffnen wir heute die Motorhaube. Wie erreicht man PageSpeed-Scores von 95+? Wie verwaltet man dutzende Mandanten mit einer Codebasis? Die Antwort liegt in der durchdachten Kombination modernster Technologien und bewährter Patterns.

Der Technology Stack: Best-of-Breed statt Kompromisse

Moderne Architekturen folgen dem Prinzip der Composability: Statt auf monolithische All-in-One-Lösungen zu setzen, komponieren wir die besten Tools für jeden Zweck zu einem harmonischen Ganzen.

```typescript
// Unser Enterprise Stack im Überblick
const enterpriseStack = {
frontend: {
framework: "Next.js 15.1.6",
ui: "React 19 mit Server Components",
styling: "Tailwind CSS + CSS Modules",
types: "TypeScript durchgängig"
},
backend: {
cms: "Payload CMS 3.48",
database: "PostgreSQL 16",
storage: "Vercel Blob Storage",
auth: "NextAuth + Payload Users"
},
infrastructure: {
hosting: "Vercel Edge Network",
cdn: "Global Edge Caching",
monitoring: "Vercel Analytics",
ci: "GitHub Actions"
}
}
```

Jede Komponente wurde bewusst gewählt und spielt eine spezifische Rolle im Gesamtsystem.

React Server Components: Die Revolution der Webentwicklung

React Server Components (RSC) sind der Game-Changer für Performance. Statt den gesamten React-Code an den Browser zu senden, wird er auf dem Server ausgeführt. Das Ergebnis: 70% kleinere JavaScript-Bundles.

Das Problem mit traditionellem React

```jsx
// Traditionell: Alles wird zum Client geschickt
'use client'
export function ProductList() {
const [products, setProducts] = useState([])

useEffect(() => {
// Daten werden client-seitig geladen
fetch('/api/products')
.then(res => res.json())
.then(setProducts)
}, [])

// Loading State, Error Handling, Re-rendering...
return <div>{/* Komplexe Client-Logik */}</div>
}
// Bundle-Größe: ~150KB nur für diese Komponente
```

Die Lösung mit Server Components

```tsx
// Modern: Ausführung auf dem Server
export async function ProductList() {
// Direkte Datenbankabfrage, kein API-Call nötig
const products = await getProductsFromDB()

// HTML wird fertig gerendert zum Client geschickt
return (
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<ProductCard key={product.id} {...product} />
))}
</div>
)
}
// Bundle-Größe: 0KB - nur HTML wird übertragen
```

Der Unterschied ist dramatisch: Keine Loading States, kein Layout Shift, sofortiger Content.

Payload CMS: Enterprise-CMS neu gedacht

Payload CMS ist nicht "noch ein Headless CMS". Es ist von Grund auf für moderne Architekturen konzipiert, mit TypeScript als First-Class-Citizen.

TypeScript durchgängig

```typescript
// Payload Collection Definition
export const Products: CollectionConfig = {
slug: 'products',
access: {
read: publicTenantReadAccess,
create: superAdminOrTenantAdmin,
},
fields: [
{
name: 'title',
type: 'text',
required: true,
localized: true,
},
{
name: 'price',
type: 'number',
min: 0,
required: true,
},
{
name: 'tenant',
type: 'relationship',
relationTo: 'tenants',
required: true,
hasMany: false,
}
],
hooks: {
beforeChange: [ensureTenantIsolation],
afterRead: [enrichWithComputedFields],
}
}

// Automatisch generierte TypeScript-Types
export interface Product {
id: string
title: string
price: number
tenant: string | Tenant
createdAt: string
updatedAt: string
}
```

Diese Types fließen durch die gesamte Anwendung – von der Datenbank bis zum Frontend. Keine Runtime-Fehler mehr durch Tippfehler oder falsche Annahmen.

Das Multi-Tenant Plugin

Das Herzstück unserer Architektur ist das offizielle Multi-Tenant-Plugin von Payload:

```typescript
// Multi-Tenant Konfiguration
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'

export default buildConfig({
plugins: [
multiTenantPlugin({
tenantCollection: 'tenants',
tenantField: 'tenant',
isolationStrategy: 'filter', // Automatische Filterung
sharedCollections: ['media'], // Geteilte Ressourcen
})
],
// Rest der Konfiguration
})
```

Mit dieser Konfiguration wird jede Abfrage automatisch nach Mandant gefiltert. Keine Chance für Datenlecks zwischen Mandanten.

Die Service Layer: Clean Architecture in der Praxis

Zwischen Frontend und CMS liegt unsere Service Layer – das Gehirn der Anwendung:

```typescript
// /lib/payload-services.ts
import { cache } from 'react'
import { getPayload } from 'payload'

// Request-Level Caching mit React.cache
export const getTenantBySlug = cache(async (slug: string) => {
const payload = await getPayload()

const result = await payload.find({
collection: 'tenants',
where: {
slug: { equals: slug }
},
depth: 2 // Lade Beziehungen mit
})

return result.docs[0] || null
})

// Tenant-aware Datenabfrage
export const getProductsByTenant = cache(async (
tenantId: string,
options?: {
limit?: number
category?: string
featured?: boolean
}
) => {
const payload = await getPayload()

const where: Where = {
tenant: { equals: tenantId },
status: { equals: 'published' }
}

if (options?.category) {
where.category = { equals: options.category }
}

if (options?.featured) {
where.featured = { equals: true }
}

const result = await payload.find({
collection: 'products',
where,
limit: options?.limit || 10,
sort: '-createdAt'
})

return result.docs
})
```

Diese Service Layer bietet mehrere Vorteile:

  1. Zentrale Business Logic: Alle Geschäftsregeln an einem Ort
  2. Type Safety: Durchgängige TypeScript-Unterstützung
  3. Caching: Automatisches Request-Level-Caching
  4. Testbarkeit: Services können isoliert getestet werden

PostgreSQL: Die Basis für Datenintegrität

Während NoSQL-Datenbanken ihren Platz haben, bietet PostgreSQL für Multi-Tenant-Systeme unschlagbare Vorteile:

```sql
-- Automatische Tenant-Isolation auf Datenbankebene
CREATE POLICY tenant_isolation ON products
FOR ALL
USING (tenant_id = current_setting('app.current_tenant')::uuid);

-- Referentielle Integrität
ALTER TABLE products
ADD CONSTRAINT fk_tenant
FOREIGN KEY (tenant_id)
REFERENCES tenants(id)
ON DELETE CASCADE;

-- Performante Indizes für Multi-Tenant-Abfragen
CREATE INDEX idx_products_tenant_status
ON products(tenant_id, status)
WHERE deleted_at IS NULL;
```

PostgreSQL garantiert ACID-Compliance, unterstützt komplexe Abfragen und skaliert vertikal bis zu enormen Datenmengen.

Das Routing-System: Subdomain-Magic

Wie erkennt das System automatisch den richtigen Mandanten? Durch intelligentes Routing:

```typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
const hostname = request.headers.get('host') || ''

// Extrahiere Subdomain
const subdomain = hostname.split('.')[0]

// Entwicklung: subdomain.localhost:3000
// Produktion: subdomain.ihredomain.de

if (subdomain && subdomain !== 'www' && subdomain !== 'admin') {
// Rewrite zu tenant-spezifischer Route
return NextResponse.rewrite(
new URL(/${subdomain}${request.nextUrl.pathname}, request.url)
)
}

return NextResponse.next()
}

export const config = {
matcher: ['/((?!api|_next/static|favicon.ico).*)']
}
```

Dieses Pattern ermöglicht URLs wie:

  • berlin.ihrefirma.de → Berlin-Niederlassung
  • shop.ihrefirma.de → Online-Shop
  • partner.ihrefirma.de → Partner-Portal

Alles mit derselben Codebasis.

Component-Driven Development: Der Baukasten-Ansatz

Moderne Architekturen sind modular. Jede Komponente ist ein eigenständiger, wiederverwendbarer Baustein:

```tsx
// Basis-Komponente mit Tenant-Theming
export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
tenantTheme
}) => {
const styles = {
primary: {
backgroundColor: tenantTheme?.primaryColor || '#0070f3',
color: tenantTheme?.primaryTextColor || '#ffffff'
},
secondary: {
borderColor: tenantTheme?.primaryColor || '#0070f3',
color: tenantTheme?.primaryColor || '#0070f3'
}
}

return (
<button
className="px-6 py-3 rounded-lg transition-all"
style={styles[variant]}
>
{children}
</button>
)
}

// Verwendung in verschiedenen Kontexten
<Button tenantTheme={berlinTheme}>Jetzt kaufen</Button>
<Button tenantTheme={münchenTheme} variant="secondary">Mehr erfahren</Button>
```

Einmal entwickelt, überall einsetzbar, individuell anpassbar.

Performance-Optimierung: Built-in, nicht Bolt-on

Performance ist kein nachträgliches Feature, sondern integraler Bestandteil der Architektur:

Parallele Datenabfragen

```typescript
// Schlecht: Sequentielle Abfragen
const tenant = await getTenant(slug)
const pages = await getPages(tenant.id) // Wartet auf tenant
const posts = await getPosts(tenant.id) // Wartet auf pages
// Gesamtzeit: 300ms + 200ms + 250ms = 750ms

// Gut: Parallele Abfragen
const [tenant, settings] = await Promise.all([
getTenant(slug),
getGlobalSettings()
])

const [pages, posts, products] = await Promise.all([
getPages(tenant.id),
getPosts(tenant.id),
getProducts(tenant.id)
])
// Gesamtzeit: max(300ms, 250ms) = 300ms
```

Edge Caching

```typescript
// Statische Seiten mit ISR (Incremental Static Regeneration)
export const revalidate = 3600 // Cache für 1 Stunde

export async function generateStaticParams() {
const tenants = await getAllTenants()
const pages = await getAllPages()

return tenants.flatMap(tenant =>
pages.map(page => ({
tenantSlug: tenant.slug,
pageSlug: page.slug
}))
)
}

// Seiten werden beim ersten Besuch generiert und gecacht
```

Access Control: Sicherheit by Design

Multi-Tenant-Systeme müssen Daten strikt isolieren. Unser Access Control System garantiert dies:

```typescript
// Standardisierte Access Control Patterns
export const publicTenantReadAccess: Access = ({ req }) => {
// Öffentliche Leser sehen nur ihren Mandanten
if (!req.user) {
return {
tenant: {
equals: req.context?.tenant?.id
}
}
}
return true
}

export const tenantAdminAccess: Access = ({ req }) => {
const user = req.user

// Super-Admins haben vollen Zugriff
if (user?.role === 'super-admin') return true

// Tenant-Admins nur auf ihren Bereich
if (user?.role === 'tenant-admin') {
return {
tenant: {
equals: user.tenant?.id
}
}
}

return false
}
```

Diese Patterns werden konsequent auf alle Collections angewendet. Kein Datenleck möglich.

Die Entwickler-Experience: Produktivität durch Tooling

Eine gute Architektur macht Entwickler produktiv und glücklich:

```bash

Type-Safe von Anfang an

pnpm generate:types # Generiert Types aus Payload-Schema

Hot Reload in < 1 Sekunde

pnpm dev # Next.js Fast Refresh + Payload Admin

Automatische Formatierung

pnpm lint # ESLint + Prettier

Database Migrations

pnpm migrate:create # Erstellt neue Migration
pnpm migrate:run # Führt Migrations aus

Testing

pnpm test # Jest + React Testing Library
pnpm test:e2e # Playwright E2E Tests
```

Die Werkzeuge arbeiten nahtlos zusammen. Keine Konfigurationshölle, keine Inkompatibilitäten.

Real-World Patterns: Aus der Praxis für die Praxis

Pattern 1: Lazy Loading für Heavy Components

```tsx
// Dynamischer Import für große Komponenten
import dynamic from 'next/dynamic'

const InteractiveMap = dynamic(
() => import('@/components/InteractiveMap'),
{
loading: () => <MapSkeleton />,
ssr: false // Nur client-seitig laden
}
)

// Komponente wird erst geladen, wenn benötigt
export function ContactPage() {
return (
<div>
<h1>Kontakt</h1>
<Suspense fallback={<MapSkeleton />}>
<InteractiveMap />
</Suspense>
</div>
)
}
```

Pattern 2: Optimistic Updates

```tsx
// Sofortiges UI-Feedback, während Server arbeitet
export function LikeButton({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes)
const [isLiking, setIsLiking] = useState(false)

async function handleLike() {
// Optimistic Update
setLikes(prev => prev + 1)
setIsLiking(true)

try {
await likePost(postId)
} catch (error) {
// Rollback bei Fehler
setLikes(prev => prev - 1)
toast.error('Fehler beim Liken')
} finally {
setIsLiking(false)
}
}

return (
<button onClick={handleLike} disabled={isLiking}>
❤️ {likes}
</button>
)
}
```

Die Architektur-Entscheidungen: Das "Warum" hinter dem "Was"

Warum Next.js statt Gatsby/Remix/Astro?

  • App Router: Modernste React-Features out-of-the-box
  • Vercel-Integration: Optimales Deployment und Edge Functions
  • Hybrid Rendering: SSG, SSR und ISR in einer App
  • Community: Größtes Ökosystem, beste Unterstützung

Warum Payload CMS statt Strapi/Directus/Sanity?

  • TypeScript-First: Durchgängige Type-Safety
  • Code-basierte Konfiguration: Versionierbar, reviewbar
  • Multi-Tenant-Plugin: Enterprise-Feature out-of-the-box
  • Local API: Keine HTTP-Overhead für Server-Abfragen

Warum PostgreSQL statt MongoDB/MySQL?

  • ACID-Compliance: Transaktionale Integrität
  • JSON-Support: Flexibilität wo nötig
  • Row-Level Security: Native Multi-Tenant-Unterstützung
  • Bewährt: 30+ Jahre Entwicklung und Optimierung

Fazit: Die Summe ist größer als ihre Teile

Die vorgestellte Architektur ist kein zufälliges Zusammenspiel von Technologien. Jede Komponente wurde bewusst gewählt und optimiert, um zusammen ein System zu schaffen, das:

  • Performant ist (95+ PageSpeed)
  • Skalierbar wächst (unbegrenzte Mandanten)
  • Wartbar bleibt (klare Patterns)
  • Sicher operiert (strikte Isolation)
  • Entwicklerfreundlich funktioniert (moderne Tools)

Diese Architektur ist nicht theoretisch – sie läuft produktiv, sie skaliert, sie liefert messbare Geschäftsergebnisse.


Key Technical Takeaways

React Server Components reduzieren Bundle-Größen um 70%

  • Weniger JavaScript = schnellere Ladezeiten

Payload CMS bietet Enterprise-Features out-of-the-box

  • Multi-Tenant, Versionierung, Lokalisierung

Service Layer Pattern sorgt für Clean Architecture

  • Testbar, wartbar, erweiterbar

PostgreSQL garantiert Datenintegrität

  • ACID, Constraints, Row-Level Security

Component-Driven Development ermöglicht Wiederverwendung

  • Einmal bauen, überall nutzen

Im nächsten Artikel: "Performance als Wettbewerbsvorteil" – Wir zeigen konkret, wie wir 95+ PageSpeed Scores erreichen und was das für Ihr Business bedeutet.

Bleiben Sie dran für Teil 3 der Serie →


Über diese Serie

Dies ist Teil 2 einer 5-teiligen Serie über Enterprise-Ready Multi-Tenant-Architekturen.

Weitere Artikel
KI & Tech
· 14 Min.

KI-Realitätscheck 1. bis 14. Juni 2026: Was sich wirklich bewegt hat

Die Halbwertszeit einer KI-Schlagzeile liegt weiter bei etwa 48 Stunden. In den ersten zwei Juni-Wochen wurde aus "der Bundestag verabschiedet ein Durchführungsgesetz" ein "Deutschland reguliert KI scharf", aus einem freiwilligen Verhaltenskodex eine "Pflicht zum Wasserzeichen", und aus einer Exportkontroll-Anordnung ein "Anthropic schaltet Claude ab".

Toni Haupt
KI & Tech
· 15 Min.

KI-Realitätscheck 2026: Was zwischen April und Juni wirklich passiert ist (und was davon nur Lärm war)

Die Halbwertszeit einer KI-Schlagzeile liegt bei ungefähr 48 Stunden. In dieser Zeitspanne wird aus "Microsoft schränkt intern ein Tool ein" ein "Microsoft wirft Claude raus", aus "ein Automatisierungstool verdoppelt seinen Wert" ein "n8n ist tot", und aus einer verschobenen Frist ein "die EU kippt das KI-Gesetz". Wer beruflich mit künstlicher Intelligenz arbeitet, steht damit vor einem doppelten Problem: Man muss nicht nur die Entwicklungen kennen, sondern auch laufend zwischen Signal und Lärm unterscheiden.

Toni Haupt