Files
my2/docs/TECH_STACK.md
Bastian Masanek 6e50ec7034 Init
2025-10-30 08:24:44 +01:00

1699 lines
40 KiB
Markdown

# Tech Stack Dokumentation
## my.experimenta.science
**Version:** 1.0
**Datum:** 28. Oktober 2025
---
## Übersicht
Dieser Dokument beschreibt den vollständigen Tech Stack für die my.experimenta.science E-Commerce App und begründet die getroffenen Entscheidungen.
---
## 1. Frontend Framework
### Nuxt 4
**Entscheidung:** Nuxt 4 (Vue.js Meta-Framework)
**Begründung:**
- **Vue 3 Composition API:** Moderne, reaktive Architektur
- **Server-Side Rendering (SSR):** Bessere Performance & SEO
- **File-based Routing:** Intuitive Projektstruktur
- **Auto-Import:** Weniger Boilerplate-Code
- **Hybrid Rendering:** Flexible Rendering-Modi (SSR, SSG, CSR)
- **TypeScript Support:** Native TypeScript-Integration
- **Server APIs:** Backend & Frontend in einem Projekt
- **Active Development:** Nuxt 4 ist die neueste Version mit langfristigem Support
**Alternativen:**
- **Next.js (React):** Großartig, aber Team bevorzugt Vue
- **SvelteKit:** Weniger Ecosystem, kleinere Community
- **Remix:** Noch jünger, weniger etabliert
**Version:** `^4.0.0`
---
## 2. UI Framework
### Optionen: Nuxt UI vs. shadcn-vue
#### Option A: Nuxt UI
**Pro:**
- Speziell für Nuxt entwickelt
- Out-of-the-box Integration
- Headless UI Components
- Tailwind CSS basiert
- Dark Mode Support
- Accessibility eingebaut
- Icons & Colors mitgeliefert
- Nuxt-native (optimiert)
**Contra:**
- Kleinere Component-Bibliothek
- Weniger flexibel als shadcn
- Stärkere Meinung zum Styling
**Best for:** Schneller Start, weniger Customization
---
#### Option B: shadcn-nuxt
**Pro:**
- Copy-Paste Components (volle Kontrolle)
- Radix Vue (hochwertige Primitives)
- Vollständig anpassbar
- Kein Package Lock-in
- Große Component-Bibliothek
- Sehr flexibel
- Hervorragende TypeScript-Support
- **Nuxt-native:** Offizielle Nuxt-Integration
**Contra:**
- Manuelles Hinzufügen von Components via CLI
- Mehr Setup als Nuxt UI
**Best for:** Maximale Anpassung, langfristige Wartbarkeit
---
### Empfehlung: shadcn-nuxt
**Grund:**
- Corporate Design Integration benötigt maximale Flexibilität
- Copy-Paste-Ansatz = kein Vendor Lock-in
- Vollständige Kontrolle über Komponenten
- Einfacher zu debuggen (Code liegt im Projekt)
- Bessere Anpassung an experimenta Styleguide möglich
- Offizielle Nuxt-Module Integration
**Installation:**
```bash
npx nuxi@latest module add shadcn-nuxt
npx shadcn-nuxt@latest init
```
**Official Docs:** https://www.shadcn-vue.com/docs/installation/nuxt.html
**Dependencies:**
- `shadcn-nuxt` - Nuxt module
- `radix-vue` - UI Primitives (auto-installed)
- `tailwindcss` - Utility-first CSS
- `class-variance-authority` - Varianten-Management
- `tailwind-merge` - Classname-Merging
- `clsx` - Conditional Classes
---
## 3. Styling
### Tailwind CSS
**Entscheidung:** Tailwind CSS v4 (Beta)
**Begründung:**
- **Utility-First:** Schnelle Entwicklung
- **Responsive Design:** Mobile-first eingebaut
- **Customization:** Einfache Theme-Anpassung
- **Performance:** PurgeCSS integriert (kleine Bundles)
- **Developer Experience:** Hervorragende IDE-Integration (Intellisense)
- **Konsistenz:** Design-Tokens erzwingen Konsistenz
**Corporate Design Integration:**
```javascript
// tailwind.config.js
export default {
theme: {
extend: {
colors: {
experimenta: {
primary: '#YOUR_PRIMARY_COLOR',
secondary: '#YOUR_SECONDARY_COLOR',
// ... weitere Farben aus Styleguide
},
},
fontFamily: {
sans: ['Your Corporate Font', 'sans-serif'],
},
},
},
}
```
---
## 4. Datenbank
### PostgreSQL
**Entscheidung:** PostgreSQL 16+
**Begründung:**
- **Relational:** Klare Datenstrukturen für E-Commerce
- **ACID-Compliant:** Transaktionssicherheit für Bestellungen
- **Robust:** Bewährte Technologie
- **Performance:** Exzellent für Read-Heavy Workloads
- **JSON Support:** Flexibilität für metadata
- **Full-Text Search:** Native Suchfunktionen
- **Extensions:** PostGIS, pg_trgm, etc.
- **Open Source:** Keine Lizenzkosten
**Hosting:**
- Docker Container auf Hetzner Proxmox
- Persistent Volume für Daten
- Automated Backups (tägliche Snapshots)
**Connection Pooling:**
- `pg_bouncer` für effizientes Connection Management
---
## 5. ORM
### Drizzle ORM
**Entscheidung:** Drizzle ORM
**Begründung:**
- **TypeScript-First:** Native TypeScript ohne Code-Generation
- **Performance:** Minimal Runtime Overhead
- **SQL-Like Syntax:** Leicht zu lernen für SQL-Kenner
- **Type Safety:** Compile-time Checks
- **PostgreSQL-Optimiert:** Nutzt PostgreSQL-Features voll aus
- **Drizzle Studio:** GUI für DB-Verwaltung
- **Migrations:** Einfache Schema-Verwaltung
- **Lightweight:** Keine schwere Runtime wie Prisma
**Alternativen:**
- **Prisma:** Mehr Overhead, langsamer, aber größere Community
- **TypeORM:** Veraltet, weniger TypeScript-fokussiert
- **Kysely:** Gut, aber weniger Features
**Setup:**
```typescript
// drizzle.config.ts
export default {
schema: './server/database/schema.ts',
out: './server/database/migrations',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL,
},
}
```
**Version:** `^0.29.0`
---
## 5.1 Data Storage Design: Billing Address
### Entscheidung: Structured Columns in Users Table
**Problem:**
Should we store billing addresses in the user profile or only in orders?
**Entscheidung:**
Store billing address directly in the `users` table as structured columns (not JSONB).
**Begründung:**
**Pro Speicherung im Profil:**
- **UX-Verbesserung:** Wiederkehrende Kunden müssen Daten nicht erneut eingeben
- **Conversion-Rate:** Reduziert Checkout-Abbrüche (10-20% lt. Branchenstandard)
- **Mobile-First:** Schnellerer Checkout ist kritisch für mobile Nutzer
- **Geschäftsmodell:** Jahreskarten = wiederkehrende Kunden (jährliche Erneuerung)
- **E-Commerce Best Practice:** Amazon, PayPal, alle großen Shops speichern Adressen
- **DSGVO-konform:** Rechtsgrundlage "Vertragserfüllung" (Art. 6(1)(b))
**Strukturierte Spalten vs. JSONB:**
| Aspekt | Structured Columns | JSONB Column |
| ----------------- | -------------------------------- | ------------------------ |
| **Type Safety** | ✅ Voll typsicher | ⚠️ Zod-Validierung nötig |
| **Queries** | ✅ Einfach zu filtern/sortieren | ❌ Komplexere Queries |
| **Performance** | ✅ Besser indexierbar | ⚠️ Langsamer |
| **Flexibility** | ❌ Schema-Änderungen = Migration | ✅ Flexibel |
| **X-API Mapping** | ✅ Direkt mappbar | ⚠️ Parsing nötig |
**Entscheidung:** Strukturierte Spalten
**Felder:**
```typescript
export const users = pgTable('users', {
// ... existing fields
// Billing address (optional - filled during checkout)
salutation: varchar('salutation', { length: 20 }), // 'male', 'female', 'other'
dateOfBirth: date('date_of_birth'), // Required for annual passes
street: varchar('street', { length: 100 }),
postCode: varchar('post_code', { length: 20 }),
city: varchar('city', { length: 100 }),
countryCode: varchar('country_code', { length: 10 }).default('DE'),
})
```
**UX Flow:**
1. **First Checkout:** User fills address form, checkbox "Save for future orders" is pre-checked
2. **Subsequent Checkouts:** Form pre-filled, editable before submission
3. **Profile Management:** `/profil/adresse` page to view/edit/delete address
**Migration Strategy:**
- Address fields are optional in DB schema
- Required validation only at checkout (Zod)
- Existing users: Backfill from most recent order (optional)
**Privacy:**
- Opt-out via unchecking checkbox at checkout
- Can be deleted in profile settings
- Included in GDPR data export/deletion
---
## 5.2 Queue System: BullMQ
### Entscheidung: BullMQ (MIT License)
**Problem:**
Asynchrone Verarbeitung von zeitaufwendigen Operationen (X-API Order Submission, NAV Product Sync).
**Entscheidung:**
BullMQ als Queue-System mit Redis als Backend.
**Begründung:**
**Pro BullMQ:**
- **TypeScript-First:** Native TypeScript-Integration, perfekt für Nuxt
- **Einfache Integration:** npm package, kein separater Broker-Service
- **Redis-basiert:** Nutzt Redis (für Sessions/Caching eh sinnvoll)
- **MIT License:** Kostenlos, echtes Open Source
- **Features:**
- Retry Logic mit exponential backoff
- Priority Queues
- Delayed Jobs
- Rate Limiting
- Dead Letter Queue
- Flow Control (Job Chains)
- **Monitoring:** BullBoard (kostenlos) für Web-Dashboard
- **Performance:** 10.000+ Jobs/Sekunde (ausreichend)
- **Community:** Große, aktive Community
- **Production-Ready:** Von vielen Unternehmen eingesetzt
**Alternativen:**
| Lösung | Pro | Contra | Empfehlung |
| ------------------ | ------------------------ | ----------------------------- | ------------------- |
| **BullMQ** | TypeScript, einfach, MIT | Nur Node.js | ✅ Empfohlen |
| **RabbitMQ** | Robust, Multi-Language | Zu komplex, separater Service | ❌ Overkill |
| **Database Queue** | Keine neue Dependency | Weniger Features, Polling | ❌ Nicht skalierbar |
| **Kafka** | Extreme Skalierung | Viel zu komplex | ❌ Overkill |
**Für my.experimenta.science ist BullMQ ideal:**
- ✅ Nuxt-only (Node.js)
- ✅ Einfache Use Cases (Order → X-API, Product Sync)
- ✅ Moderater Traffic (~10-500 Orders/Tag)
- ✅ Redis eh nützlich (Sessions, Caching)
### Use Cases
#### 1. Order Submission (Async)
```typescript
// server/queues/orderQueue.ts
import { Queue } from 'bullmq'
export const orderQueue = new Queue('x-api-orders', {
connection: { host: 'redis', port: 6379 },
})
// Add job after PayPal success
export async function queueOrderSubmission(orderId: string) {
await orderQueue.add(
'submit-order',
{ orderId },
{
attempts: 5,
backoff: { type: 'exponential', delay: 2000 },
removeOnComplete: 1000,
removeOnFail: false, // Keep for manual review
}
)
}
```
#### 2. Product Sync (Async)
```typescript
// server/queues/productQueue.ts
export const productSyncQueue = new Queue('product-sync', {
connection: { host: 'redis', port: 6379 },
})
// API endpoint: POST /api/erp/products
export default defineEventHandler(async (event) => {
const product = await readBody(event)
// Validate & queue (instant response!)
await productSyncQueue.add('sync-product', product, {
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
})
return { status: 'accepted', queued: true }
})
```
#### 3. Worker Implementation
```typescript
// server/workers/orderWorker.ts
import { Worker } from 'bullmq'
export const orderWorker = new Worker(
'x-api-orders',
async (job) => {
const { orderId } = job.data
// 1. Fetch order from DB
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: { items: true, user: true },
})
// 2. Transform & submit to X-API
const payload = transformOrderToXAPI(order)
await submitToXAPI(payload)
// 3. Update status
await db.update(orders).set({ status: 'completed' }).where(eq(orders.id, orderId))
},
{
connection: { host: 'redis', port: 6379 },
concurrency: 5,
limiter: { max: 10, duration: 1000 },
}
)
```
### Monitoring: BullBoard
```typescript
// server/api/admin/queues.ts
import { createBullBoard } from '@bull-board/api'
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'
import { NuxtAdapter } from '@bull-board/nuxt'
const serverAdapter = new NuxtAdapter()
createBullBoard({
queues: [new BullMQAdapter(orderQueue), new BullMQAdapter(productSyncQueue)],
serverAdapter,
})
// Access: http://localhost:3000/admin/queues
```
**Dashboard Features (kostenlos):**
- Real-time Job-Status
- Manual Retry
- Job Details & Logs
- Queue-Metriken
- Fehler-Analyse
### BullMQ Dependencies
```json
{
"dependencies": {
"bullmq": "^5.0.0",
"@bull-board/api": "^5.0.0",
"@bull-board/nuxt": "^5.0.0"
}
}
```
**Alle MIT License - kostenlos!**
### BullMQ Kosten
**BullMQ:** €0 (MIT License)
**BullMQ Pro:** €99-299/Monat (nicht nötig!)
**Taskforce.sh:** €49-199/Monat (BullBoard reicht!)
**Für dich:** €0 - alles kostenlos!
---
## 5.3 In-Memory Store: Redis
### Entscheidung: Redis 7
**Verwendung:**
1. **BullMQ Queue Storage** (Hauptzweck)
2. **Session Storage** (HTTP-only cookies)
3. **Caching** (Produkt-Daten, etc.)
4. **Rate Limiting** (Brute-Force-Schutz)
**Begründung:**
- ✅ Persistenz mit AOF + RDB (keine Datenverluste)
- ✅ Schnell (In-Memory)
- ✅ Bewährt (Millionen Installationen)
- ✅ Docker-ready (redis:7-alpine)
**Lizenz-Situation:**
**Redis License (seit März 2024):**
- RSALv2 (Redis Source Available License)
- SSPL (Server Side Public License)
**Dein Use Case:**
**Vollständig legal und kostenlos!**
**Was du machst:**
- Redis selbst hosten (in docker-compose)
- Für interne App-Funktionen nutzen
- Nicht als Hosting-Service verkaufen
**Keine Lizenzkosten, keine Probleme!**
**Was verboten wäre:**
- ❌ "experimenta Redis Cloud" Service anbieten
- ❌ Managed Redis Hosting verkaufen
#### Alternative: Valkey (100% Open Source)
Falls Lizenz-Unsicherheit:
- **Valkey:** Redis-Fork (Linux Foundation)
- **Lizenz:** BSD-3-Clause (echtes Open Source)
- **Kompatibilität:** 100% Drop-in Replacement
- **Docker:** `valkey/valkey:7.2-alpine`
**Empfehlung:** Start mit Redis, später optional auf Valkey migrieren.
### Persistenz-Konfiguration
```yaml
# docker-compose.yml
services:
redis:
image: redis:7-alpine
command: >
redis-server
--appendonly yes
--appendfsync everysec
--save 60 1000
--save 300 100
--save 900 1
volumes:
- redis_data:/data
restart: unless-stopped
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 10s
volumes:
redis_data:
```
**Garantie:** Max. 1 Sekunde Datenverlust bei Server-Crash.
**Erklärung:**
- `--appendonly yes`: AOF aktiviert
- `--appendfsync everysec`: Jede Sekunde auf Disk
- `--save`: RDB Snapshots als Backup
### Redis Dependencies
```json
{
"dependencies": {
"ioredis": "^5.3.0" // Redis client für Node.js
}
}
```
### Redis Kosten
**Redis/Valkey:** €0 (Open Source)
**Einzige Kosten:** Server-RAM (~512MB für Redis)
---
## 6. Authentifizierung
### Cidaas (OIDC/OAuth2)
**Entscheidung:** Cidaas Integration mit Custom UI
**Begründung:**
- **Unternehmensvorgabe:** Bereits im Einsatz bei experimenta
- **OIDC/OAuth2:** Standard-Protokolle
- **Compliance:** DSGVO-konform (EU-basiert, Widas)
- **Features:** MFA, Social Login (optional)
**Architektur:**
- **Cidaas:** Nur für Authentifizierung (User Identity)
- **Lokale DB:** User-Profile & Rollen
- **Custom UI:** Eigene Login/Registrierungs-Masken im experimenta Design
**Integration:**
```typescript
// Nuxt OAuth Module oder Custom Implementation
import { createOAuth2Client } from '@nuxt/oauth'
export default defineNuxtConfig({
oauth: {
cidaas: {
clientId: process.env.CIDAAS_CLIENT_ID,
clientSecret: process.env.CIDAAS_CLIENT_SECRET,
authorizeUrl: 'https://experimenta.cidaas.de/authorize',
tokenUrl: 'https://experimenta.cidaas.de/token',
userInfoUrl: 'https://experimenta.cidaas.de/userinfo',
},
},
})
```
**Libraries:**
- `nuxt-auth-utils` oder Custom OAuth2 Implementation
- `jose` für JWT-Validierung
---
## 7. Payment Gateway
### PayPal (MVP)
**Entscheidung:** PayPal Checkout Integration
**Begründung:**
- **Weit verbreitet:** Hohe Akzeptanz in Deutschland
- **Einfache Integration:** Offizielle SDK & Dokumentation
- **Buyer Protection:** Käuferschutz erhöht Vertrauen
- **Schnell:** Weniger Daten als Kreditkarte erforderlich
- **Mobile-optimiert:** PayPal App-Integration
**SDK:**
```typescript
// PayPal JavaScript SDK
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID&currency=EUR"></script>
```
**Server-Side:**
- PayPal REST API für Order Creation & Capture
- Webhook für Payment-Benachrichtigungen
**Post-MVP:**
- Stripe (Kreditkarte, Apple Pay, Google Pay)
- SEPA-Lastschrift
- Klarna / Ratenzahlung
---
## 8. External API Integration: X-API
### X-API Client (Order Submission)
**Entscheidung:** Native `fetch` with retry logic
**Purpose:** Submit completed orders to NAV ERP via X-API `/shopware/order` endpoint
**Begründung:**
- **Native:** No additional dependencies (fetch is built-in)
- **TypeScript:** Full type safety with Zod validation
- **Retry Logic:** Custom implementation with exponential backoff
- **Error Handling:** Comprehensive error tracking and logging
**Environments:**
| Environment | URL |
| ----------- | ----------------------------------------- |
| Development | `https://x-api-dev.experimenta.science` |
| Staging | `https://x-api-stage.experimenta.science` |
| Production | `https://x-api.experimenta.science` |
### X-API Authentication
**Entscheidung:** HTTP Basic Authentication
**Begründung:**
- **Standard:** HTTP Basic Auth ist ein bewährter Standard (RFC 7617)
- **Einfach:** Keine komplexe OAuth2-Integration nötig
- **Secure:** HTTPS-verschlüsselte Übertragung der Credentials
- **Kompatibel:** Unterstützt von allen HTTP-Clients
**Implementierung:**
```typescript
// server/utils/xapi.ts
const config = useRuntimeConfig()
// Prepare Basic Auth header
const authString = Buffer.from(`${config.xApiUsername}:${config.xApiPassword}`).toString('base64')
const response = await fetch(`${config.xApiBaseUrl}/shopware/order`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${authString}`,
},
body: JSON.stringify(payload),
})
```
**Sicherheitshinweise:**
- ✅ Credentials **nur** über HTTPS übertragen
- ✅ Credentials **niemals** im Code hardcoden
- ✅ Separate Service-Accounts pro Environment (dev/stage/prod)
- ✅ Credentials regelmäßig rotieren (alle 90 Tage)
- ✅ In Production: Docker Secrets verwenden statt Environment Variables
**Environment Variables:**
```bash
# .env
X_API_BASE_URL=https://x-api-dev.experimenta.science
X_API_USERNAME=shop_user_dev
X_API_PASSWORD=xxx
```
**Docker Secrets (Production):**
```yaml
# docker-compose.yml
services:
app:
secrets:
- xapi_username
- xapi_password
environment:
X_API_USERNAME_FILE: /run/secrets/xapi_username
X_API_PASSWORD_FILE: /run/secrets/xapi_password
secrets:
xapi_username:
file: ./secrets/xapi_username.txt
xapi_password:
file: ./secrets/xapi_password.txt
```
**Implementation:**
```typescript
// server/utils/xapi.ts
import { z } from 'zod'
// Zod schema for X-API request validation
const XAPIOrderSchema = z.object({
shopPOSOrder: z.object({
documentType: z.enum(['Order', 'Credit Memo']),
externalDocumentNo: z.string().max(35),
externalPOSReceiptNo: z.string().max(250),
salesChannel: z.enum(['Shop', 'POS']),
shoppingCartCompletion: z.string().datetime(),
visitType: z.enum(['Private', 'Institution']),
invoiceDiscountValue: z.number().int(),
invoiceDiscPct: z.number().int(),
amountIncludingVAT: z.number().int(), // Cents!
language: z.enum(['DEU', 'ENU']),
salesLine: z.array(
z.object({
type: z.enum(['', 'Item']),
lineNo: z.string().max(30),
no: z.string().max(20),
variantCode: z.string().max(10),
description: z.string().max(50),
description2: z.string().max(50),
quantity: z.number().int(),
unitPrice: z.number().int(), // Cents!
vatPct: z.number(),
lineAmountIncludingVAT: z.number(),
lineDiscountPct: z.number(),
lineDiscountAmount: z.number().int(),
seasonTicketCode: z.string().max(20),
couponCode: z.string().max(20),
visitorCategory: z.enum(['00', '10', '40', '50', '60', '99', '120', '128']),
ticketPriceType: z.string().max(50),
ticketCode: z.string().max(250),
annualPass: z
.object({
salutationCode: z.enum(['K_ANGABE', 'FRAU', 'HERR']),
firstName: z.string().max(30),
lastName: z.string().max(30),
streetAndHouseNumber: z.string().max(50),
postCode: z.string().max(20),
city: z.string().max(30),
countryCode: z.string().max(10),
dateOfBirth: z.string(),
eMail: z.string().email(),
validFrom: z.string(),
})
.optional(),
})
),
payment: z.array(
z.object({
paymentEntryNo: z.number().int(),
amount: z.number().int(), // Cents!
paymentType: z.enum(['Shop PayPal', 'Shop Credit Card', 'Shop Direct Debit']),
createdOn: z.string().datetime(),
reference: z.string().max(50),
paymentId: z.string().max(50),
})
),
personContact: z.object({
experimentaAccountID: z.string().uuid(),
eMail: z.string().email(),
salutationCode: z.enum(['K_ANGABE', 'FRAU', 'HERR']),
jobTitle: z.string().max(30),
firstName: z.string().max(30),
lastName: z.string().max(30),
streetAndHouseNumber: z.string().max(50),
postCode: z.string().max(20),
city: z.string().max(30),
countryCode: z.string().max(10),
phoneNumber: z.string().max(30),
guest: z.boolean(),
}),
}),
})
export type XAPIOrderPayload = z.infer<typeof XAPIOrderSchema>
// Submit order to X-API with retry logic
export async function submitOrderToXAPI(payload: XAPIOrderPayload) {
// Validate payload
const validatedPayload = XAPIOrderSchema.parse(payload)
const config = useRuntimeConfig()
const baseURL = config.xApiBaseUrl // From environment
// Retry logic with exponential backoff
const maxRetries = 3
const retryDelays = [1000, 3000, 9000] // 1s, 3s, 9s
// Prepare Basic Auth header
const authString = Buffer.from(`${config.xApiUsername}:${config.xApiPassword}`).toString('base64')
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(`${baseURL}/shopware/order`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${authString}`,
},
body: JSON.stringify(validatedPayload),
})
if (!response.ok) {
throw new Error(`X-API returned ${response.status}: ${await response.text()}`)
}
const result = await response.json()
// Log success
console.info(`Order submitted to X-API successfully`, {
orderNo: payload.shopPOSOrder.externalDocumentNo,
attempt: attempt + 1,
})
return result
} catch (error) {
// Log attempt
console.error(`X-API submission failed (attempt ${attempt + 1}/${maxRetries})`, {
orderNo: payload.shopPOSOrder.externalDocumentNo,
error,
})
// If this was the last attempt, throw
if (attempt === maxRetries - 1) {
throw error
}
// Wait before retry
await new Promise((resolve) => setTimeout(resolve, retryDelays[attempt]))
}
}
}
// Helper: Transform order from DB to X-API format
export function transformOrderToXAPI(order: Order, user: User): XAPIOrderPayload {
return {
shopPOSOrder: {
documentType: 'Order',
externalDocumentNo: order.orderNumber,
externalPOSReceiptNo: '',
salesChannel: 'Shop',
shoppingCartCompletion: order.createdAt.toISOString(),
visitType: 'Private',
invoiceDiscountValue: 0,
invoiceDiscPct: 0,
amountIncludingVAT: Math.round(order.totalAmount * 100), // EUR → Cents
language: 'DEU',
salesLine: order.items.map((item, index) => ({
type: 'Item',
lineNo: String((index + 1) * 10000),
no: item.product.navProductId,
variantCode: item.product.variantCode || '',
description: item.product.name.substring(0, 50),
description2: item.product.description?.substring(0, 50) || '',
quantity: item.quantity,
unitPrice: Math.round(item.priceSnapshot * 100), // EUR → Cents
vatPct: 7,
lineAmountIncludingVAT: Math.round(item.quantity * item.priceSnapshot * 100),
lineDiscountPct: 0,
lineDiscountAmount: 0,
seasonTicketCode: '',
couponCode: '',
visitorCategory: '120',
ticketPriceType: 'Jahreskarte Makerspace',
ticketCode: generateTicketCode(),
annualPass: {
salutationCode: mapSalutation(user.salutation),
firstName: user.firstName,
lastName: user.lastName,
streetAndHouseNumber: user.address.street,
postCode: user.address.postCode,
city: user.address.city,
countryCode: user.address.countryCode || 'DE',
dateOfBirth: user.dateOfBirth,
eMail: user.email,
validFrom: new Date().toISOString().split('T')[0], // YYYY-MM-DD
},
})),
payment: [
{
paymentEntryNo: 10000,
amount: Math.round(order.totalAmount * 100), // EUR → Cents
paymentType: 'Shop PayPal',
createdOn: order.paymentCompletedAt.toISOString(),
reference: order.paymentId,
paymentId: order.paymentId,
},
],
personContact: {
experimentaAccountID: user.experimentaId,
eMail: user.email,
salutationCode: mapSalutation(user.salutation),
jobTitle: user.jobTitle || '',
firstName: user.firstName,
lastName: user.lastName,
streetAndHouseNumber: order.billingAddress.street,
postCode: order.billingAddress.postCode,
city: order.billingAddress.city,
countryCode: order.billingAddress.countryCode || 'DE',
phoneNumber: user.phone || '',
guest: false,
},
},
}
}
```
**Environment Variables:**
```bash
# .env (Development/Staging)
X_API_BASE_URL=https://x-api-dev.experimenta.science
X_API_USERNAME=shop_user_dev
X_API_PASSWORD=xxx
# .env.stage
# X_API_BASE_URL=https://x-api-stage.experimenta.science
# X_API_USERNAME=shop_user_stage
# X_API_PASSWORD=xxx
# Production: Use Docker Secrets (see X-API Authentication section above)
```
**Alternative:** Use `ofetch` (Nuxt's built-in fetch wrapper) for better error handling:
```typescript
import { ofetch } from 'ofetch'
const xapi = ofetch.create({
baseURL: config.xApiBaseUrl,
retry: 3,
retryDelay: 1000,
})
```
---
## 9. Backend & APIs
### Nuxt Server APIs
**Entscheidung:** Nuxt Server Routes (Nitro)
**Begründung:**
- **Monorepo:** Backend & Frontend in einem Projekt
- **Serverless-Ready:** Kann als Serverless Functions deployed werden
- **Auto-Import:** Shared Utils zwischen Client & Server
- **TypeScript:** Type-Safety über Frontend/Backend-Grenze
- **Performance:** Nitro Engine (schneller als Express)
- **Flexible Deployment:** Node.js, Deno, Workers, Docker
**API Struktur:**
```
server/
api/
auth/
login.post.ts
callback.get.ts
products/
index.get.ts
[id].get.ts
cart/
items.post.ts
items/[id].patch.ts
orders/
index.post.ts
[id].get.ts
payment/
paypal/
create.post.ts
capture.post.ts
webhook.post.ts
erp/
products.post.ts
```
---
## 9. Internationalization (i18n)
### @nuxtjs/i18n
**Entscheidung:** @nuxtjs/i18n (Official Nuxt module)
**Begründung:**
- **Offizielles Nuxt-Modul:** Beste Integration mit Nuxt 4
- **Vue I18n powered:** Industry-Standard für Vue.js
- **SEO-optimiert:** Automatische locale routes, hreflang tags
- **SSR/SSG Support:** Perfekt für Nuxt's rendering modes
- **Lazy Loading:** Sprachdateien on-demand laden
- **TypeScript Support:** Vollständig typisiert
- **Auto-Import:** Composables wie `useI18n()`, `$t()` direkt verfügbar
**Supported Languages:**
- 🇩🇪 **Deutsch** (Primary, default locale)
- 🇬🇧 **English** (Secondary, für internationale Besucher)
**Installation:**
```bash
npx nuxi@latest module add i18n
```
**Configuration:**
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
locales: [
{
code: 'de',
iso: 'de-DE',
name: 'Deutsch',
file: 'de-DE.json',
},
{
code: 'en',
iso: 'en-US',
name: 'English',
file: 'en-US.json',
},
],
defaultLocale: 'de',
strategy: 'prefix_except_default', // URLs: /produkte (de), /en/products (en)
langDir: 'locales',
lazy: true,
detectBrowserLanguage: {
useCookie: true,
cookieKey: 'i18n_redirected',
redirectOn: 'root',
},
},
})
```
**Project Structure:**
```
/locales
├── de-DE.json # German translations
└── en-US.json # English translations
```
**Translation Files Example:**
```json
// locales/de-DE.json
{
"nav": {
"home": "Startseite",
"products": "Produkte",
"cart": "Warenkorb"
},
"product": {
"addToCart": "In den Warenkorb",
"price": "Preis",
"description": "Beschreibung"
},
"checkout": {
"title": "Zur Kasse",
"total": "Gesamt",
"payNow": "Jetzt bezahlen"
}
}
// locales/en-US.json
{
"nav": {
"home": "Home",
"products": "Products",
"cart": "Cart"
},
"product": {
"addToCart": "Add to Cart",
"price": "Price",
"description": "Description"
},
"checkout": {
"title": "Checkout",
"total": "Total",
"payNow": "Pay Now"
}
}
```
**Usage in Components:**
```vue
<template>
<div>
<h1>{{ $t('nav.home') }}</h1>
<p>{{ $t('product.description') }}</p>
<!-- With parameters -->
<p>{{ $t('product.price', { amount: formatPrice(99.0) }) }}</p>
<!-- Language switcher -->
<USelectMenu v-model="locale" :options="availableLocales">
<template #label>
{{ locale === 'de' ? '🇩🇪 Deutsch' : '🇬🇧 English' }}
</template>
</USelectMenu>
</div>
</template>
<script setup lang="ts">
const { locale, t, availableLocales } = useI18n()
// Format price according to locale
const formatPrice = (price: number) => {
return new Intl.NumberFormat(locale.value, {
style: 'currency',
currency: 'EUR',
}).format(price)
}
// Format date according to locale
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat(locale.value, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date)
}
</script>
```
**SEO Benefits:**
- Automatische Routes: `/produkte` (de), `/en/products` (en)
- Automatische `<link rel="alternate" hreflang="de-DE" />` Tags
- Locale-specific meta tags
- Sitemap mit allen Sprachen
**X-API Integration:**
```typescript
// server/utils/xapi.ts
import { useI18n } from '#i18n'
export function transformOrderToXAPI(order: Order, user: User) {
const { locale } = useI18n()
return {
shopPOSOrder: {
// Map locale to X-API language format
language: locale.value === 'de' ? 'DEU' : 'ENU',
// ... rest of payload
},
}
}
```
**Version:** `^8.0.0`
**Official Docs:** <https://i18n.nuxtjs.org/>
---
## 10. Validation & Forms
### Zod + VeeValidate
**Entscheidung:**
- **Zod:** Schema Validation
- **VeeValidate:** Form Handling
**Begründung:**
- **Type Safety:** Zod generiert TypeScript-Types
- **Reusable Schemas:** Validierung auf Client & Server
- **Error Handling:** Strukturierte Error-Messages
- **VeeValidate:** Vue-native Form Library
- **Composition API Support:** Moderne Vue 3 Syntax
**Example:**
```typescript
import { z } from 'zod'
export const checkoutSchema = z.object({
firstName: z.string().min(2),
lastName: z.string().min(2),
email: z.string().email(),
address: z.string().min(5),
})
export type CheckoutForm = z.infer<typeof checkoutSchema>
```
**Dependencies:**
- `zod` - Schema validation
- `vee-validate` - Form handling
- `@vee-validate/zod` - Integration
---
## 10. State Management
### Pinia (Optional)
**Entscheidung:** Pinia für globale State (sparsam einsetzen)
**Begründung:**
- **Vue 3 Native:** Offizielle State Library für Vue
- **TypeScript:** Hervorragende Type-Inference
- **Devtools:** Vue Devtools Integration
- **Composition API:** Moderne Syntax
- **SSR-Ready:** Nuxt Auto-Import
**Verwendung:**
- User Store (Auth-Status)
- Cart Store (Warenkorb)
- Minimal halten, meiste State in Composables
**Alternative:**
- Nuxt Composables (`useState`) für einfache Cases
---
## 11. Deployment & Infrastructure
### Docker
**Entscheidung:** Docker Containerisierung
**Begründung:**
- **Konsistenz:** Gleiche Umgebung überall (Dev, Staging, Prod)
- **Portabilität:** Läuft auf Hetzner, AWS, überall
- **Isolation:** Keine Konflikte mit anderen Services
- **Skalierung:** Einfach mehrere Container starten
**Dockerfile (Multi-Stage Build):**
```dockerfile
# Build Stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production Stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.output ./
EXPOSE 3000
CMD ["node", "server/index.mjs"]
```
**Docker Compose (für Dev):**
```yaml
services:
app:
build: .
ports:
- '3000:3000'
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/experimenta
depends_on:
- db
db:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=experimenta
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
volumes:
postgres_data:
```
---
### Hetzner Cloud
**Hosting:**
- Hetzner Dedicated Server oder VPS
- Proxmox für Virtualisierung
- Docker Container für App & DB
- Nginx/Traefik als Reverse Proxy
- Let's Encrypt für SSL
**Empfohlene Server-Specs (Production):**
- CPU: 4 Cores
- RAM: 8 GB
- Storage: 160 GB SSD
- Backup: Tägliche Snapshots
---
### CI/CD mit GitLab
**Pipeline Stages:**
1. **Build:** Docker Image bauen
2. **Test:** Unit & E2E Tests laufen
3. **Deploy Staging:** Automatisch auf Staging deployen
4. **Deploy Production:** Manuell getriggert (approval)
**Deployment-Optionen:**
**Option A: GitLab Runner auf Server**
- Runner auf Hetzner Server installiert
- Direkter Zugriff zu Docker
- Schnelleres Deployment
**Option B: SSH-Zugriff**
- Runner im GitLab-Server
- Deployment via SSH + rsync/docker commands
- Mehr Isolation
**Empfehlung:** Runner auf Server (Option A)
**.gitlab-ci.yml:**
```yaml
stages:
- build
- test
- deploy
build:
stage: build
script:
- docker build -t experimenta-app:$CI_COMMIT_SHA .
only:
- main
test:
stage: test
script:
- npm ci
- npm run test
deploy_production:
stage: deploy
script:
- docker pull experimenta-app:$CI_COMMIT_SHA
- docker stop experimenta-app || true
- docker rm experimenta-app || true
- docker run -d --name experimenta-app -p 3000:3000 experimenta-app:$CI_COMMIT_SHA
only:
- main
when: manual
```
---
## 12. Testing
### Vitest
**Entscheidung:** Vitest für Unit & Integration Tests
**Begründung:**
- **Vite-Native:** Gleiche Config wie Nuxt
- **Fast:** Parallelisierung, Watch Mode
- **Vue Support:** @vue/test-utils Integration
- **TypeScript:** Native TS-Support
- **API kompatibel mit Jest:** Einfache Migration
**Config:**
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
threshold: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
})
```
---
### Playwright
**Entscheidung:** Playwright für E2E Tests
**Begründung:**
- **Modern:** Neueste Browser-Automation
- **Multi-Browser:** Chrome, Firefox, Safari, Edge
- **Mobile Emulation:** Responsive Testing
- **Fast:** Parallelisierung
- **Debug-Tools:** Hervorragende Dev Experience
**Tests:**
```typescript
// e2e/checkout.spec.ts
import { test, expect } from '@playwright/test'
test('complete checkout flow', async ({ page }) => {
await page.goto('/')
await page.click('text=Makerspace Jahreskarte')
await page.click('text=In den Warenkorb')
await page.click('[aria-label="Warenkorb"]')
await page.click('text=Zur Kasse')
// ... weitere Schritte
})
```
---
## 13. Monitoring & Logging
### Sentry (Optional)
**Entscheidung:** Sentry für Error Tracking
**Begründung:**
- **Real-time Errors:** Sofortige Benachrichtigung
- **Stack Traces:** Detaillierte Error-Informationen
- **User Context:** Welcher User ist betroffen?
- **Release Tracking:** Errors pro Version
- **Performance Monitoring:** Transaction Tracing
**Integration:**
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/sentry'],
sentry: {
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
},
})
```
**Alternative:** Eigenes Logging-System mit Loki/Grafana
---
## 14. Email-Versand
### Optionen
**Option A: Eigener SMTP-Server**
- Pro: Volle Kontrolle, keine Kosten
- Contra: Wartung, Spam-Probleme, Deliverability
**Option B: Transactional Email Service**
- **SendGrid:** Zuverlässig, gute API
- **Postmark:** Spezialisiert auf Transactional
- **Mailgun:** Flexibel, gute Dokumentation
- **AWS SES:** Günstig, aber mehr Setup
**Empfehlung:** Postmark oder SendGrid
- Bessere Deliverability
- Weniger Wartung
- Templates & Analytics
- Webhooks für Bounce/Spam
**Nuxt Mailer:**
```typescript
// server/utils/mail.ts
import nodemailer from 'nodemailer'
export const sendOrderConfirmation = async (email: string, order: Order) => {
const transporter = nodemailer.createTransporter({
host: process.env.SMTP_HOST,
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
})
await transporter.sendMail({
from: 'noreply@experimenta.science',
to: email,
subject: `Bestellbestätigung ${order.orderNumber}`,
html: renderOrderEmail(order),
})
}
```
---
## 15. Development Tools
### ESLint + Prettier
**Entscheidung:** ESLint + Prettier für Code Quality
**Config:**
```json
{
"extends": ["@nuxt/eslint-config", "prettier"],
"rules": {
"vue/multi-word-component-names": "off"
}
}
```
---
### TypeScript
**Entscheidung:** Strict TypeScript
**tsconfig.json:**
```json
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true
}
}
```
---
## 16. Package Manager
### pnpm
**Entscheidung:** pnpm statt npm/yarn
**Begründung:**
- **Schneller:** Symlinks statt Kopien
- **Disk Space:** Shared Dependencies
- **Strict:** Verhindert Ghost Dependencies
- **Monorepo-Ready:** Workspaces Support
---
## 17. Vollständige Dependency-Liste
### Dependencies (package.json)
```json
{
"dependencies": {
"nuxt": "^4.0.0",
"vue": "^3.4.0",
"@nuxtjs/tailwindcss": "^6.11.0",
"radix-vue": "^1.9.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"tailwind-merge": "^2.2.0",
"drizzle-orm": "^0.29.0",
"postgres": "^3.4.0",
"zod": "^3.22.0",
"vee-validate": "^4.12.0",
"@vee-validate/zod": "^4.12.0",
"pinia": "^2.1.0",
"@pinia/nuxt": "^0.5.0",
"jose": "^5.2.0",
"@paypal/checkout-server-sdk": "^1.0.3",
"nodemailer": "^6.9.0"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.2.0",
"@nuxtjs/sentry": "^8.0.0",
"drizzle-kit": "^0.20.0",
"eslint": "^8.56.0",
"prettier": "^3.2.0",
"typescript": "^5.3.0",
"vitest": "^1.2.0",
"@vue/test-utils": "^2.4.0",
"@playwright/test": "^1.41.0",
"@nuxt/test-utils": "^3.11.0"
}
}
```
---
## 18. Zusammenfassung
| Kategorie | Technologie | Version |
| ------------------ | ----------------- | ------- |
| **Framework** | Nuxt | 4.x |
| **UI Library** | shadcn-vue | latest |
| **Styling** | Tailwind CSS | 4.x |
| **Datenbank** | PostgreSQL | 16+ |
| **ORM** | Drizzle | 0.29+ |
| **Auth** | Cidaas | - |
| **Payment** | PayPal | - |
| **Validation** | Zod | 3.x |
| **Forms** | VeeValidate | 4.x |
| **State** | Pinia | 2.x |
| **Testing (Unit)** | Vitest | 1.x |
| **Testing (E2E)** | Playwright | 1.x |
| **Deployment** | Docker | latest |
| **CI/CD** | GitLab CI | - |
| **Hosting** | Hetzner | - |
| **Error Tracking** | Sentry (optional) | 8.x |
| **Email** | Postmark/SendGrid | - |
---
**Ende des Dokuments**