# 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 ``` **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 // 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 ``` **SEO Benefits:** - Automatische Routes: `/produkte` (de), `/en/products` (en) - Automatische `` 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:** --- ## 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 ``` **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**