# Tech Stack Dokumentation

## my.experimenta.science

**Version:** 1.0
**Datum:** 28. Oktober 2025

---

## Übersicht

Dieser Dokument beschreibt den vollständigen Tech Stack für die my.experimenta.science E-Commerce App und begründet die getroffenen Entscheidungen.

---

## 1. Frontend Framework

### Nuxt 4

**Entscheidung:** Nuxt 4 (Vue.js Meta-Framework)

**Begründung:**

- **Vue 3 Composition API:** Moderne, reaktive Architektur
- **Server-Side Rendering (SSR):** Bessere Performance & SEO
- **File-based Routing:** Intuitive Projektstruktur
- **Auto-Import:** Weniger Boilerplate-Code
- **Hybrid Rendering:** Flexible Rendering-Modi (SSR, SSG, CSR)
- **TypeScript Support:** Native TypeScript-Integration
- **Server APIs:** Backend & Frontend in einem Projekt
- **Active Development:** Nuxt 4 ist die neueste Version mit langfristigem Support

**Alternativen:**

- **Next.js (React):** Großartig, aber Team bevorzugt Vue
- **SvelteKit:** Weniger Ecosystem, kleinere Community
- **Remix:** Noch jünger, weniger etabliert

**Version:** `^4.0.0`

---

## 2. UI Framework

### Optionen: Nuxt UI vs. shadcn-vue

#### Option A: Nuxt UI

**Pro:**

- Speziell für Nuxt entwickelt
- Out-of-the-box Integration
- Headless UI Components
- Tailwind CSS basiert
- Dark Mode Support
- Accessibility eingebaut
- Icons & Colors mitgeliefert
- Nuxt-native (optimiert)

**Contra:**

- Kleinere Component-Bibliothek
- Weniger flexibel als shadcn
- Stärkere Meinung zum Styling

**Best for:** Schneller Start, weniger Customization

---

#### Option B: shadcn-nuxt

**Pro:**

- Copy-Paste Components (volle Kontrolle)
- Radix Vue (hochwertige Primitives)
- Vollständig anpassbar
- Kein Package Lock-in
- Große Component-Bibliothek
- Sehr flexibel
- Hervorragende TypeScript-Support
- **Nuxt-native:** Offizielle Nuxt-Integration

**Contra:**

- Manuelles Hinzufügen von Components via CLI
- Mehr Setup als Nuxt UI

**Best for:** Maximale Anpassung, langfristige Wartbarkeit

---

### Empfehlung: shadcn-nuxt

**Grund:**

- Corporate Design Integration benötigt maximale Flexibilität
- Copy-Paste-Ansatz = kein Vendor Lock-in
- Vollständige Kontrolle über Komponenten
- Einfacher zu debuggen (Code liegt im Projekt)
- Bessere Anpassung an experimenta Styleguide möglich
- Offizielle Nuxt-Module Integration

**Installation:**

```bash
npx nuxi@latest module add shadcn-nuxt
npx shadcn-nuxt@latest init
```

**Official Docs:** https://www.shadcn-vue.com/docs/installation/nuxt.html

**Dependencies:**

- `shadcn-nuxt` - Nuxt module
- `radix-vue` - UI Primitives (auto-installed)
- `tailwindcss` - Utility-first CSS
- `class-variance-authority` - Varianten-Management
- `tailwind-merge` - Classname-Merging
- `clsx` - Conditional Classes

---

## 3. Styling

### Tailwind CSS

**Entscheidung:** Tailwind CSS v4 (Beta)

**Begründung:**

- **Utility-First:** Schnelle Entwicklung
- **Responsive Design:** Mobile-first eingebaut
- **Customization:** Einfache Theme-Anpassung
- **Performance:** PurgeCSS integriert (kleine Bundles)
- **Developer Experience:** Hervorragende IDE-Integration (Intellisense)
- **Konsistenz:** Design-Tokens erzwingen Konsistenz

**Corporate Design Integration:**

```javascript
// tailwind.config.js
export default {
  theme: {
    extend: {
      colors: {
        experimenta: {
          primary: '#YOUR_PRIMARY_COLOR',
          secondary: '#YOUR_SECONDARY_COLOR',
          // ... weitere Farben aus Styleguide
        },
      },
      fontFamily: {
        sans: ['Your Corporate Font', 'sans-serif'],
      },
    },
  },
}
```

---

## 4. Datenbank

### PostgreSQL

**Entscheidung:** PostgreSQL 16+

**Begründung:**

- **Relational:** Klare Datenstrukturen für E-Commerce
- **ACID-Compliant:** Transaktionssicherheit für Bestellungen
- **Robust:** Bewährte Technologie
- **Performance:** Exzellent für Read-Heavy Workloads
- **JSON Support:** Flexibilität für metadata
- **Full-Text Search:** Native Suchfunktionen
- **Extensions:** PostGIS, pg_trgm, etc.
- **Open Source:** Keine Lizenzkosten

**Hosting:**

- Docker Container auf Hetzner Proxmox
- Persistent Volume für Daten
- Automated Backups (tägliche Snapshots)

**Connection Pooling:**

- `pg_bouncer` für effizientes Connection Management

---

## 5. ORM

### Drizzle ORM

**Entscheidung:** Drizzle ORM

**Begründung:**

- **TypeScript-First:** Native TypeScript ohne Code-Generation
- **Performance:** Minimal Runtime Overhead
- **SQL-Like Syntax:** Leicht zu lernen für SQL-Kenner
- **Type Safety:** Compile-time Checks
- **PostgreSQL-Optimiert:** Nutzt PostgreSQL-Features voll aus
- **Drizzle Studio:** GUI für DB-Verwaltung
- **Migrations:** Einfache Schema-Verwaltung
- **Lightweight:** Keine schwere Runtime wie Prisma

**Alternativen:**

- **Prisma:** Mehr Overhead, langsamer, aber größere Community
- **TypeORM:** Veraltet, weniger TypeScript-fokussiert
- **Kysely:** Gut, aber weniger Features

**Setup:**

```typescript
// drizzle.config.ts
export default {
  schema: './server/database/schema.ts',
  out: './server/database/migrations',
  driver: 'pg',
  dbCredentials: {
    connectionString: process.env.DATABASE_URL,
  },
}
```

**Version:** `^0.29.0`

---

## 5.1 Data Storage Design: Billing Address

### Entscheidung: Structured Columns in Users Table

**Problem:**
Should we store billing addresses in the user profile or only in orders?

**Entscheidung:**
Store billing address directly in the `users` table as structured columns (not JSONB).

**Begründung:**

**Pro Speicherung im Profil:**

- **UX-Verbesserung:** Wiederkehrende Kunden müssen Daten nicht erneut eingeben
- **Conversion-Rate:** Reduziert Checkout-Abbrüche (10-20% lt. Branchenstandard)
- **Mobile-First:** Schnellerer Checkout ist kritisch für mobile Nutzer
- **Geschäftsmodell:** Jahreskarten = wiederkehrende Kunden (jährliche Erneuerung)
- **E-Commerce Best Practice:** Amazon, PayPal, alle großen Shops speichern Adressen
- **DSGVO-konform:** Rechtsgrundlage "Vertragserfüllung" (Art. 6(1)(b))

**Strukturierte Spalten vs. JSONB:**

| Aspekt            | Structured Columns               | JSONB Column             |
| ----------------- | -------------------------------- | ------------------------ |
| **Type Safety**   | ✅ Voll typsicher                | ⚠️ Zod-Validierung nötig |
| **Queries**       | ✅ Einfach zu filtern/sortieren  | ❌ Komplexere Queries    |
| **Performance**   | ✅ Besser indexierbar            | ⚠️ Langsamer             |
| **Flexibility**   | ❌ Schema-Änderungen = Migration | ✅ Flexibel              |
| **X-API Mapping** | ✅ Direkt mappbar                | ⚠️ Parsing nötig         |

**Entscheidung:** Strukturierte Spalten

**Felder:**

```typescript
export const users = pgTable('users', {
  // ... existing fields

  // Billing address (optional - filled during checkout)
  salutation: varchar('salutation', { length: 20 }), // 'male', 'female', 'other'
  dateOfBirth: date('date_of_birth'), // Required for annual passes
  street: varchar('street', { length: 100 }),
  postCode: varchar('post_code', { length: 20 }),
  city: varchar('city', { length: 100 }),
  countryCode: varchar('country_code', { length: 10 }).default('DE'),
})
```

**UX Flow:**

1. **First Checkout:** User fills address form, checkbox "Save for future orders" is pre-checked
2. **Subsequent Checkouts:** Form pre-filled, editable before submission
3. **Profile Management:** `/profil/adresse` page to view/edit/delete address

**Migration Strategy:**

- Address fields are optional in DB schema
- Required validation only at checkout (Zod)
- Existing users: Backfill from most recent order (optional)

**Privacy:**

- Opt-out via unchecking checkbox at checkout
- Can be deleted in profile settings
- Included in GDPR data export/deletion

---

## 5.2 Queue System: BullMQ

### Entscheidung: BullMQ (MIT License)

**Problem:**
Asynchrone Verarbeitung von zeitaufwendigen Operationen (X-API Order Submission, NAV Product Sync).

**Entscheidung:**
BullMQ als Queue-System mit Redis als Backend.

**Begründung:**

**Pro BullMQ:**

- **TypeScript-First:** Native TypeScript-Integration, perfekt für Nuxt
- **Einfache Integration:** npm package, kein separater Broker-Service
- **Redis-basiert:** Nutzt Redis (für Sessions/Caching eh sinnvoll)
- **MIT License:** Kostenlos, echtes Open Source
- **Features:**
  - Retry Logic mit exponential backoff
  - Priority Queues
  - Delayed Jobs
  - Rate Limiting
  - Dead Letter Queue
  - Flow Control (Job Chains)
- **Monitoring:** BullBoard (kostenlos) für Web-Dashboard
- **Performance:** 10.000+ Jobs/Sekunde (ausreichend)
- **Community:** Große, aktive Community
- **Production-Ready:** Von vielen Unternehmen eingesetzt

**Alternativen:**

| Lösung             | Pro                      | Contra                        | Empfehlung          |
| ------------------ | ------------------------ | ----------------------------- | ------------------- |
| **BullMQ**         | TypeScript, einfach, MIT | Nur Node.js                   | ✅ Empfohlen        |
| **RabbitMQ**       | Robust, Multi-Language   | Zu komplex, separater Service | ❌ Overkill         |
| **Database Queue** | Keine neue Dependency    | Weniger Features, Polling     | ❌ Nicht skalierbar |
| **Kafka**          | Extreme Skalierung       | Viel zu komplex               | ❌ Overkill         |

**Für my.experimenta.science ist BullMQ ideal:**

- ✅ Nuxt-only (Node.js)
- ✅ Einfache Use Cases (Order → X-API, Product Sync)
- ✅ Moderater Traffic (~10-500 Orders/Tag)
- ✅ Redis eh nützlich (Sessions, Caching)

### Use Cases

#### 1. Order Submission (Async)

```typescript
// server/queues/orderQueue.ts
import { Queue } from 'bullmq'

export const orderQueue = new Queue('x-api-orders', {
  connection: { host: 'redis', port: 6379 },
})

// Add job after PayPal success
export async function queueOrderSubmission(orderId: string) {
  await orderQueue.add(
    'submit-order',
    { orderId },
    {
      attempts: 5,
      backoff: { type: 'exponential', delay: 2000 },
      removeOnComplete: 1000,
      removeOnFail: false, // Keep for manual review
    }
  )
}
```

#### 2. Product Sync (Async)

```typescript
// server/queues/productQueue.ts
export const productSyncQueue = new Queue('product-sync', {
  connection: { host: 'redis', port: 6379 },
})

// API endpoint: POST /api/erp/products
export default defineEventHandler(async (event) => {
  const product = await readBody(event)

  // Validate & queue (instant response!)
  await productSyncQueue.add('sync-product', product, {
    attempts: 3,
    backoff: { type: 'exponential', delay: 1000 },
  })

  return { status: 'accepted', queued: true }
})
```

#### 3. Worker Implementation

```typescript
// server/workers/orderWorker.ts
import { Worker } from 'bullmq'

export const orderWorker = new Worker(
  'x-api-orders',
  async (job) => {
    const { orderId } = job.data

    // 1. Fetch order from DB
    const order = await db.query.orders.findFirst({
      where: eq(orders.id, orderId),
      with: { items: true, user: true },
    })

    // 2. Transform & submit to X-API
    const payload = transformOrderToXAPI(order)
    await submitToXAPI(payload)

    // 3. Update status
    await db.update(orders).set({ status: 'completed' }).where(eq(orders.id, orderId))
  },
  {
    connection: { host: 'redis', port: 6379 },
    concurrency: 5,
    limiter: { max: 10, duration: 1000 },
  }
)
```

### Monitoring: BullBoard

```typescript
// server/api/admin/queues.ts
import { createBullBoard } from '@bull-board/api'
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'
import { NuxtAdapter } from '@bull-board/nuxt'

const serverAdapter = new NuxtAdapter()

createBullBoard({
  queues: [new BullMQAdapter(orderQueue), new BullMQAdapter(productSyncQueue)],
  serverAdapter,
})

// Access: http://localhost:3000/admin/queues
```

**Dashboard Features (kostenlos):**

- Real-time Job-Status
- Manual Retry
- Job Details & Logs
- Queue-Metriken
- Fehler-Analyse

### BullMQ Dependencies

```json
{
  "dependencies": {
    "bullmq": "^5.0.0",
    "@bull-board/api": "^5.0.0",
    "@bull-board/nuxt": "^5.0.0"
  }
}
```

**Alle MIT License - kostenlos!**

### BullMQ Kosten

**BullMQ:** €0 (MIT License)
**BullMQ Pro:** €99-299/Monat (nicht nötig!)
**Taskforce.sh:** €49-199/Monat (BullBoard reicht!)

**Für dich:** €0 - alles kostenlos!

---

## 5.3 In-Memory Store: Redis

### Entscheidung: Redis 7

**Verwendung:**

1. **BullMQ Queue Storage** (Hauptzweck)
2. **Session Storage** (HTTP-only cookies)
3. **Caching** (Produkt-Daten, etc.)
4. **Rate Limiting** (Brute-Force-Schutz)

**Begründung:**

- ✅ Persistenz mit AOF + RDB (keine Datenverluste)
- ✅ Schnell (In-Memory)
- ✅ Bewährt (Millionen Installationen)
- ✅ Docker-ready (redis:7-alpine)

**Lizenz-Situation:**

**Redis License (seit März 2024):**

- RSALv2 (Redis Source Available License)
- SSPL (Server Side Public License)

**Dein Use Case:**

✅ **Vollständig legal und kostenlos!**

**Was du machst:**

- Redis selbst hosten (in docker-compose)
- Für interne App-Funktionen nutzen
- Nicht als Hosting-Service verkaufen

→ **Keine Lizenzkosten, keine Probleme!**

**Was verboten wäre:**

- ❌ "experimenta Redis Cloud" Service anbieten
- ❌ Managed Redis Hosting verkaufen

#### Alternative: Valkey (100% Open Source)

Falls Lizenz-Unsicherheit:

- **Valkey:** Redis-Fork (Linux Foundation)
- **Lizenz:** BSD-3-Clause (echtes Open Source)
- **Kompatibilität:** 100% Drop-in Replacement
- **Docker:** `valkey/valkey:7.2-alpine`

**Empfehlung:** Start mit Redis, später optional auf Valkey migrieren.

### Persistenz-Konfiguration

```yaml
# docker-compose.yml
services:
  redis:
    image: redis:7-alpine
    command: >
      redis-server
      --appendonly yes
      --appendfsync everysec
      --save 60 1000
      --save 300 100
      --save 900 1
    volumes:
      - redis_data:/data
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 10s

volumes:
  redis_data:
```

**Garantie:** Max. 1 Sekunde Datenverlust bei Server-Crash.

**Erklärung:**

- `--appendonly yes`: AOF aktiviert
- `--appendfsync everysec`: Jede Sekunde auf Disk
- `--save`: RDB Snapshots als Backup

### Redis Dependencies

```json
{
  "dependencies": {
    "ioredis": "^5.3.0" // Redis client für Node.js
  }
}
```

### Redis Kosten

**Redis/Valkey:** €0 (Open Source)
**Einzige Kosten:** Server-RAM (~512MB für Redis)

---

## 6. Authentifizierung

### Cidaas (OIDC/OAuth2)

**Entscheidung:** Cidaas Integration mit Custom UI

**Begründung:**

- **Unternehmensvorgabe:** Bereits im Einsatz bei experimenta
- **OIDC/OAuth2:** Standard-Protokolle
- **Compliance:** DSGVO-konform (EU-basiert, Widas)
- **Features:** MFA, Social Login (optional)

**Architektur:**

- **Cidaas:** Nur für Authentifizierung (User Identity)
- **Lokale DB:** User-Profile & Rollen
- **Custom UI:** Eigene Login/Registrierungs-Masken im experimenta Design

**Integration:**

```typescript
// Nuxt OAuth Module oder Custom Implementation
import { createOAuth2Client } from '@nuxt/oauth'

export default defineNuxtConfig({
  oauth: {
    cidaas: {
      clientId: process.env.CIDAAS_CLIENT_ID,
      clientSecret: process.env.CIDAAS_CLIENT_SECRET,
      authorizeUrl: 'https://experimenta.cidaas.de/authorize',
      tokenUrl: 'https://experimenta.cidaas.de/token',
      userInfoUrl: 'https://experimenta.cidaas.de/userinfo',
    },
  },
})
```

**Libraries:**

- `nuxt-auth-utils` oder Custom OAuth2 Implementation
- `jose` für JWT-Validierung

---

## 7. Payment Gateway

### PayPal (MVP)

**Entscheidung:** PayPal Checkout Integration

**Begründung:**

- **Weit verbreitet:** Hohe Akzeptanz in Deutschland
- **Einfache Integration:** Offizielle SDK & Dokumentation
- **Buyer Protection:** Käuferschutz erhöht Vertrauen
- **Schnell:** Weniger Daten als Kreditkarte erforderlich
- **Mobile-optimiert:** PayPal App-Integration

**SDK:**

```typescript
// PayPal JavaScript SDK
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID&currency=EUR"></script>
```

**Server-Side:**

- PayPal REST API für Order Creation & Capture
- Webhook für Payment-Benachrichtigungen

**Post-MVP:**

- Stripe (Kreditkarte, Apple Pay, Google Pay)
- SEPA-Lastschrift
- Klarna / Ratenzahlung

---

## 8. External API Integration: X-API

### X-API Client (Order Submission)

**Entscheidung:** Native `fetch` with retry logic

**Purpose:** Submit completed orders to NAV ERP via X-API `/shopware/order` endpoint

**Begründung:**

- **Native:** No additional dependencies (fetch is built-in)
- **TypeScript:** Full type safety with Zod validation
- **Retry Logic:** Custom implementation with exponential backoff
- **Error Handling:** Comprehensive error tracking and logging

**Environments:**

| Environment | URL                                       |
| ----------- | ----------------------------------------- |
| Development | `https://x-api-dev.experimenta.science`   |
| Staging     | `https://x-api-stage.experimenta.science` |
| Production  | `https://x-api.experimenta.science`       |

### X-API Authentication

**Entscheidung:** HTTP Basic Authentication

**Begründung:**

- **Standard:** HTTP Basic Auth ist ein bewährter Standard (RFC 7617)
- **Einfach:** Keine komplexe OAuth2-Integration nötig
- **Secure:** HTTPS-verschlüsselte Übertragung der Credentials
- **Kompatibel:** Unterstützt von allen HTTP-Clients

**Implementierung:**

```typescript
// server/utils/xapi.ts
const config = useRuntimeConfig()

// Prepare Basic Auth header
const authString = Buffer.from(`${config.xApiUsername}:${config.xApiPassword}`).toString('base64')

const response = await fetch(`${config.xApiBaseUrl}/shopware/order`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Basic ${authString}`,
  },
  body: JSON.stringify(payload),
})
```

**Sicherheitshinweise:**

- ✅ Credentials **nur** über HTTPS übertragen
- ✅ Credentials **niemals** im Code hardcoden
- ✅ Separate Service-Accounts pro Environment (dev/stage/prod)
- ✅ Credentials regelmäßig rotieren (alle 90 Tage)
- ✅ In Production: Docker Secrets verwenden statt Environment Variables

**Environment Variables:**

```bash
# .env
X_API_BASE_URL=https://x-api-dev.experimenta.science
X_API_USERNAME=shop_user_dev
X_API_PASSWORD=xxx
```

**Docker Secrets (Production):**

```yaml
# docker-compose.yml
services:
  app:
    secrets:
      - xapi_username
      - xapi_password
    environment:
      X_API_USERNAME_FILE: /run/secrets/xapi_username
      X_API_PASSWORD_FILE: /run/secrets/xapi_password

secrets:
  xapi_username:
    file: ./secrets/xapi_username.txt
  xapi_password:
    file: ./secrets/xapi_password.txt
```

**Implementation:**

```typescript
// server/utils/xapi.ts
import { z } from 'zod'

// Zod schema for X-API request validation
const XAPIOrderSchema = z.object({
  shopPOSOrder: z.object({
    documentType: z.enum(['Order', 'Credit Memo']),
    externalDocumentNo: z.string().max(35),
    externalPOSReceiptNo: z.string().max(250),
    salesChannel: z.enum(['Shop', 'POS']),
    shoppingCartCompletion: z.string().datetime(),
    visitType: z.enum(['Private', 'Institution']),
    invoiceDiscountValue: z.number().int(),
    invoiceDiscPct: z.number().int(),
    amountIncludingVAT: z.number().int(), // Cents!
    language: z.enum(['DEU', 'ENU']),
    salesLine: z.array(
      z.object({
        type: z.enum(['', 'Item']),
        lineNo: z.string().max(30),
        no: z.string().max(20),
        variantCode: z.string().max(10),
        description: z.string().max(50),
        description2: z.string().max(50),
        quantity: z.number().int(),
        unitPrice: z.number().int(), // Cents!
        vatPct: z.number(),
        lineAmountIncludingVAT: z.number(),
        lineDiscountPct: z.number(),
        lineDiscountAmount: z.number().int(),
        seasonTicketCode: z.string().max(20),
        couponCode: z.string().max(20),
        visitorCategory: z.enum(['00', '10', '40', '50', '60', '99', '120', '128']),
        ticketPriceType: z.string().max(50),
        ticketCode: z.string().max(250),
        annualPass: z
          .object({
            salutationCode: z.enum(['K_ANGABE', 'FRAU', 'HERR']),
            firstName: z.string().max(30),
            lastName: z.string().max(30),
            streetAndHouseNumber: z.string().max(50),
            postCode: z.string().max(20),
            city: z.string().max(30),
            countryCode: z.string().max(10),
            dateOfBirth: z.string(),
            eMail: z.string().email(),
            validFrom: z.string(),
          })
          .optional(),
      })
    ),
    payment: z.array(
      z.object({
        paymentEntryNo: z.number().int(),
        amount: z.number().int(), // Cents!
        paymentType: z.enum(['Shop PayPal', 'Shop Credit Card', 'Shop Direct Debit']),
        createdOn: z.string().datetime(),
        reference: z.string().max(50),
        paymentId: z.string().max(50),
      })
    ),
    personContact: z.object({
      experimentaAccountID: z.string().uuid(),
      eMail: z.string().email(),
      salutationCode: z.enum(['K_ANGABE', 'FRAU', 'HERR']),
      jobTitle: z.string().max(30),
      firstName: z.string().max(30),
      lastName: z.string().max(30),
      streetAndHouseNumber: z.string().max(50),
      postCode: z.string().max(20),
      city: z.string().max(30),
      countryCode: z.string().max(10),
      phoneNumber: z.string().max(30),
      guest: z.boolean(),
    }),
  }),
})

export type XAPIOrderPayload = z.infer<typeof XAPIOrderSchema>

// Submit order to X-API with retry logic
export async function submitOrderToXAPI(payload: XAPIOrderPayload) {
  // Validate payload
  const validatedPayload = XAPIOrderSchema.parse(payload)

  const config = useRuntimeConfig()
  const baseURL = config.xApiBaseUrl // From environment

  // Retry logic with exponential backoff
  const maxRetries = 3
  const retryDelays = [1000, 3000, 9000] // 1s, 3s, 9s

  // Prepare Basic Auth header
  const authString = Buffer.from(`${config.xApiUsername}:${config.xApiPassword}`).toString('base64')

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(`${baseURL}/shopware/order`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Basic ${authString}`,
        },
        body: JSON.stringify(validatedPayload),
      })

      if (!response.ok) {
        throw new Error(`X-API returned ${response.status}: ${await response.text()}`)
      }

      const result = await response.json()

      // Log success
      console.info(`Order submitted to X-API successfully`, {
        orderNo: payload.shopPOSOrder.externalDocumentNo,
        attempt: attempt + 1,
      })

      return result
    } catch (error) {
      // Log attempt
      console.error(`X-API submission failed (attempt ${attempt + 1}/${maxRetries})`, {
        orderNo: payload.shopPOSOrder.externalDocumentNo,
        error,
      })

      // If this was the last attempt, throw
      if (attempt === maxRetries - 1) {
        throw error
      }

      // Wait before retry
      await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt]))
    }
  }
}

// Helper: Transform order from DB to X-API format
export function transformOrderToXAPI(order: Order, user: User): XAPIOrderPayload {
  return {
    shopPOSOrder: {
      documentType: 'Order',
      externalDocumentNo: order.orderNumber,
      externalPOSReceiptNo: '',
      salesChannel: 'Shop',
      shoppingCartCompletion: order.createdAt.toISOString(),
      visitType: 'Private',
      invoiceDiscountValue: 0,
      invoiceDiscPct: 0,
      amountIncludingVAT: Math.round(order.totalAmount * 100), // EUR → Cents
      language: 'DEU',
      salesLine: order.items.map((item, index) => ({
        type: 'Item',
        lineNo: String((index + 1) * 10000),
        no: item.product.navProductId,
        variantCode: item.product.variantCode || '',
        description: item.product.name.substring(0, 50),
        description2: item.product.description?.substring(0, 50) || '',
        quantity: item.quantity,
        unitPrice: Math.round(item.priceSnapshot * 100), // EUR → Cents
        vatPct: 7,
        lineAmountIncludingVAT: Math.round(item.quantity * item.priceSnapshot * 100),
        lineDiscountPct: 0,
        lineDiscountAmount: 0,
        seasonTicketCode: '',
        couponCode: '',
        visitorCategory: '120',
        ticketPriceType: 'Jahreskarte Makerspace',
        ticketCode: generateTicketCode(),
        annualPass: {
          salutationCode: mapSalutation(user.salutation),
          firstName: user.firstName,
          lastName: user.lastName,
          streetAndHouseNumber: user.address.street,
          postCode: user.address.postCode,
          city: user.address.city,
          countryCode: user.address.countryCode || 'DE',
          dateOfBirth: user.dateOfBirth,
          eMail: user.email,
          validFrom: new Date().toISOString().split('T')[0], // YYYY-MM-DD
        },
      })),
      payment: [
        {
          paymentEntryNo: 10000,
          amount: Math.round(order.totalAmount * 100), // EUR → Cents
          paymentType: 'Shop PayPal',
          createdOn: order.paymentCompletedAt.toISOString(),
          reference: order.paymentId,
          paymentId: order.paymentId,
        },
      ],
      personContact: {
        experimentaAccountID: user.experimentaId,
        eMail: user.email,
        salutationCode: mapSalutation(user.salutation),
        jobTitle: user.jobTitle || '',
        firstName: user.firstName,
        lastName: user.lastName,
        streetAndHouseNumber: order.billingAddress.street,
        postCode: order.billingAddress.postCode,
        city: order.billingAddress.city,
        countryCode: order.billingAddress.countryCode || 'DE',
        phoneNumber: user.phone || '',
        guest: false,
      },
    },
  }
}
```

**Environment Variables:**

```bash
# .env (Development/Staging)
X_API_BASE_URL=https://x-api-dev.experimenta.science
X_API_USERNAME=shop_user_dev
X_API_PASSWORD=xxx

# .env.stage
# X_API_BASE_URL=https://x-api-stage.experimenta.science
# X_API_USERNAME=shop_user_stage
# X_API_PASSWORD=xxx

# Production: Use Docker Secrets (see X-API Authentication section above)
```

**Alternative:** Use `ofetch` (Nuxt's built-in fetch wrapper) for better error handling:

```typescript
import { ofetch } from 'ofetch'

const xapi = ofetch.create({
  baseURL: config.xApiBaseUrl,
  retry: 3,
  retryDelay: 1000,
})
```

---

## 9. Backend & APIs

### Nuxt Server APIs

**Entscheidung:** Nuxt Server Routes (Nitro)

**Begründung:**

- **Monorepo:** Backend & Frontend in einem Projekt
- **Serverless-Ready:** Kann als Serverless Functions deployed werden
- **Auto-Import:** Shared Utils zwischen Client & Server
- **TypeScript:** Type-Safety über Frontend/Backend-Grenze
- **Performance:** Nitro Engine (schneller als Express)
- **Flexible Deployment:** Node.js, Deno, Workers, Docker

**API Struktur:**

```
server/
  api/
    auth/
      login.post.ts
      callback.get.ts
    products/
      index.get.ts
      [id].get.ts
    cart/
      items.post.ts
      items/[id].patch.ts
    orders/
      index.post.ts
      [id].get.ts
    payment/
      paypal/
        create.post.ts
        capture.post.ts
        webhook.post.ts
    erp/
      products.post.ts
```

---

## 9. Internationalization (i18n)

### @nuxtjs/i18n

**Entscheidung:** @nuxtjs/i18n (Official Nuxt module)

**Begründung:**

- **Offizielles Nuxt-Modul:** Beste Integration mit Nuxt 4
- **Vue I18n powered:** Industry-Standard für Vue.js
- **SEO-optimiert:** Automatische locale routes, hreflang tags
- **SSR/SSG Support:** Perfekt für Nuxt's rendering modes
- **Lazy Loading:** Sprachdateien on-demand laden
- **TypeScript Support:** Vollständig typisiert
- **Auto-Import:** Composables wie `useI18n()`, `$t()` direkt verfügbar

**Supported Languages:**

- 🇩🇪 **Deutsch** (Primary, default locale)
- 🇬🇧 **English** (Secondary, für internationale Besucher)

**Installation:**

```bash
npx nuxi@latest module add i18n
```

**Configuration:**

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/i18n'],

  i18n: {
    locales: [
      {
        code: 'de',
        iso: 'de-DE',
        name: 'Deutsch',
        file: 'de-DE.json',
      },
      {
        code: 'en',
        iso: 'en-US',
        name: 'English',
        file: 'en-US.json',
      },
    ],
    defaultLocale: 'de',
    strategy: 'prefix_except_default', // URLs: /produkte (de), /en/products (en)
    langDir: 'locales',
    lazy: true,
    detectBrowserLanguage: {
      useCookie: true,
      cookieKey: 'i18n_redirected',
      redirectOn: 'root',
    },
  },
})
```

**Project Structure:**

```
/locales
  ├── de-DE.json    # German translations
  └── en-US.json    # English translations
```

**Translation Files Example:**

```json
// locales/de-DE.json
{
  "nav": {
    "home": "Startseite",
    "products": "Produkte",
    "cart": "Warenkorb"
  },
  "product": {
    "addToCart": "In den Warenkorb",
    "price": "Preis",
    "description": "Beschreibung"
  },
  "checkout": {
    "title": "Zur Kasse",
    "total": "Gesamt",
    "payNow": "Jetzt bezahlen"
  }
}

// locales/en-US.json
{
  "nav": {
    "home": "Home",
    "products": "Products",
    "cart": "Cart"
  },
  "product": {
    "addToCart": "Add to Cart",
    "price": "Price",
    "description": "Description"
  },
  "checkout": {
    "title": "Checkout",
    "total": "Total",
    "payNow": "Pay Now"
  }
}
```

**Usage in Components:**

```vue
<template>
  <div>
    <h1>{{ $t('nav.home') }}</h1>
    <p>{{ $t('product.description') }}</p>

    <!-- With parameters -->
    <p>{{ $t('product.price', { amount: formatPrice(99.0) }) }}</p>

    <!-- Language switcher -->
    <USelectMenu v-model="locale" :options="availableLocales">
      <template #label>
        {{ locale === 'de' ? '🇩🇪 Deutsch' : '🇬🇧 English' }}
      </template>
    </USelectMenu>
  </div>
</template>

<script setup lang="ts">
const { locale, t, availableLocales } = useI18n()

// Format price according to locale
const formatPrice = (price: number) => {
  return new Intl.NumberFormat(locale.value, {
    style: 'currency',
    currency: 'EUR',
  }).format(price)
}

// Format date according to locale
const formatDate = (date: Date) => {
  return new Intl.DateTimeFormat(locale.value, {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  }).format(date)
}
</script>
```

**SEO Benefits:**

- Automatische Routes: `/produkte` (de), `/en/products` (en)
- Automatische `<link rel="alternate" hreflang="de-DE" />` Tags
- Locale-specific meta tags
- Sitemap mit allen Sprachen

**X-API Integration:**

```typescript
// server/utils/xapi.ts
import { useI18n } from '#i18n'

export function transformOrderToXAPI(order: Order, user: User) {
  const { locale } = useI18n()

  return {
    shopPOSOrder: {
      // Map locale to X-API language format
      language: locale.value === 'de' ? 'DEU' : 'ENU',
      // ... rest of payload
    },
  }
}
```

**Version:** `^8.0.0`

**Official Docs:** <https://i18n.nuxtjs.org/>

---

## 10. Validation & Forms

### Zod + VeeValidate

**Entscheidung:**

- **Zod:** Schema Validation
- **VeeValidate:** Form Handling

**Begründung:**

- **Type Safety:** Zod generiert TypeScript-Types
- **Reusable Schemas:** Validierung auf Client & Server
- **Error Handling:** Strukturierte Error-Messages
- **VeeValidate:** Vue-native Form Library
- **Composition API Support:** Moderne Vue 3 Syntax

**Example:**

```typescript
import { z } from 'zod'

export const checkoutSchema = z.object({
  firstName: z.string().min(2),
  lastName: z.string().min(2),
  email: z.string().email(),
  address: z.string().min(5),
})

export type CheckoutForm = z.infer<typeof checkoutSchema>
```

**Dependencies:**

- `zod` - Schema validation
- `vee-validate` - Form handling
- `@vee-validate/zod` - Integration

---

## 10. State Management

### Pinia (Optional)

**Entscheidung:** Pinia für globale State (sparsam einsetzen)

**Begründung:**

- **Vue 3 Native:** Offizielle State Library für Vue
- **TypeScript:** Hervorragende Type-Inference
- **Devtools:** Vue Devtools Integration
- **Composition API:** Moderne Syntax
- **SSR-Ready:** Nuxt Auto-Import

**Verwendung:**

- User Store (Auth-Status)
- Cart Store (Warenkorb)
- Minimal halten, meiste State in Composables

**Alternative:**

- Nuxt Composables (`useState`) für einfache Cases

---

## 11. Deployment & Infrastructure

### Docker

**Entscheidung:** Docker Containerisierung

**Begründung:**

- **Konsistenz:** Gleiche Umgebung überall (Dev, Staging, Prod)
- **Portabilität:** Läuft auf Hetzner, AWS, überall
- **Isolation:** Keine Konflikte mit anderen Services
- **Skalierung:** Einfach mehrere Container starten

**Dockerfile (Multi-Stage Build):**

```dockerfile
# Build Stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production Stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.output ./
EXPOSE 3000
CMD ["node", "server/index.mjs"]
```

**Docker Compose (für Dev):**

```yaml
services:
  app:
    build: .
    ports:
      - '3000:3000'
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/experimenta
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=experimenta
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass

volumes:
  postgres_data:
```

---

### Hetzner Cloud

**Hosting:**

- Hetzner Dedicated Server oder VPS
- Proxmox für Virtualisierung
- Docker Container für App & DB
- Nginx/Traefik als Reverse Proxy
- Let's Encrypt für SSL

**Empfohlene Server-Specs (Production):**

- CPU: 4 Cores
- RAM: 8 GB
- Storage: 160 GB SSD
- Backup: Tägliche Snapshots

---

### CI/CD mit GitLab

**Pipeline Stages:**

1. **Build:** Docker Image bauen
2. **Test:** Unit & E2E Tests laufen
3. **Deploy Staging:** Automatisch auf Staging deployen
4. **Deploy Production:** Manuell getriggert (approval)

**Deployment-Optionen:**

**Option A: GitLab Runner auf Server**

- Runner auf Hetzner Server installiert
- Direkter Zugriff zu Docker
- Schnelleres Deployment

**Option B: SSH-Zugriff**

- Runner im GitLab-Server
- Deployment via SSH + rsync/docker commands
- Mehr Isolation

**Empfehlung:** Runner auf Server (Option A)

**.gitlab-ci.yml:**

```yaml
stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - docker build -t experimenta-app:$CI_COMMIT_SHA .
  only:
    - main

test:
  stage: test
  script:
    - npm ci
    - npm run test

deploy_production:
  stage: deploy
  script:
    - docker pull experimenta-app:$CI_COMMIT_SHA
    - docker stop experimenta-app || true
    - docker rm experimenta-app || true
    - docker run -d --name experimenta-app -p 3000:3000 experimenta-app:$CI_COMMIT_SHA
  only:
    - main
  when: manual
```

---

## 12. Testing

### Vitest

**Entscheidung:** Vitest für Unit & Integration Tests

**Begründung:**

- **Vite-Native:** Gleiche Config wie Nuxt
- **Fast:** Parallelisierung, Watch Mode
- **Vue Support:** @vue/test-utils Integration
- **TypeScript:** Native TS-Support
- **API kompatibel mit Jest:** Einfache Migration

**Config:**

```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      threshold: {
        lines: 80,
        functions: 80,
        branches: 80,
        statements: 80,
      },
    },
  },
})
```

---

### Playwright

**Entscheidung:** Playwright für E2E Tests

**Begründung:**

- **Modern:** Neueste Browser-Automation
- **Multi-Browser:** Chrome, Firefox, Safari, Edge
- **Mobile Emulation:** Responsive Testing
- **Fast:** Parallelisierung
- **Debug-Tools:** Hervorragende Dev Experience

**Tests:**

```typescript
// e2e/checkout.spec.ts
import { test, expect } from '@playwright/test'

test('complete checkout flow', async ({ page }) => {
  await page.goto('/')
  await page.click('text=Makerspace Jahreskarte')
  await page.click('text=In den Warenkorb')
  await page.click('[aria-label="Warenkorb"]')
  await page.click('text=Zur Kasse')
  // ... weitere Schritte
})
```

---

## 13. Monitoring & Logging

### Sentry (Optional)

**Entscheidung:** Sentry für Error Tracking

**Begründung:**

- **Real-time Errors:** Sofortige Benachrichtigung
- **Stack Traces:** Detaillierte Error-Informationen
- **User Context:** Welcher User ist betroffen?
- **Release Tracking:** Errors pro Version
- **Performance Monitoring:** Transaction Tracing

**Integration:**

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/sentry'],
  sentry: {
    dsn: process.env.SENTRY_DSN,
    environment: process.env.NODE_ENV,
  },
})
```

**Alternative:** Eigenes Logging-System mit Loki/Grafana

---

## 14. Email-Versand

### Optionen

**Option A: Eigener SMTP-Server**

- Pro: Volle Kontrolle, keine Kosten
- Contra: Wartung, Spam-Probleme, Deliverability

**Option B: Transactional Email Service**

- **SendGrid:** Zuverlässig, gute API
- **Postmark:** Spezialisiert auf Transactional
- **Mailgun:** Flexibel, gute Dokumentation
- **AWS SES:** Günstig, aber mehr Setup

**Empfehlung:** Postmark oder SendGrid

- Bessere Deliverability
- Weniger Wartung
- Templates & Analytics
- Webhooks für Bounce/Spam

**Nuxt Mailer:**

```typescript
// server/utils/mail.ts
import nodemailer from 'nodemailer'

export const sendOrderConfirmation = async (email: string, order: Order) => {
  const transporter = nodemailer.createTransporter({
    host: process.env.SMTP_HOST,
    port: 587,
    auth: {
      user: process.env.SMTP_USER,
      pass: process.env.SMTP_PASS,
    },
  })

  await transporter.sendMail({
    from: 'noreply@experimenta.science',
    to: email,
    subject: `Bestellbestätigung ${order.orderNumber}`,
    html: renderOrderEmail(order),
  })
}
```

---

## 15. Development Tools

### ESLint + Prettier

**Entscheidung:** ESLint + Prettier für Code Quality

**Config:**

```json
{
  "extends": ["@nuxt/eslint-config", "prettier"],
  "rules": {
    "vue/multi-word-component-names": "off"
  }
}
```

---

### TypeScript

**Entscheidung:** Strict TypeScript

**tsconfig.json:**

```json
{
  "extends": "./.nuxt/tsconfig.json",
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true
  }
}
```

---

## 16. Package Manager

### pnpm

**Entscheidung:** pnpm statt npm/yarn

**Begründung:**

- **Schneller:** Symlinks statt Kopien
- **Disk Space:** Shared Dependencies
- **Strict:** Verhindert Ghost Dependencies
- **Monorepo-Ready:** Workspaces Support

---

## 17. Vollständige Dependency-Liste

### Dependencies (package.json)

```json
{
  "dependencies": {
    "nuxt": "^4.0.0",
    "vue": "^3.4.0",
    "@nuxtjs/tailwindcss": "^6.11.0",
    "radix-vue": "^1.9.0",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.0",
    "tailwind-merge": "^2.2.0",
    "drizzle-orm": "^0.29.0",
    "postgres": "^3.4.0",
    "zod": "^3.22.0",
    "vee-validate": "^4.12.0",
    "@vee-validate/zod": "^4.12.0",
    "pinia": "^2.1.0",
    "@pinia/nuxt": "^0.5.0",
    "jose": "^5.2.0",
    "@paypal/checkout-server-sdk": "^1.0.3",
    "nodemailer": "^6.9.0"
  },
  "devDependencies": {
    "@nuxt/eslint-config": "^0.2.0",
    "@nuxtjs/sentry": "^8.0.0",
    "drizzle-kit": "^0.20.0",
    "eslint": "^8.56.0",
    "prettier": "^3.2.0",
    "typescript": "^5.3.0",
    "vitest": "^1.2.0",
    "@vue/test-utils": "^2.4.0",
    "@playwright/test": "^1.41.0",
    "@nuxt/test-utils": "^3.11.0"
  }
}
```

---

## 18. Zusammenfassung

| Kategorie          | Technologie       | Version |
| ------------------ | ----------------- | ------- |
| **Framework**      | Nuxt              | 4.x     |
| **UI Library**     | shadcn-vue        | latest  |
| **Styling**        | Tailwind CSS      | 4.x     |
| **Datenbank**      | PostgreSQL        | 16+     |
| **ORM**            | Drizzle           | 0.29+   |
| **Auth**           | Cidaas            | -       |
| **Payment**        | PayPal            | -       |
| **Validation**     | Zod               | 3.x     |
| **Forms**          | VeeValidate       | 4.x     |
| **State**          | Pinia             | 2.x     |
| **Testing (Unit)** | Vitest            | 1.x     |
| **Testing (E2E)**  | Playwright        | 1.x     |
| **Deployment**     | Docker            | latest  |
| **CI/CD**          | GitLab CI         | -       |
| **Hosting**        | Hetzner           | -       |
| **Error Tracking** | Sentry (optional) | 8.x     |
| **Email**          | Postmark/SendGrid | -       |

---

**Ende des Dokuments**
