1699 lines
40 KiB
Markdown
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¤cy=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**
|