You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

98 KiB

System-Architektur

my.experimenta.science

Version: 1.0 Datum: 28. Oktober 2025


1. Architektur-Übersicht

Die my.experimenta.science App folgt einer modernen Full-Stack-Architektur mit:

  • Nuxt 4 als Full-Stack-Framework (Frontend + Backend)
  • PostgreSQL als primäre Datenbank
  • Cidaas für Authentifizierung
  • Externe Integrationen (NAV ERP, X-API, PayPal)
  • Docker für Containerisierung

2. High-Level Architektur

┌─────────────────────────────────────────────────────────────────┐
│                         Client Layer                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │ Browser  │  │  Mobile  │  │  Tablet  │  │ Desktop  │       │
│  │  (PWA)   │  │  Safari  │  │  iPad    │  │  Chrome  │       │
│  └─────┬────┘  └─────┬────┘  └─────┬────┘  └─────┬────┘       │
└────────┼─────────────┼─────────────┼─────────────┼─────────────┘
         │             │             │             │
         └─────────────┴─────────────┴─────────────┘
                          │ HTTPS
         ┌────────────────▼────────────────┐
         │    Reverse Proxy (Nginx)        │
         │  - SSL Termination              │
         │  - Load Balancing               │
         │  - Rate Limiting                │
         └────────────────┬────────────────┘
                          │
         ┌────────────────▼────────────────────────────────────┐
         │              Nuxt 4 Application                      │
         │  ┌──────────────────┐  ┌──────────────────┐        │
         │  │   Frontend       │  │   Server API     │        │
         │  │  - Vue 3         │  │  - Nitro Engine  │        │
         │  │  - SSR/SSG       │  │  - API Routes    │        │
         │  │  - shadcn-nuxt   │  │  - Business      │        │
         │  │  - Pinia Store   │  │    Logic         │        │
         │  └──────────────────┘  └─────────┬────────┘        │
         └────────────────────────────────────┼─────────────────┘
                                              │
                    ┌─────────────────────────┼─────────────────────────┐
                    │                         │                         │
         ┌──────────▼──────────┐  ┌──────────▼──────────┐  ┌──────────▼──────────┐
         │   Drizzle ORM       │  │   BullMQ Queues     │  │   Redis             │
         │                     │  │  - Orders           │  │  - Queue Storage    │
         │                     │  │  - Product Sync     │  │  - Sessions         │
         │                     │  │  - Emails           │  │  - Caching          │
         └──────────┬──────────┘  └──────────┬──────────┘  └─────────────────────┘
                    │                         │
         ┌──────────▼──────────┐  ┌──────────▼──────────┐
         │  PostgreSQL         │  │  BullMQ Workers     │
         │  - Users            │  │  - Order Worker     │
         │  - Products         │  │  - Product Worker   │
         │  - Cart / Items     │  │  - Email Worker     │
         │  - Orders / Items   │  └─────────────────────┘
         └─────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    External Services                             │
├─────────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐            │
│  │   Cidaas    │  │   NAV ERP   │  │    X-API    │            │
│  │  (Auth)     │  │  (Products) │  │  (Content)  │            │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘            │
│         │                │                 │                    │
│  ┌──────▼──────┐  ┌──────▼──────┐  ┌──────▼──────┐            │
│  │   PayPal    │  │   Email     │  │   Sentry    │            │
│  │  (Payment)  │  │  (SMTP)     │  │  (Errors)   │            │
│  └─────────────┘  └─────────────┘  └─────────────┘            │
└─────────────────────────────────────────────────────────────────┘

3. Datenfluss-Diagramme

3.1 Benutzer-Registrierung & Login

┌──────────┐                ┌──────────┐                ┌──────────┐
│          │                │          │                │          │
│  Client  │                │   Nuxt   │                │  Cidaas  │
│          │                │          │                │          │
└────┬─────┘                └────┬─────┘                └────┬─────┘
     │                           │                           │
     │ 1. Click "Login"          │                           │
     ├──────────────────────────>│                           │
     │                           │                           │
     │                           │ 2. Redirect to Cidaas     │
     │                           │  with Client ID           │
     │                           ├──────────────────────────>│
     │                           │                           │
     │                           │                           │ 3. User authenticates
     │                           │                           │    (email/password)
     │                           │                           │
     │                           │ 4. Auth Code              │
     │<──────────────────────────┴───────────────────────────┤
     │                           │                           │
     │ 5. Send Auth Code         │                           │
     ├──────────────────────────>│                           │
     │                           │                           │
     │                           │ 6. Exchange Code for Token│
     │                           ├──────────────────────────>│
     │                           │                           │
     │                           │ 7. Access Token + User Info│
     │                           │<──────────────────────────┤
     │                           │                           │
     │                           │ 8. Create/Update User in DB
     │                           │    (PostgreSQL)           │
     │                           │                           │
     │ 9. Set Session Cookie     │                           │
     │<──────────────────────────┤                           │
     │                           │                           │
     │ 10. Redirect to Dashboard │                           │
     │<──────────────────────────┤                           │
     │                           │                           │

Schritte:

  1. User klickt auf "Login"
  2. Nuxt leitet zu Cidaas weiter (OAuth2 Authorization)
  3. User authentifiziert sich bei Cidaas
  4. Cidaas redirected zurück mit Authorization Code
  5. Client sendet Code an Nuxt Server
  6. Nuxt tauscht Code gegen Access Token
  7. Nuxt erhält User-Info von Cidaas
  8. Nuxt erstellt/aktualisiert User-Profil in lokaler DB
  9. Nuxt setzt Session-Cookie
  10. User wird zur Homepage weitergeleitet

3.2 Produkt-Synchronisation (NAV ERP → App)

┌──────────┐                ┌──────────┐                ┌──────────┐
│          │                │          │                │          │
│ NAV ERP  │                │   Nuxt   │                │PostgreSQL│
│          │                │          │                │          │
└────┬─────┘                └────┬─────┘                └────┬─────┘
     │                           │                           │
     │ 1. Product Update Event   │                           │
     │    (e.g., new JK created) │                           │
     │                           │                           │
     │ 2. HTTP POST              │                           │
     │    /api/erp/products      │                           │
     ├──────────────────────────>│                           │
     │    Body:                  │                           │
     │    {                      │                           │
     │      nav_product_id,      │                           │
     │      name, price,         │                           │
     │      stock, ...           │                           │
     │    }                      │                           │
     │                           │                           │
     │                           │ 3. Validate API Key       │
     │                           │                           │
     │                           │ 4. Validate Payload       │
     │                           │    (Zod Schema)           │
     │                           │                           │
     │                           │ 5. Check if Product exists│
     │                           ├──────────────────────────>│
     │                           │                           │
     │                           │ 6. Product exists? (Y/N)  │
     │                           │<──────────────────────────┤
     │                           │                           │
     │                           │ 7. INSERT or UPDATE       │
     │                           ├──────────────────────────>│
     │                           │                           │
     │                           │ 8. Success                │
     │                           │<──────────────────────────┤
     │                           │                           │
     │ 9. HTTP 200 OK            │                           │
     │<──────────────────────────┤                           │
     │    {                      │                           │
     │      success: true,       │                           │
     │      product_id: "..."    │                           │
     │    }                      │                           │
     │                           │                           │

Schritte:

  1. NAV ERP erkennt Produktänderung
  2. NAV sendet POST-Request an /api/erp/products
  3. Nuxt validiert API-Key
  4. Nuxt validiert Payload (Zod)
  5. Nuxt fügt Job zu BullMQ Queue hinzu (async!)
  6. Nuxt antwortet sofort mit 202 Accepted (NAV wartet nicht!)
  7. [Background] Worker holt Job aus Queue
  8. [Background] Worker prüft ob Produkt bereits existiert
  9. [Background] Worker führt INSERT oder UPDATE aus (Drizzle)
  10. [Background] DB bestätigt Operation

Vorteile:

  • NAV ERP bekommt sofortige Response (nicht blockierend)
  • Bulk-Import möglich (1000+ Produkte in Queue)
  • Automatische Retries bei DB-Fehlern
  • Rate Limiting verhindert DB-Überlastung

3.3 Checkout & Bestellung

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  Client  │     │   Nuxt   │     │  PayPal  │     │  X-API   │     │ NAV ERP  │
└────┬─────┘     └────┬─────┘     └────┬─────┘     └────┬─────┘     └────┬─────┘
     │                │                │                │                │
     │ 1. Checkout    │                │                │                │
     ├───────────────>│                │                │                │
     │                │                │                │                │
     │                │ 2. Create Order│                │                │
     │                │  in DB (pending)                │                │
     │                │                │                │                │
     │                │ 3. Create PayPal Order          │                │
     │                ├───────────────>│                │                │
     │                │                │                │                │
     │                │ 4. Order ID    │                │                │
     │                │<───────────────┤                │                │
     │                │                │                │                │
     │ 5. PayPal ID   │                │                │                │
     │<───────────────┤                │                │                │
     │                │                │                │                │
     │ 6. User redirected to PayPal    │                │                │
     ├────────────────────────────────>│                │                │
     │                │                │                │                │
     │                │                │ 7. User pays   │                │
     │                │                │                │                │
     │ 8. Redirect back to App         │                │                │
     │<────────────────────────────────┤                │                │
     │                │                │                │                │
     │ 9. Capture Payment               │                │                │
     ├───────────────>│                │                │                │
     │                │ 10. Capture    │                │                │
     │                ├───────────────>│                │                │
     │                │                │                │                │
     │                │ 11. Success    │                │                │
     │                │<───────────────┤                │                │
     │                │                │                │                │
     │                │ 12. Update Order Status (paid)  │                │
     │                │                │                │                │
     │                │ 13. Transform Order to X-API Format              │
     │                │                │                │                │
     │                │ 14. POST /shopware/order        │                │
     │                ├────────────────────────────────>│                │
     │                │                │                │                │
     │                │                │                │ 15. Forward to NAV (SOAP)
     │                │                │                ├───────────────>│
     │                │                │                │                │
     │                │                │                │ 16. Process Order
     │                │                │                │                │
     │                │                │                │ 17. Confirmation
     │                │                │                │<───────────────┤
     │                │                │                │                │
     │                │ 18. HTTP 200 OK│                │                │
     │                │<────────────────────────────────┤                │
     │                │                │                │                │
     │                │ 19. Update Order Status (completed)              │
     │                │                │                │                │
     │                │ 20. Send Confirmation Email     │                │
     │                │                │                │                │
     │ 21. Show Success Page            │                │                │
     │<───────────────┤                │                │                │
     │                │                │                │                │

Schritte:

  1. User klickt "Jetzt kaufen"
    • If user has saved address: Form is pre-filled with saved data
    • Else: Empty form is shown
  2. Nuxt erstellt Order in DB (Status: pending)
    • If user checked "Save address": Update user profile with billing address
  3. Nuxt erstellt PayPal Order
  4. PayPal gibt Order-ID zurück
  5. Nuxt sendet Order-ID an Client
  6. Client redirected zu PayPal
  7. User zahlt bei PayPal
  8. PayPal redirected zurück zur App
  9. Client sendet "Capture Payment" Request
  10. Nuxt captured Payment bei PayPal
  11. PayPal bestätigt erfolgreiche Zahlung
  12. Nuxt updated Order-Status auf "paid"
  13. Nuxt fügt Job zu BullMQ Queue hinzu (async!)
  14. User sieht sofort Erfolgsseite mit Bestellnummer
  15. [Background] Worker holt Job aus Queue
  16. [Background] Worker transformiert Order in X-API Format (siehe 3.4)
  17. [Background] Worker sendet POST zu X-API /shopware/order
  18. [Background] X-API leitet Order an NAV ERP weiter (SOAP)
  19. [Background] NAV ERP verarbeitet Bestellung
  20. [Background] NAV sendet Bestätigung zurück an X-API
  21. [Background] X-API gibt HTTP 200 OK an Worker zurück
  22. [Background] Worker updated Order-Status auf "completed"
  23. [Background] Worker fügt Email-Job zu Queue hinzu
  24. [Background] Email-Worker sendet Bestätigungs-E-Mail

Vorteile:

  • User wartet nicht auf X-API/NAV (sofortige Bestätigung)
  • Automatische Retries bei X-API-Fehlern
  • Keine verlorenen Bestellungen bei Server-Restart
  • Entkoppelte Verarbeitung = robuster

3.4 X-API Order Transformation

Endpoint: POST https://x-api.experimenta.science/shopware/order

Environments:

  • DEV: https://x-api-dev.experimenta.science/shopware/order
  • STAGE: https://x-api-stage.experimenta.science/shopware/order
  • LIVE: https://x-api.experimenta.science/shopware/order

Authentication:

Die X-API ist mit HTTP Basic Authentication geschützt.

POST /shopware/order HTTP/1.1
Host: x-api.experimenta.science
Content-Type: application/json
Authorization: Basic <base64(username:password)>

{
  "shopPOSOrder": { ... }
}

Credentials:

  • Separate Credentials pro Environment (DEV, STAGE, LIVE)
  • Gespeichert in Environment Variables:
    • X_API_USERNAME - Username für X-API Basic Auth
    • X_API_PASSWORD - Password für X-API Basic Auth
    • X_API_BASE_URL - Base URL des X-API Endpoints

Security:

  • ⚠️ Credentials niemals im Code hardcoden
  • Nur über HTTPS-Verbindungen
  • In Production: Docker Secrets verwenden (nicht plain ENV vars)
  • Credentials regelmäßig rotieren
  • Getrennte Service-Accounts pro Environment

Order Mapping (our DB → X-API):

// Transform local order to X-API format
{
  shopPOSOrder: {
    // Document metadata
    documentType: "Order",  // Fixed for MVP
    externalDocumentNo: order.orderNumber,  // e.g., "EXP-2025-00001"
    externalPOSReceiptNo: "",  // Empty for web shop
    salesChannel: "Shop",  // Fixed
    shoppingCartCompletion: order.createdAt.toISOString(),  // ISO 8601 UTC
    visitType: "Private",  // Fixed for MVP (no institutions yet)

    // Pricing (in cents!)
    invoiceDiscountValue: 0,  // No discounts in MVP
    invoiceDiscPct: 0,
    amountIncludingVAT: Math.round(order.totalAmount * 100),  // EUR → Cents
    language: "DEU",  // Fixed for German MVP

    // Line items (one per cart item)
    salesLine: order.items.map((item, index) => ({
      type: "Item",
      lineNo: String((index + 1) * 10000),  // 10000, 20000, 30000, ...
      no: item.product.navProductId,  // Article number from NAV
      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,  // Standard 7% VAT for Jahreskarten
      lineAmountIncludingVAT: Math.round(item.quantity * item.priceSnapshot * 100),
      lineDiscountPct: 0,
      lineDiscountAmount: 0,
      seasonTicketCode: "",  // Empty for MVP
      couponCode: "",  // Empty for MVP
      visitorCategory: "120",  // Code for "Jahreskarten-Inhabende"
      ticketPriceType: "Jahreskarte Makerspace",
      ticketCode: generateUniqueTicketCode(),  // Generate unique code

      // Annual pass details (required for Makerspace-JK!)
      annualPass: {
        salutationCode: mapSalutation(user.salutation),  // "HERR", "FRAU", "K_ANGABE"
        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,  // YYYY-MM-DD format
        eMail: user.email,
        validFrom: calculateValidFromDate(),  // Today or next day
      }
    })),

    // Payment information
    payment: [{
      paymentEntryNo: 10000,
      amount: Math.round(order.totalAmount * 100),  // EUR → Cents
      paymentType: "Shop PayPal",  // Fixed for MVP
      createdOn: order.paymentCompletedAt.toISOString(),  // ISO 8601 UTC
      reference: order.paymentId,  // PayPal transaction ID (GUID format)
      paymentId: order.paymentId,  // PayPal order ID
    }],

    // Customer contact
    personContact: {
      experimentaAccountID: user.experimentaId,  // Our experimenta_id from Cidaas!
      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,  // false for registered users, true for guest checkout
    }
  }
}

Important Data Transformations:

  1. Prices: EUR (Decimal) → Cents (Integer)
    • 99.00 EUR → 9900 Cents
  2. Dates: JavaScript Date → ISO 8601 UTC String
    • new Date()"2025-10-28T14:30:00.000Z"
  3. Line Numbers: Sequential multiples of 10000
    • Item 1: "10000", Item 2: "20000", etc.
  4. Salutation Mapping:
    • "male""HERR"
    • "female""FRAU"
    • "other" / null"K_ANGABE"

Error Handling:

  • Retry logic with exponential backoff (3 attempts)
  • If X-API fails: Keep order as "paid" status, alert admin
  • Manual retry option in admin panel (post-MVP)
  • Log all requests/responses for debugging

Validation:

  • Zod schema validation before sending
  • Check all required fields present
  • Validate formats (email, dates, UUIDs)
  • Ensure amounts match (checksum validation)

3.5 Queue-Architektur (BullMQ + Redis)

Asynchrone Verarbeitung mit Job Queues für robuste und skalierbare Order- und Produkt-Synchronisation.

Übersicht

┌─────────────────────────────────────────────────────────────┐
│                    Queue-basierte Architektur                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Producer (Nuxt API)          Redis Queue          Worker  │
│  ┌────────────────┐         ┌──────────┐       ┌─────────┐│
│  │ POST /checkout │         │          │       │ Order   ││
│  │  → add job     ├────────>│  Orders  ├──────>│ Worker  ││
│  └────────────────┘         │          │       └─────────┘│
│                              └──────────┘                   │
│  ┌────────────────┐         ┌──────────┐       ┌─────────┐│
│  │ POST /erp/     │         │ Product  │       │ Product ││
│  │  products      ├────────>│  Sync    ├──────>│ Worker  ││
│  └────────────────┘         └──────────┘       └─────────┘│
│                              ┌──────────┐       ┌─────────┐│
│  ┌────────────────┐         │          │       │ Email   ││
│  │ Order complete ├────────>│  Emails  ├──────>│ Worker  ││
│  └────────────────┘         └──────────┘       └─────────┘│
│                                                             │
└─────────────────────────────────────────────────────────────┘

Queue-Definitionen (MVP)

1. x-api-orders Queue
  • Zweck: Bestellungen asynchron an X-API/NAV senden
  • Producer: POST /api/payment/capture (nach PayPal Success)
  • Worker: Order Worker
  • Retry: 5 Versuche mit exponential backoff (2s, 4s, 8s, 16s, 32s)
  • Priority: Hoch
  • Persistenz: Redis AOF
2. product-sync Queue
  • Zweck: Produkte von NAV ERP synchronisieren
  • Producer: POST /api/erp/products
  • Worker: Product Sync Worker
  • Retry: 3 Versuche mit exponential backoff (1s, 3s, 9s)
  • Priority: Mittel
  • Rate Limiting: Max 10 Jobs/Sekunde (DB schonen)
3. emails Queue (Post-MVP)
  • Zweck: E-Mails asynchron versenden
  • Producer: Order Worker (nach X-API Success)
  • Worker: Email Worker
  • Retry: 3 Versuche mit fixed delay (5s)
  • Priority: Niedrig

Redis-Konfiguration

Persistenz-Setup:

# Redis mit AOF + RDB für maximale Datensicherheit
redis-server \
  --appendonly yes \
  --appendfsync everysec \
  --save 60 1000 \
  --save 300 100 \
  --save 900 1

Was bedeutet das:

  • --appendonly yes: Append-Only File aktiviert
  • --appendfsync everysec: Jede Sekunde auf Disk schreiben
  • --save: RDB Snapshots als Backup

Garantie: Max. 1 Sekunde Datenverlust bei Server-Crash

Worker-Implementierung

Order Worker (server/workers/order-worker.ts):

import { Worker } from 'bullmq'

const worker = 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 },
    })

    if (!order) {
      throw new Error(`Order ${orderId} not found`)
    }

    // 2. Transform to X-API format
    const payload = transformOrderToXAPI(order)

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

    // 4. Submit to X-API (with timeout and Basic Auth)
    const response = await fetch(`${config.xApiBaseUrl}/shopware/order`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Basic ${authString}`,
      },
      body: JSON.stringify(payload),
      signal: AbortSignal.timeout(30000), // 30s timeout
    })

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

    // 5. Update order status
    await db.update(orders).set({ status: 'completed' }).where(eq(orders.id, orderId))

    // 6. Queue confirmation email
    await emailQueue.add('order-confirmation', {
      orderId,
      email: order.user.email,
    })

    return { success: true, orderId }
  },
  {
    connection: { host: 'redis', port: 6379 },
    concurrency: 5, // Max 5 parallel jobs
    limiter: {
      max: 10, // Max 10 jobs
      duration: 1000, // per second
    },
  }
)

// Error handling
worker.on('failed', (job, error) => {
  logger.error(`Order job ${job.id} failed:`, error)

  if (job.attemptsMade >= 5) {
    // Alert admin after max retries
    alertAdmin(`Order ${job.data.orderId} failed permanently`)
  }
})

worker.on('completed', (job) => {
  logger.info(`Order ${job.data.orderId} submitted successfully`)
})

Product Sync Worker (server/workers/product-worker.ts):

import { Worker } from 'bullmq'

const worker = new Worker(
  'product-sync',
  async (job) => {
    const product = job.data

    // Upsert product
    await db
      .insert(products)
      .values({
        navProductId: product.nav_product_id,
        name: product.name,
        price: product.price,
        stock: product.stock,
        description: product.description,
        imageUrl: product.image_url,
        status: product.status,
      })
      .onConflictDoUpdate({
        target: products.navProductId,
        set: {
          name: product.name,
          price: product.price,
          stock: product.stock,
          description: product.description,
          imageUrl: product.image_url,
          status: product.status,
          updatedAt: new Date(),
        },
      })

    return { success: true, navProductId: product.nav_product_id }
  },
  {
    connection: { host: 'redis', port: 6379 },
    limiter: {
      max: 10, // Max 10 products
      duration: 1000, // per second (DB schonen!)
    },
  }
)

Monitoring: BullBoard

Dashboard-Setup (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),
    new BullMQAdapter(emailQueue),
  ],
  serverAdapter,
  options: {
    uiConfig: {
      boardTitle: 'experimenta Queue Monitor',
    },
  },
})

// Protected with auth middleware
// Access: http://localhost:3000/admin/queues

Features:

  • Real-time Job-Status
  • Manual Retry
  • Job Details & Logs
  • Queue-Metriken (Waiting, Active, Completed, Failed)
  • Fehler-Analyse

Fehlerbehandlung & Retry-Strategien

Exponential Backoff:

// Order Queue: 5 Versuche
await orderQueue.add('submit', data, {
  attempts: 5,
  backoff: {
    type: 'exponential',
    delay: 2000, // 2s, 4s, 8s, 16s, 32s
  },
  removeOnComplete: 1000, // Keep last 1000 completed
  removeOnFail: false, // Keep all failed (manual review)
})

Dead Letter Queue:

  • Jobs die 5x fehlschlagen bleiben in Redis
  • Admin-Alert bei permanenten Fehlern
  • Manuelle Retry-Option in BullBoard

Skalierung

Mehrere Worker-Instanzen:

# docker-compose.yml
services:
  worker-orders:
    build: .
    command: node server/workers/order-worker.js
    deploy:
      replicas: 2 # 2 Instanzen für höheren Durchsatz
    environment:
      - REDIS_URL=redis://redis:6379

Load Distribution:

  • BullMQ verteilt Jobs automatisch über Worker
  • Jeder Job wird nur 1x verarbeitet (Lock-Mechanismus)

Vorteile der Queue-Architektur

Robustheit:

  • Keine verlorenen Jobs bei Server-Restart (Redis Persistenz)
  • Automatische Retries bei temporären Fehlern
  • Graceful Degradation (User sieht Erfolg, auch wenn X-API langsam)

Performance:

  • Non-blocking API Endpoints (sofortige Response)
  • Rate Limiting schützt DB vor Überlastung
  • Parallelverarbeitung durch multiple Worker

Monitoring:

  • BullBoard Dashboard für Ops-Team
  • Fehler-Tracking pro Job
  • Metriken für Durchsatz-Analyse

Skalierbarkeit:

  • Horizontal skalierbar (mehr Worker = höherer Durchsatz)
  • Priority Queues für wichtige Jobs
  • Delayed Jobs für zeitgesteuerte Tasks

3.6 Authentication & Authorization (Cidaas OAuth2/OIDC)

Vollständige Authentifizierungsarchitektur mit OAuth2 Authorization Code Flow + PKCE.

Übersicht

┌──────────────────────────────────────────────────────────────────────┐
│              Authentication Architecture (Cidaas + Local DB)          │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌──────────┐      ┌──────────────┐      ┌──────────┐             │
│  │  Client  │ HTTPS │  Nuxt 4 App  │ OIDC │  Cidaas  │             │
│  │ (Browser)│◄─────►│   (Server)   │◄────►│ (OAuth2) │             │
│  └──────────┘      └───────┬──────┘      └──────────┘             │
│                            │                                        │
│                            │ Link via experimenta_id                │
│                            │                                        │
│                     ┌──────▼────────┐                               │
│                     │  PostgreSQL   │                               │
│                     │  - users      │                               │
│                     │  - orders     │                               │
│                     │  - ...        │                               │
│                     └───────────────┘                               │
│                                                                      │
│  Cidaas Rolle:   ✅ Authentifizierung (Login/Registration)          │
│                  ❌ NICHT für Nutzerdaten-Speicherung               │
│                                                                      │
│  Local DB Rolle: ✅ Nutzerprofile (Name, Adresse, Präferenzen)      │
│                  ✅ Bestellhistorie, Warenkorb                      │
│                  ✅ Rollen & Berechtigungen (Post-MVP)              │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

Technologie-Stack

Komponente Technologie Version
Auth Module nuxt-auth-utils Latest
OAuth2 Provider Cidaas (Widas) OIDC-konform
OAuth2 Flow Authorization Code + PKCE RFC 7636
Session Storage Encrypted HTTP-only Cookies AES-256-GCM
JWT Validation jose Latest
Session Dauer 30 Tage Configurable

Detailed Login Flow (OAuth2 + PKCE)

┌─────────────┐         ┌─────────────┐         ┌─────────────┐         ┌──────────────┐
│   Browser   │  HTTPS  │ Nuxt 4 App  │  OIDC   │   Cidaas    │  Link   │  PostgreSQL  │
│  (Client)   │◄───────►│  (Server)   │◄───────►│ (Identity)  │         │ (User Store) │
└─────────────┘         └─────────────┘         └─────────────┘         └──────────────┘
      │                       │                       │                         │
      │  1. Navigate /auth    │                       │                         │
      │     (Login Tab)       │                       │                         │
      ├──────────────────────►│                       │                         │
      │                       │                       │                         │
      │  2. Render Login Form │                       │                         │
      │◄──────────────────────┤                       │                         │
      │                       │                       │                         │
      │  3. Submit email      │                       │                         │
      │     POST /api/auth/   │                       │                         │
      │     login             │                       │                         │
      ├──────────────────────►│                       │                         │
      │                       │                       │                         │
      │                       │  4. Generate PKCE     │                         │
      │                       │     verifier (64chars)│                         │
      │                       │     + challenge       │                         │
      │                       │     (SHA256 hash)     │                         │
      │                       │                       │                         │
      │                       │  5. Generate state    │                         │
      │                       │     (CSRF token)      │                         │
      │                       │                       │                         │
      │                       │  6. Store verifier +  │                         │
      │                       │     state in cookies  │                         │
      │                       │     (5min TTL)        │                         │
      │                       │                       │                         │
      │                       │  7. Build OAuth2 URL: │                         │
      │                       │     - client_id       │                         │
      │                       │     - redirect_uri    │                         │
      │                       │     - code_challenge  │                         │
      │                       │     - state           │                         │
      │                       │     - scope: openid   │                         │
      │                       │       profile email   │                         │
      │                       │                       │                         │
      │  8. Redirect to       │                       │                         │
      │     Cidaas AuthZ URL  │                       │                         │
      │◄──────────────────────┤                       │                         │
      │                       │                       │                         │
      │  9. Navigate to       │                       │                         │
      │     Cidaas login page │                       │                         │
      ├──────────────────────────────────────────────►│                         │
      │                       │                       │                         │
      │  10. User enters      │                       │                         │
      │      email + password │                       │                         │
      ├──────────────────────────────────────────────►│                         │
      │                       │                       │                         │
      │                       │  11. Cidaas validates │                         │
      │                       │      credentials      │                         │
      │                       │                       │                         │
      │  12. Redirect back    │                       │                         │
      │      to callback with │                       │                         │
      │      auth code + state│                       │                         │
      │◄──────────────────────────────────────────────┤                         │
      │                       │                       │                         │
      │  13. GET /api/auth/   │                       │                         │
      │      callback?        │                       │                         │
      │      code=xxx&        │                       │                         │
      │      state=yyy        │                       │                         │
      ├──────────────────────►│                       │                         │
      │                       │                       │                         │
      │                       │  14. Validate state   │                         │
      │                       │      (CSRF check)     │                         │
      │                       │                       │                         │
      │                       │  15. Retrieve PKCE    │                         │
      │                       │      verifier from    │                         │
      │                       │      cookie           │                         │
      │                       │                       │                         │
      │                       │  16. Exchange code    │                         │
      │                       │      + verifier for   │                         │
      │                       │      tokens           │                         │
      │                       │      POST /token-srv  │                         │
      │                       ├──────────────────────►│                         │
      │                       │                       │                         │
      │                       │  17. Validate code +  │                         │
      │                       │      verifier:        │                         │
      │                       │      SHA256(verifier) │                         │
      │                       │      == challenge?    │                         │
      │                       │                       │                         │
      │                       │  18. Return tokens:   │                         │
      │                       │      - access_token   │                         │
      │                       │      - id_token (JWT) │                         │
      │                       │      - refresh_token  │                         │
      │                       │◄──────────────────────┤                         │
      │                       │                       │                         │
      │                       │  19. Validate ID token│                         │
      │                       │      (JWT signature,  │                         │
      │                       │       exp, iss, aud)  │                         │
      │                       │                       │                         │
      │                       │  20. Fetch UserInfo   │                         │
      │                       │      GET /users-srv   │                         │
      │                       ├──────────────────────►│                         │
      │                       │                       │                         │
      │                       │  21. User profile:    │                         │
      │                       │      { sub, email,    │                         │
      │                       │        given_name,    │                         │
      │                       │        family_name }  │                         │
      │                       │◄──────────────────────┤                         │
      │                       │                       │                         │
      │                       │  22. Query user by experimenta_id = sub         │
      │                       ├─────────────────────────────────────────────────►│
      │                       │                       │                         │
      │                       │  23. User exists?                               │
      │                       │◄─────────────────────────────────────────────────┤
      │                       │                       │                         │
      │                       │  24. If new: INSERT user                        │
      │                       │      If exists: UPDATE updated_at               │
      │                       ├─────────────────────────────────────────────────►│
      │                       │                       │                         │
      │                       │  25. User record                                │
      │                       │◄─────────────────────────────────────────────────┤
      │                       │                       │                         │
      │                       │  26. Create encrypted │                         │
      │                       │      session cookie:  │                         │
      │                       │      { user: {        │                         │
      │                       │        id, email,     │                         │
      │                       │        firstName,     │                         │
      │                       │        lastName       │                         │
      │                       │      }}               │                         │
      │                       │      (AES-256-GCM)    │                         │
      │                       │                       │                         │
      │  27. Set-Cookie:      │                       │                         │
      │      experimenta-     │                       │                         │
      │      session=<enc>;   │                       │                         │
      │      HttpOnly; Secure;│                       │                         │
      │      SameSite=Lax;    │                       │                         │
      │      Max-Age=2592000  │                       │                         │
      │◄──────────────────────┤                       │                         │
      │                       │                       │                         │
      │  28. Redirect to /    │                       │                         │
      │      (Homepage)       │                       │                         │
      │◄──────────────────────┤                       │                         │
      │                       │                       │                         │
      │  29. Navigate Home    │                       │                         │
      │      (logged in!)     │                       │                         │
      ├──────────────────────►│                       │                         │
      │                       │                       │                         │

Key Security Features:

  1. PKCE (Proof Key for Code Exchange):

    • Prevents authorization code interception attacks
    • Verifier stored temporarily (5min), never transmitted
    • Challenge sent to Cidaas, verified on token exchange
    • Required even for confidential clients (defense in depth)
  2. State Parameter:

    • CSRF protection for OAuth2 callback
    • Random 32-byte string generated per request
    • Validated on callback before code exchange
  3. Short-lived Temporary Cookies:

    • PKCE verifier: 5min TTL (enough for auth flow)
    • OAuth state: 5min TTL
    • Prevents replay attacks
  4. Encrypted Session Cookie:

    • AES-256-GCM encryption via nuxt-auth-utils
    • HTTP-only: Not accessible via JavaScript (XSS protection)
    • Secure flag: Only transmitted over HTTPS
    • SameSite=Lax: CSRF protection
    • 30-day expiration (configurable)

Registration Flow

┌─────────────┐         ┌─────────────┐         ┌─────────────┐
│   Browser   │         │ Nuxt 4 App  │         │   Cidaas    │
└─────────────┘         └─────────────┘         └─────────────┘
      │                       │                       │
      │  1. Navigate /auth    │                       │
      │     (Register Tab)    │                       │
      ├──────────────────────►│                       │
      │                       │                       │
      │  2. Render Register   │                       │
      │     Form              │                       │
      │◄──────────────────────┤                       │
      │                       │                       │
      │  3. Submit:           │                       │
      │     - email           │                       │
      │     - password        │                       │
      │     - firstName       │                       │
      │     - lastName        │                       │
      │     POST /api/auth/   │                       │
      │     register          │                       │
      ├──────────────────────►│                       │
      │                       │                       │
      │                       │  4. Validate input    │
      │                       │     (Zod schema)      │
      │                       │                       │
      │                       │  5. Call Cidaas       │
      │                       │     Registration API: │
      │                       │     POST /users-srv/  │
      │                       │     register          │
      │                       ├──────────────────────►│
      │                       │                       │
      │                       │  6. Create user       │
      │                       │     Send verification │
      │                       │     email             │
      │                       │                       │
      │                       │  7. Success response  │
      │                       │◄──────────────────────┤
      │                       │                       │
      │  8. Success message:  │                       │
      │     "Please verify    │                       │
      │     your email"       │                       │
      │◄──────────────────────┤                       │
      │                       │                       │
      │  9. User clicks email │                       │
      │     verification link │                       │
      ├──────────────────────────────────────────────►│
      │                       │                       │
      │                       │  10. Verify email     │
      │                       │      Mark as verified │
      │                       │                       │
      │  11. Confirmation     │                       │
      │◄──────────────────────────────────────────────┤
      │                       │                       │
      │  12. User can now     │                       │
      │      login (follow    │                       │
      │      login flow)      │                       │
      │                       │                       │

Important Notes:

  • User is created in Cidaas, NOT in local DB
  • Local DB user record created on first login (via callback)
  • Email verification required before login
  • Password requirements enforced by Cidaas
  • Registration can fail if email already exists

Session Management

Session Lifecycle:

// Session created on login (server/api/auth/callback.get.ts)
await setUserSession(event, {
  user: {
    id: user.id, // Local DB UUID
    experimentaId: user.experimentaId, // Cidaas sub claim
    email: user.email,
    firstName: user.firstName,
    lastName: user.lastName,
  },
  loggedInAt: new Date().toISOString(),
})

// Session cookie:
// - Name: experimenta-session
// - Value: <encrypted-data>
// - Max-Age: 2592000 (30 days)
// - HttpOnly, Secure, SameSite=Lax

Client-Side Session Access:

// In Vue components (composables/useAuth.ts)
const { user, loggedIn } = useUserSession()

// user.value: { id, email, firstName, ... } or null
// loggedIn.value: boolean

Server-Side Session Validation:

// Protected API routes
export default defineEventHandler(async (event) => {
  // Require authentication (throws 401 if not logged in)
  const { user } = await requireUserSession(event)

  // Access user data
  const userId = user.id
  // ...
})

Session Expiration:

  • Default: 30 days (2592000 seconds)
  • Configurable via nuxt.config.ts
  • Auto-refresh: Session cookie updated on each request (sliding expiration)
  • Logout: clearUserSession() deletes cookie immediately

Protected Routes

Middleware Pattern:

// middleware/auth.ts
export default defineNuxtRouteMiddleware(async (to, from) => {
  const { loggedIn } = useUserSession()

  if (!loggedIn.value) {
    // Store intended destination for post-login redirect
    useCookie('redirect_after_login', {
      maxAge: 600, // 10 minutes
      path: '/',
    }).value = to.fullPath

    return navigateTo('/auth')
  }
})

Usage in Pages:

<!-- pages/profile.vue -->
<script setup lang="ts">
definePageMeta({
  middleware: 'auth', // Require authentication
})

const { user } = useAuth()
</script>

<template>
  <div>
    <h1>Welcome, {{ user.firstName }}!</h1>
  </div>
</template>

JWT Token Validation

ID Token Verification (jose):

// server/utils/jwt.ts
import { jwtVerify, createRemoteJWKSet } from 'jose'

const config = useRuntimeConfig()

// JWKS (JSON Web Key Set) - Cidaas public keys
const JWKS = createRemoteJWKSet(
  new URL(config.cidaas.jwksUrl) // /.well-known/jwks.json
)

export async function verifyIdToken(idToken: string) {
  const { payload } = await jwtVerify(idToken, JWKS, {
    issuer: config.cidaas.issuer, // Must match Cidaas URL
    audience: config.cidaas.clientId, // Must match our client ID
  })

  return payload // { sub, email, exp, iat, ... }
}

When Tokens Are Validated:

  1. On OAuth2 Callback - ID token validated before creating session
  2. Token Refresh (if implemented) - New tokens validated
  3. Every Request - NOT needed! Session cookie already validated by nuxt-auth-utils

Token Storage:

  • Access Token: Used once (UserInfo fetch), then discarded
  • ID Token: Validated once, then discarded
  • Refresh Token: NOT stored (stateless sessions preferred)
  • Session Data: Stored in encrypted cookie (no tokens needed)

User Data Synchronization

Cidaas → PostgreSQL Mapping:

// server/api/auth/callback.get.ts

// Cidaas UserInfo response
const cidaasUser = {
  sub: 'cidaas-user-123', // Unique Cidaas ID
  email: 'user@example.com',
  email_verified: true,
  given_name: 'Max',
  family_name: 'Mustermann',
  updated_at: 1698765432,
}

// Map to local user record
await db.insert(users).values({
  experimentaId: cidaasUser.sub, // Link to Cidaas!
  email: cidaasUser.email,
  firstName: cidaasUser.given_name,
  lastName: cidaasUser.family_name,
})

Upsert Strategy:

  • First Login: INSERT new user record
  • Subsequent Logins: UPDATE updated_at timestamp
  • No automatic sync: User profile changes in Cidaas not synced (by design)
  • Manual sync: User can re-login to refresh profile data (if needed)

Security Architecture

Multi-Layer Security Model:

┌────────────────────────────────────────────────────────────┐
│                   Security Layers                          │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  Layer 1: Transport (HTTPS)                                │
│  ├─ TLS 1.3                                                │
│  ├─ HSTS Headers (Strict-Transport-Security)              │
│  └─ Certificate: Let's Encrypt                            │
│                                                            │
│  Layer 2: OAuth2 Security (PKCE + State)                   │
│  ├─ PKCE: Authorization code interception prevention      │
│  ├─ State: CSRF protection                                │
│  └─ Short-lived temporary cookies (5min)                  │
│                                                            │
│  Layer 3: Session Security                                 │
│  ├─ Encrypted cookies (AES-256-GCM)                       │
│  ├─ HTTP-only flag (XSS protection)                       │
│  ├─ Secure flag (HTTPS only)                              │
│  ├─ SameSite=Lax (CSRF protection)                        │
│  └─ 30-day expiration                                     │
│                                                            │
│  Layer 4: JWT Validation                                   │
│  ├─ Signature verification (JWKS)                         │
│  ├─ Expiration check (exp claim)                          │
│  ├─ Issuer validation (iss claim)                         │
│  └─ Audience validation (aud claim)                       │
│                                                            │
│  Layer 5: Input Validation                                 │
│  ├─ Zod schemas (all endpoints)                           │
│  ├─ SQL injection prevention (Drizzle ORM)                │
│  └─ XSS prevention (Vue auto-escaping)                    │
│                                                            │
│  Layer 6: Rate Limiting                                    │
│  ├─ Login: 5 attempts / 15min per IP                      │
│  ├─ Register: 3 attempts / hour per IP                    │
│  └─ Global: 100 req/min per IP (Nginx)                    │
│                                                            │
└────────────────────────────────────────────────────────────┘

API Endpoints

Authentication Endpoints:

POST   /api/auth/login          - Initiate OAuth2 flow
GET    /api/auth/callback       - OAuth2 callback handler
POST   /api/auth/register       - Register new user
POST   /api/auth/logout         - Clear session
GET    /api/auth/me             - Get current user

Implementation Details:

See docs/CIDAAS_INTEGRATION.md for:

  • Complete endpoint implementations
  • Server utilities (PKCE, Cidaas API client, JWT validation)
  • Client components (Login/Register forms)
  • Middleware (auth, rate limiting)
  • i18n translations
  • Testing strategies

Environment Configuration

Required Environment Variables:

# Cidaas OAuth2 Configuration
CIDAAS_CLIENT_ID=<from-cidaas-admin>
CIDAAS_CLIENT_SECRET=<from-cidaas-admin>
CIDAAS_ISSUER=https://experimenta.cidaas.de
CIDAAS_AUTHORIZE_URL=https://experimenta.cidaas.de/authz-srv/authz
CIDAAS_TOKEN_URL=https://experimenta.cidaas.de/token-srv/token
CIDAAS_USERINFO_URL=https://experimenta.cidaas.de/users-srv/userinfo
CIDAAS_JWKS_URL=https://experimenta.cidaas.de/.well-known/jwks.json
CIDAAS_REDIRECT_URI=https://my.experimenta.science/api/auth/callback

# Session Encryption Secret (generate with: openssl rand -hex 32)
NUXT_SESSION_SECRET=<64-char-hex-secret>

Cidaas Admin Panel Setup:

  1. Create OAuth2 application in Cidaas
  2. Configure Grant Types: authorization_code, refresh_token
  3. Enable PKCE (code challenge method: S256)
  4. Set Redirect URIs (exact match required!)
  5. Configure Scopes: openid, profile, email
  6. Note Client ID & Secret

See docs/CIDAAS_INTEGRATION.md#setup-anleitung for detailed setup checklist.

Monitoring & Debugging

Logging:

// Auth-related logs (automatically logged)
logger.info({ userId, action: 'login' }, 'User logged in')
logger.error({ userId, error }, 'Login failed')
logger.info({ userId }, 'User logged out')

Debug Tips:

  1. Inspect session cookie:

    // Browser console
    document.cookie.split(';').find((c) => c.includes('experimenta-session'))
    
  2. Decode JWT (unverified):

    const idToken = 'eyJ...'
    const payload = JSON.parse(atob(idToken.split('.')[1]))
    console.log(payload)
    
  3. Enable debug logging:

    // nuxt.config.ts
    export default defineNuxtConfig({
      nitro: { logLevel: 'debug' },
    })
    

Fehlerbehandlung

Common Errors:

Error Cause Solution
Invalid state parameter State cookie expired or CSRF attack Ensure cookies enabled, check cookie domain
PKCE verifier not found Verifier cookie expired Complete auth flow within 5 minutes
JWT verification failed Invalid signature or expired token Verify JWKS URL, check system clock
Token exchange failed Invalid code or client credentials Check client ID/secret, redirect URI exact match
Too many requests (429) Rate limit exceeded Wait for limit window to expire

Error Handling Pattern:

// server/api/auth/callback.get.ts
try {
  // OAuth2 flow
  const tokens = await exchangeCodeForToken(code, verifier)
  // ... create session
  return sendRedirect(event, '/')
} catch (error) {
  console.error('OAuth callback error:', error)

  // Clean up cookies
  deleteCookie(event, 'oauth_state')
  deleteCookie(event, 'pkce_verifier')

  // Redirect to login with error
  return sendRedirect(event, '/auth?error=login_failed')
}

Performance Considerations

JWKS Caching:

  • Cidaas JWKS endpoint cached for 1 hour (jose default)
  • Reduces latency on token validation
  • Auto-refreshes when keys rotated

Session Cookie Size:

  • Typical size: ~500 bytes (encrypted)
  • Transmitted on every request (overhead minimal)
  • Consider Redis sessions for very large user objects

Database Queries:

  • User lookup by experimenta_id (indexed)
  • Single query on login (no joins needed)
  • No additional queries per request (session-based)

4. Datenbankschema

4.1 ER-Diagramm (Textual)

┌─────────────────────┐
│       User          │
├─────────────────────┤
│ id (PK)             │
│ experimenta_id (UQ) │
│ email               │
│ first_name          │
│ last_name           │
│ phone               │
│ salutation          │
│ date_of_birth       │
│ street              │
│ post_code           │
│ city                │
│ country_code        │
│ created_at          │
│ updated_at          │
└──────────┬──────────┘
           │
           │ 1:N
           │
┌──────────▼──────────┐         ┌─────────────────────┐
│       Cart          │         │      Product        │
├─────────────────────┤         ├─────────────────────┤
│ id (PK)             │         │ id (PK)             │
│ user_id (FK)        │         │ nav_product_id (UQ) │
│ session_id          │         │ name                │
│ created_at          │         │ description         │
│ updated_at          │         │ price               │
└──────────┬──────────┘         │ image_url           │
           │                    │ stock               │
           │ 1:N                │ status              │
           │                    │ created_at          │
┌──────────▼──────────┐         │ updated_at          │
│     CartItem        │         └──────────┬──────────┘
├─────────────────────┤                    │
│ id (PK)             │                    │
│ cart_id (FK)        │                    │
│ product_id (FK) ────┼────────────────────┘
│ quantity            │
│ price_snapshot      │
│ created_at          │
└─────────────────────┘


┌─────────────────────┐
│       Order         │
├─────────────────────┤
│ id (PK)             │
│ order_number (UQ)   │
│ user_id (FK) ───────┼────> User
│ status              │
│ total_amount        │
│ payment_method      │
│ payment_id          │
│ billing_address     │
│ created_at          │
│ updated_at          │
└──────────┬──────────┘
           │
           │ 1:N
           │
┌──────────▼──────────┐
│     OrderItem       │
├─────────────────────┤
│ id (PK)             │
│ order_id (FK)       │
│ product_id (FK) ────┼────> Product
│ quantity            │
│ price               │
│ created_at          │
└─────────────────────┘

4.2 Drizzle Schema Definition

// server/database/schema.ts
import {
  pgTable,
  uuid,
  varchar,
  timestamp,
  decimal,
  integer,
  text,
  jsonb,
  pgEnum,
} from 'drizzle-orm/pg-core'

// Enums
export const orderStatusEnum = pgEnum('order_status', [
  'pending',
  'paid',
  'processing',
  'completed',
  'cancelled',
])
export const productStatusEnum = pgEnum('product_status', ['active', 'inactive'])
export const paymentMethodEnum = pgEnum('payment_method', ['paypal'])

// Users
export const users = pgTable('users', {
  id: uuid('id').defaultRandom().primaryKey(),
  experimentaId: varchar('experimenta_id', { length: 255 }).notNull().unique(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  firstName: varchar('first_name', { length: 100 }),
  lastName: varchar('last_name', { length: 100 }),
  phone: varchar('phone', { length: 20 }),

  // Billing address fields (optional - filled during checkout or profile edit)
  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'),

  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
})

// Products
export const products = pgTable('products', {
  id: uuid('id').defaultRandom().primaryKey(),
  navProductId: varchar('nav_product_id', { length: 100 }).notNull().unique(),
  name: varchar('name', { length: 255 }).notNull(),
  description: text('description'),
  price: decimal('price', { precision: 10, scale: 2 }).notNull(),
  imageUrl: varchar('image_url', { length: 500 }),
  stock: integer('stock').default(0).notNull(),
  status: productStatusEnum('status').default('active').notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
})

// Carts
export const carts = pgTable('carts', {
  id: uuid('id').defaultRandom().primaryKey(),
  userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }),
  sessionId: varchar('session_id', { length: 255 }),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
})

// Cart Items
export const cartItems = pgTable('cart_items', {
  id: uuid('id').defaultRandom().primaryKey(),
  cartId: uuid('cart_id')
    .references(() => carts.id, { onDelete: 'cascade' })
    .notNull(),
  productId: uuid('product_id')
    .references(() => products.id)
    .notNull(),
  quantity: integer('quantity').default(1).notNull(),
  priceSnapshot: decimal('price_snapshot', { precision: 10, scale: 2 }).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})

// Orders
export const orders = pgTable('orders', {
  id: uuid('id').defaultRandom().primaryKey(),
  orderNumber: varchar('order_number', { length: 50 }).notNull().unique(),
  userId: uuid('user_id')
    .references(() => users.id)
    .notNull(),
  status: orderStatusEnum('status').default('pending').notNull(),
  totalAmount: decimal('total_amount', { precision: 10, scale: 2 }).notNull(),
  paymentMethod: paymentMethodEnum('payment_method').notNull(),
  paymentId: varchar('payment_id', { length: 255 }),
  billingAddress: jsonb('billing_address'),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
})

// Order Items
export const orderItems = pgTable('order_items', {
  id: uuid('id').defaultRandom().primaryKey(),
  orderId: uuid('order_id')
    .references(() => orders.id, { onDelete: 'cascade' })
    .notNull(),
  productId: uuid('product_id')
    .references(() => products.id)
    .notNull(),
  quantity: integer('quantity').default(1).notNull(),
  price: decimal('price', { precision: 10, scale: 2 }).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})

5. API-Architektur

5.1 REST API Struktur

/api
  /auth
    /login             POST   - Initiate Cidaas OAuth
    /callback          GET    - OAuth callback handler
    /logout            POST   - End session
    /me                GET    - Get current user

  /products
    /                  GET    - List all products
    /:id               GET    - Get product details

  /cart
    /                  GET    - Get current cart
    /items             POST   - Add item to cart
    /items/:id         PATCH  - Update item quantity
    /items/:id         DELETE - Remove item from cart

  /orders
    /                  GET    - List user's orders
    /                  POST   - Create new order
    /:id               GET    - Get order details

  /payment
    /paypal
      /create          POST   - Create PayPal order
      /capture         POST   - Capture PayPal payment
      /webhook         POST   - PayPal webhook handler

  /erp
    /products          POST   - Receive product updates from NAV
    /stock             POST   - Update stock levels

5.2 API Authentication

Client-to-Server:

  • Session-based (HTTP-only Cookie)
  • Cookie: __session (encrypted)
  • Nuxt useSession() composable

ERP-to-Server:

  • API Key Authentication
  • Header: X-API-Key: <secret>
  • Stored in Environment Variables

Cidaas OAuth:

  • OAuth 2.0 Authorization Code Flow
  • PKCE for additional security

6. Sicherheitsarchitektur

6.1 Authentifizierung & Autorisierung

┌─────────────────────────────────────────────────────────────┐
│                     Security Layers                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Layer 1: Transport Security (HTTPS/TLS 1.3)               │
│  ├─ Let's Encrypt SSL Certificate                          │
│  ├─ HSTS Headers                                           │
│  └─ Secure Cookies (httpOnly, secure, sameSite)            │
│                                                             │
│  Layer 2: Authentication (Cidaas OAuth2)                    │
│  ├─ Authorization Code Flow + PKCE                         │
│  ├─ JWT Token Validation (jose)                            │
│  └─ Session Management (encrypted cookies)                 │
│                                                             │
│  Layer 3: Authorization (Route Guards)                      │
│  ├─ Middleware: requireAuth()                              │
│  ├─ API Middleware: validateApiKey()                       │
│  └─ Resource Ownership Checks                              │
│                                                             │
│  Layer 4: Input Validation (Zod)                            │
│  ├─ Request Body Validation                                │
│  ├─ Query Params Validation                                │
│  └─ SQL Injection Prevention (Drizzle)                     │
│                                                             │
│  Layer 5: Rate Limiting                                     │
│  ├─ Nginx: 100 req/min per IP                              │
│  ├─ API Endpoints: Custom limits                           │
│  └─ Redis-based tracking (optional)                        │
│                                                             │
│  Layer 6: CORS & CSP                                        │
│  ├─ CORS: Only experimenta.science                         │
│  ├─ CSP Headers                                            │
│  └─ X-Frame-Options, X-Content-Type-Options                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6.2 Secrets Management

Environment Variables:

# .env (NEVER commit!)
DATABASE_URL=postgresql://user:pass@db:5432/experimenta

# Cidaas Authentication
CIDAAS_CLIENT_ID=xxx
CIDAAS_CLIENT_SECRET=xxx

# PayPal Payment
PAYPAL_CLIENT_ID=xxx
PAYPAL_CLIENT_SECRET=xxx

# X-API Integration (Basic Auth)
X_API_BASE_URL=https://x-api-dev.experimenta.science
X_API_USERNAME=shop_user_dev
X_API_PASSWORD=xxx

# Internal API Security
ERP_API_KEY=xxx
SESSION_SECRET=xxx

# Email (Optional)
SMTP_USER=xxx
SMTP_PASS=xxx

# Monitoring (Optional)
SENTRY_DSN=xxx

Docker Secrets (Production):

# docker-compose.yml
services:
  app:
    environment:
      - DATABASE_URL_FILE=/run/secrets/db_url
    secrets:
      - db_url
      - cidaas_secret
      - paypal_secret
      - xapi_username
      - xapi_password

  worker:
    # Workers need X-API credentials for order submission
    secrets:
      - xapi_username
      - xapi_password

secrets:
  db_url:
    file: ./secrets/db_url.txt
  cidaas_secret:
    file: ./secrets/cidaas.txt
  paypal_secret:
    file: ./secrets/paypal.txt
  xapi_username:
    file: ./secrets/xapi_username.txt
  xapi_password:
    file: ./secrets/xapi_password.txt

7. Deployment-Architektur

7.1 Production Setup (Hetzner)

┌─────────────────────────────────────────────────────────────┐
│              Hetzner Dedicated Server / VPS                 │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              Proxmox Virtualization                  │   │
│  │                                                       │   │
│  │  ┌───────────────────────────────────────────────┐  │   │
│  │  │         Docker Container (Production)         │  │   │
│  │  │                                               │  │   │
│  │  │  ┌─────────────────────────────────────────┐ │  │   │
│  │  │  │    Nginx (Reverse Proxy)                │ │  │   │
│  │  │  │  - SSL Termination (Let's Encrypt)      │ │  │   │
│  │  │  │  - Load Balancing                       │ │  │   │
│  │  │  │  - Rate Limiting                        │ │  │   │
│  │  │  │  - Static File Serving                  │ │  │   │
│  │  │  └─────────────┬───────────────────────────┘ │  │   │
│  │  │                │                             │  │   │
│  │  │  ┌─────────────▼───────────────────────────┐ │  │   │
│  │  │  │    Nuxt App (Node.js)                   │ │  │   │
│  │  │  │  - Port 3000                            │ │  │   │
│  │  │  │  - PM2 Process Manager (optional)       │ │  │   │
│  │  │  │  - Multiple Instances (Clustering)      │ │  │   │
│  │  │  └─────────────┬───────────────────────────┘ │  │   │
│  │  │                │                             │  │   │
│  │  │  ┌─────────────▼───────────────────────────┐ │  │   │
│  │  │  │    PostgreSQL 16                        │ │  │   │
│  │  │  │  - Port 5432 (internal only)            │ │  │   │
│  │  │  │  - Persistent Volume                    │ │  │   │
│  │  │  │  - Daily Backups                        │ │  │   │
│  │  │  └─────────────────────────────────────────┘ │  │   │
│  │  │                                               │  │   │
│  │  │  Optional:                                    │  │   │
│  │  │  ┌─────────────────────────────────────────┐ │  │   │
│  │  │  │    Redis (Caching/Sessions)             │ │  │   │
│  │  │  └─────────────────────────────────────────┘ │  │   │
│  │  │                                               │  │   │
│  │  └───────────────────────────────────────────────┘  │   │
│  │                                                       │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

7.2 Docker Compose (Production)

# docker-compose.prod.yml
version: '3.9'

services:
  nginx:
    image: nginx:alpine
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - ./public:/usr/share/nginx/html:ro
    depends_on:
      - app
    restart: always

  app:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=redis://redis:6379
      - CIDAAS_CLIENT_ID=${CIDAAS_CLIENT_ID}
      - CIDAAS_CLIENT_SECRET=${CIDAAS_CLIENT_SECRET}
      - PAYPAL_CLIENT_ID=${PAYPAL_CLIENT_ID}
      - PAYPAL_CLIENT_SECRET=${PAYPAL_CLIENT_SECRET}
    depends_on:
      - db
      - redis
    restart: always
    deploy:
      replicas: 2 # For load balancing

  worker:
    build:
      context: .
      dockerfile: Dockerfile
    command: node server/workers/index.js
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=redis://redis:6379
      - X_API_BASE_URL=${X_API_BASE_URL}
    depends_on:
      - db
      - redis
    restart: always
    deploy:
      replicas: 2 # 2 Worker-Instanzen für Parallelverarbeitung

  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: always
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 10s
      timeout: 3s
      retries: 3

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_DB=experimenta
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./backups:/backups
    restart: always
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U ${DB_USER}']
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:
    driver: local
  redis_data:
    driver: local

8. CI/CD Pipeline

8.1 GitLab CI/CD Flow

┌──────────────────────────────────────────────────────────────┐
│                      GitLab CI/CD Pipeline                    │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  Stage 1: Build                                              │
│  ├─ Install Dependencies (pnpm install)                      │
│  ├─ Build Nuxt App (npm run build)                           │
│  ├─ Build Docker Image                                       │
│  └─ Push to Container Registry                               │
│                                                              │
│  Stage 2: Test                                               │
│  ├─ Lint (ESLint + Prettier)                                 │
│  ├─ Unit Tests (Vitest)                                      │
│  ├─ Integration Tests                                        │
│  └─ E2E Tests (Playwright) - on Staging                      │
│                                                              │
│  Stage 3: Deploy Staging (Auto)                              │
│  ├─ Pull Docker Image                                        │
│  ├─ Run Database Migrations                                  │
│  ├─ Deploy to Staging Environment                            │
│  └─ Smoke Tests                                              │
│                                                              │
│  Stage 4: Deploy Production (Manual)                         │
│  ├─ Manual Approval Required                                 │
│  ├─ Pull Docker Image                                        │
│  ├─ Run Database Migrations                                  │
│  ├─ Blue-Green Deployment                                    │
│  │  ├─ Start new container (green)                           │
│  │  ├─ Health check                                          │
│  │  ├─ Switch traffic to green                               │
│  │  └─ Stop old container (blue)                             │
│  └─ Post-deployment Tests                                    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

8.2 GitLab CI Configuration

# .gitlab-ci.yml
stages:
  - build
  - test
  - deploy_staging
  - deploy_production

variables:
  DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  DOCKER_IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest

build:
  stage: build
  image: node:20-alpine
  script:
    - pnpm install --frozen-lockfile
    - pnpm run build
    - docker build -t $DOCKER_IMAGE .
    - docker tag $DOCKER_IMAGE $DOCKER_IMAGE_LATEST
    - docker push $DOCKER_IMAGE
    - docker push $DOCKER_IMAGE_LATEST
  only:
    - main
    - develop

test:
  stage: test
  image: node:20-alpine
  script:
    - pnpm install --frozen-lockfile
    - pnpm run lint
    - pnpm run test
  coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

deploy_staging:
  stage: deploy_staging
  script:
    - ssh deployer@staging.experimenta.science "
      docker pull $DOCKER_IMAGE &&
      cd /opt/experimenta &&
      docker-compose -f docker-compose.staging.yml down &&
      docker-compose -f docker-compose.staging.yml up -d
      "
  environment:
    name: staging
    url: https://staging.my.experimenta.science
  only:
    - develop

deploy_production:
  stage: deploy_production
  script:
    - ssh deployer@my.experimenta.science "
      docker pull $DOCKER_IMAGE &&
      cd /opt/experimenta &&
      ./scripts/blue-green-deploy.sh $DOCKER_IMAGE
      "
  environment:
    name: production
    url: https://my.experimenta.science
  when: manual
  only:
    - main

9. Monitoring & Observability

9.1 Logging-Strategie

Strukturiertes Logging:

// server/utils/logger.ts
import pino from 'pino'

export const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => {
      return { level: label }
    },
  },
  timestamp: pino.stdTimeFunctions.isoTime,
})

// Usage
logger.info({ userId: '123', action: 'checkout' }, 'User completed checkout')
logger.error({ err, userId: '123' }, 'Payment failed')

Log-Aggregation (Optional):

  • Loki + Grafana
  • Elasticsearch + Kibana
  • CloudWatch Logs (wenn auf AWS)

9.2 Error Tracking (Sentry)

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/sentry'],
  sentry: {
    dsn: process.env.SENTRY_DSN,
    environment: process.env.NODE_ENV,
    tracing: {
      tracesSampleRate: 0.1, // 10% of transactions
    },
  },
})

9.3 Performance Monitoring

Metrics to Track:

  • Response Time (p50, p95, p99)
  • Error Rate
  • Request Rate
  • Database Query Time
  • Cache Hit Rate (if Redis)
  • Active Users

Tools:

  • Sentry Performance Monitoring
  • Grafana + Prometheus (self-hosted)
  • New Relic (commercial)

10. Backup & Disaster Recovery

10.1 Backup-Strategie

Datenbank Backups:

# Cron Job: Täglich um 2 Uhr
0 2 * * * docker exec experimenta-db pg_dump -U user experimenta > /backups/db-$(date +\%Y\%m\%d).sql

Retention Policy:

  • Täglich: 7 Tage
  • Wöchentlich: 4 Wochen
  • Monatlich: 12 Monate

Backup-Speicherort:

  • Lokal: /backups (auf Host)
  • Remote: Hetzner Storage Box (SFTP)
  • Optional: AWS S3 / Backblaze B2

10.2 Disaster Recovery Plan

RTO (Recovery Time Objective): 4 Stunden RPO (Recovery Point Objective): 24 Stunden (tägliche Backups)

Recovery Steps:

  1. Neuen Server bereitstellen (Proxmox oder Hetzner Cloud)
  2. Docker & Docker Compose installieren
  3. PostgreSQL Container starten
  4. Letztes Backup wiederherstellen
  5. App Container starten
  6. DNS auf neuen Server umstellen
  7. SSL-Zertifikat neu ausstellen (Let's Encrypt)

11. Skalierungsstrategie

11.1 Vertikale Skalierung (Short-term)

  • Mehr CPU/RAM für Server
  • Upgrade: von 4 Cores / 8GB auf 8 Cores / 16GB
  • PostgreSQL Tuning (shared_buffers, work_mem)

11.2 Horizontale Skalierung (Long-term)

Load Balancer:

                    ┌────────────────┐
                    │  Load Balancer │
                    │   (Nginx)      │
                    └───────┬────────┘
                            │
            ┌───────────────┼───────────────┐
            │               │               │
     ┌──────▼─────┐  ┌──────▼─────┐  ┌──────▼─────┐
     │  App       │  │  App       │  │  App       │
     │  Instance 1│  │  Instance 2│  │  Instance 3│
     └──────┬─────┘  └──────┬─────┘  └──────┬─────┘
            │               │               │
            └───────────────┼───────────────┘
                            │
                     ┌──────▼─────┐
                     │ PostgreSQL │
                     │  (Primary) │
                     └──────┬─────┘
                            │
                     ┌──────▼─────┐
                     │ PostgreSQL │
                     │  (Replica) │
                     └────────────┘

Schritte:

  1. Mehrere App-Instanzen (Docker Replicas)
  2. PostgreSQL Read Replicas
  3. Redis für Session-Storage (shared state)
  4. CDN für Static Assets (Cloudflare)

12. Zusammenfassung

Key Architectural Decisions:

Aspekt Entscheidung Begründung
Framework Nuxt 4 Full-Stack, SSR, TypeScript
UI shadcn-nuxt Maximale Flexibilität
Database PostgreSQL ACID, bewährt für E-Commerce
ORM Drizzle Performance, TypeScript-first
Auth Cidaas + Local DB Unternehmens-Standard + Flexibilität
Payment PayPal Weit verbreitet, einfach
i18n @nuxtjs/i18n Deutsch + English, SEO-optimiert
Deployment Docker Portabilität, Konsistenz
Hosting Hetzner Kostengünstig, EU-basiert
CI/CD GitLab Bereits intern vorhanden

Ende des Dokuments