# Datenbank: "postgresql" (Standard) oder "mysql". Nach einer Aenderung # einmal `pnpm db:push` ausfuehren (schreibt prisma/schema.prisma neu und # generiert den passenden Treiber-Adapter-Client) — siehe README, Abschnitt # "Datenbank-Provider wechseln". DATABASE_PROVIDER=postgresql # PostgreSQL (Standard). Fuer docker compose siehe POSTGRES_* unten. DATABASE_URL="postgresql://wabenhain:wabenhain@localhost:5432/wabenhain?schema=public" # MySQL/MariaDB stattdessen: # DATABASE_URL="mysql://wabenhain:wabenhain@localhost:3306/wabenhain" # Postgres-Zugangsdaten fuer docker-compose.yml (Datenbank-Container) POSTGRES_USER=wabenhain POSTGRES_PASSWORD=change-me POSTGRES_DB=wabenhain # Nur noetig, wenn DATABASE_PROVIDER=mysql: Zugangsdaten fuer den # db-mysql-Container ("docker compose --profile mysql up -d db-mysql") MYSQL_USER=wabenhain MYSQL_PASSWORD=change-me MYSQL_DATABASE=wabenhain # Stripe (Test-Keys aus dem Stripe-Dashboard, https://dashboard.stripe.com/test/apikeys) # # Serverseitiger Schluessel fuer Checkout-Sessions, Erstattungen und Webhooks. # Empfohlen: ein eingeschraenkter Schluessel (rk_...) statt des vollen sk_... STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx # Oeffentlicher Schluessel fuer das eingebettete Zahlungsformular auf /checkout. # Bewusst OHNE NEXT_PUBLIC_: wird zur Laufzeit gelesen, nicht beim Build # eingebrannt. Muss zum Modus des geheimen Schluessels passen (test/live). STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxx # Webhook-Secret aus `stripe listen` bzw. dem Stripe-Dashboard-Webhook-Endpoint STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx # Admin-Zugang: Der interne /admin-Bereich wird NICHT per Basic-Auth # geschuetzt, sondern ueber das normale Kundenkonto-Login (/konto) und die # Rolle "ADMIN" am Konto (siehe README, Abschnitt "Admin-Zugang"). Diese # beiden Werte legt `prisma db seed` als initiales Admin-Konto an, falls es # noch nicht existiert. Eine UNVORHERSEHBARE E-Mail waehlen (nicht # "admin@..." o.ae.) — sonst koennte sich vor dem ersten Seed-Lauf jemand # genau diese Adresse selbst registrieren; prisma/seed.ts befoerdert ein # bereits bestehendes Nicht-Admin-Konto zwar nicht mehr automatisch, ein # unvorhersehbarer Wert ist trotzdem die robustere erste Verteidigungslinie. ADMIN_EMAIL=change-me@example.com ADMIN_PASSWORD=change-me # Geheimer Schluessel zum Signieren der Kundenkonto-JWTs (HS256). # In Produktion MINDESTENS 32 Zeichen (wird beim Start erzwungen). # Erzeugen z.B. mit: openssl rand -base64 32 SESSION_SECRET=change-me-to-a-long-random-string # Basis-URL der Seite (fuer absolute Links, z.B. in E-Mails/Sitemaps). # Lokale Entwicklung (pnpm dev): http://localhost:3000 (Standard hier). # Vollstaendiger Docker-Betrieb: http://localhost (Caddy laeuft auf Port 80, # siehe SITE_ADDRESS/Caddyfile unten) bzw. die echte Domain in Produktion. NEXT_PUBLIC_SITE_URL=http://localhost:3000 # Nur fuer den vollstaendigen Docker-Betrieb (docker-compose.yml, Caddyfile): # die oeffentliche Domain, unter der die Seite erreichbar sein soll. Leer # gelassen laeuft Caddy lokal per HTTP auf Port 80 ohne TLS — fuer eine # echte Domain mit automatischem HTTPS z.B. "wabenhain.com" eintragen. SITE_ADDRESS= # Google Rezensionen (optional). Ohne diese beiden Werte zeigt die Startseite # automatisch die statischen Beispiel-Stimmen als Fallback. # API-Key: Google-Cloud-Projekt mit aktivierter "Places API (New)" — NICHT # die gleichnamige alte API, die in neuen Projekten nicht mehr aktivierbar # ist. Ausserdem noetig: Abrechnung am Projekt aktiv, und der Wert muss ein # API-Schluessel sein (ein OAuth-Client funktioniert nicht). Keine # Referrer-Beschraenkung setzen — der Abruf erfolgt serverseitig. # Place-ID: https://developers.google.com/maps/documentation/places/web-service/place-id GOOGLE_PLACES_API_KEY= GOOGLE_PLACE_ID= # Cloudflare Turnstile (optional). Ohne diese beiden Werte wird auf # Registrierung, Newsletter und Firmenanfrage kein CAPTCHA angezeigt und die # serverseitige Pruefung uebersprungen. Site-Key ist oeffentlich (NEXT_PUBLIC_), # Secret-Key bleibt privat. Einrichtung: https://dash.cloudflare.com/?to=/:account/turnstile TURNSTILE_SECRET_KEY= NEXT_PUBLIC_TURNSTILE_SITE_KEY= # Transaktions-E-Mails (Bestellbestaetigung, Newsletter-Opt-in, # Passwort-Reset) gehen ueber Microsoft 365 mit OAuth 2.0 — mit derselben # App-Registrierung wie "Mit Microsoft anmelden" (ENTRA_* weiter unten, dort # zusaetzlich die Berechtigung SMTP.SendAsApp, siehe README # "E-Mail-Versand"). Ein Passwort gibt es nicht. Ohne SMTP_USER und die # ENTRA_*-Werte werden E-Mails nur geloggt statt versendet — praktisch fuer # lokale Entwicklung ohne Mail-Setup. # SMTP_USER = das Postfach, mit dem gesendet wird; SMTP_FROM sollte dieselbe # Adresse tragen. SMTP_USER= SMTP_FROM="Wabenhain " # ApiCore-Synchronisierung (optional). Der Sync-Knopf im Admin-Dashboard holt # Ernten, Chargen und Produkte aus ApiCores Shop-Schnittstelle (GET # /shop-sync). Zugang ausschliesslich per API-Schluessel: in ApiCore unter # Verwaltung -> Automatisierung -> API-Schluessel anlegen (Berechtigung # "Shop-Daten lesen"). Kein Benutzerkonto, kein Passwort, jederzeit # widerrufbar. Ohne beide Werte meldet der Knopf einen Konfigurationsfehler. # Details siehe src/lib/apicore-client.ts. APICORE_API_URL= APICORE_API_KEY= # Bestandsabgleich: der Schluessel braucht zusaetzlich "Verkaeufe und # Rueckbuchungen des Shops buchen" (shop-sync:write). In ApiCore einen Webhook # fuer "stock.changed" auf https:///api/apicore/webhook anlegen und das # dort angezeigte Secret hier eintragen. Ohne Secret wird der Webhook # abgelehnt; der CronJob gleicht dann alle 10 Minuten ab. APICORE_WEBHOOK_SECRET= # Rechnungsexport fuer die Buchhaltung (ERPNext, collab.vycept.com): ERPNext # holt damit stuendlich alle Rechnungen samt PDF ab und ordnet sie der # Stripe-Zahlung zu (GET /api/erp/rechnungen, nur lesend, siehe # src/lib/erp-export.ts). Mindestens 32 Zeichen, z. B. `openssl rand -hex 32`; # derselbe Wert steht in ERPNext als WABENHAIN_ERP_TOKEN. Leer = Export aus. ERP_EXPORT_TOKEN= # DHL Parcel DE Shipping API ("Post & Parcel Germany", developer.dhl.com) — # ermoeglicht das Erstellen von DHL-Versandmarken per Knopfdruck im # Admin-Bestelldetail (siehe src/lib/dhl.ts). Ohne diese fuenf Werte zeigt # der Button dort nur den Hinweis "DHL ist noch nicht konfiguriert". # # So kommt ihr an die Werte: # 1. Account anlegen auf https://developer.dhl.com und eine App registrieren, # die die API "Parcel DE Shipping" (Post & Parcel Germany) einbindet. # Die App liefert DHL_API_KEY (Consumer Key) und DHL_API_SECRET # (Consumer Secret). Fuer den Sandbox-Test reichen diese beiden Werte # plus die DHL-Testzugangsdaten unten. # 2. Fuer den PRODUKTIV-Betrieb zusaetzlich einen DHL-Geschaeftskundenvertrag # ("DHL Paket" Versandvertrag) abschliessen — Ansprechpartner ist der # DHL-Vertrieb bzw. https://www.dhl.de/de/geschaeftskunden/paket.html. # Darueber bekommt ihr: # - Zugangsdaten zum Geschaeftskundenportal (GKP) -> DHL_GKP_USERNAME/ # DHL_GKP_PASSWORD (dieselben Zugangsdaten, mit denen ihr euch auf # dhl.de/geschaeftskundenportal einloggt). # - Die EKP (10-stellige Kundennummer) plus Verfahren/Teilnahme fuer # das gebuchte Produkt (Standard-Inlandspaket V01PAK) -> zusammen # ergibt das die 14-stellige DHL_ACCOUNT_NUMBER (billingNumber). # 3. DHL_ENVIRONMENT auf "sandbox" lassen, bis alles funktioniert, dann auf # "production" umstellen (unterschiedliche Basis-URLs, siehe lib/dhl.ts). # # DHL-Sandbox-Testzugangsdaten (Stand der Recherche, koennen sich aendern — # im Zweifel im DHL-Developer-Portal unter der App nachschauen): # DHL_GKP_USERNAME=user-valid # DHL_GKP_PASSWORD=SandboxPasswort2023! # HINWEIS: Fuer die PACKSTATION-SUCHE im Checkout reicht DHL_API_KEY allein. # Sie laeuft ueber die DHL Location Finder API (lib/dhl-locations.ts), einen # eigenen Dienst mit eigener Authentifizierung — nur der API-Key im Header, # kein OAuth und kein Geschaeftskundenvertrag. Ist der Key gesetzt, bietet der # Checkout "An eine Packstation" als Lieferziel an; fehlt er, erscheint die # Option gar nicht erst. Die uebrigen DHL_*-Werte braucht erst das Erstellen # von Versandmarken im Admin-Bereich. Kontingent laut DHL: 500 Aufrufe/Tag — # der Endpunkt /api/shop/packstationen ist deshalb streng rate-limitiert und # cached Ergebnisse 15 Minuten. # SENDUNGSVERFOLGUNG auf /bestellung-verfolgen: laeuft ueber die DHL Shipment # Tracking – Unified API (lib/dhl-tracking.ts), ebenfalls nur mit API-Key. # Die DHL-App muss dafuer im Developer Portal zusaetzlich das Produkt # "Shipment Tracking - Unified" abonniert haben. Ist es in einer EIGENEN App # freigeschaltet, deren Key in DHL_TRACKING_API_KEY eintragen — sonst wird # DHL_API_KEY verwendet. Ohne freigeschalteten Key zeigt die Seite wie bisher # nur Sendungsnummer + Link zu dhl.de. Ergebnisse werden pro Sendung # 10 Minuten (zugestellt: 12 Stunden) gecached, um das Kontingent zu schonen. DHL_TRACKING_API_KEY= DHL_API_KEY= DHL_API_SECRET= DHL_GKP_USERNAME= DHL_GKP_PASSWORD= # 14-stellig: EKP (10-stellig) + Verfahren (2-stellig) + Teilnahme (2-stellig) DHL_ACCOUNT_NUMBER= DHL_ENVIRONMENT=sandbox # Microsoft Entra ID SSO ("Mit Microsoft anmelden", siehe # src/app/api/auth/entra/). Ohne diese vier Werte zeigt der Button auf # /konto einen Hinweis, dass SSO noch nicht konfiguriert ist, statt # abzustuerzen. # # So richtet ihr es ein (Owner-Aufgabe im Azure-Portal, https://entra.microsoft.com): # 1. Entra ID -> App registrations -> "New registration". # - Name frei waehlbar, z.B. "Wabenhain Kundenlogin". # - "Supported account types": i.d.R. "Accounts in this organizational # directory only" (Single Tenant) fuer eine reine Firmen-SSO-Anbindung. # - Redirect URI: Plattform "Web", URL = /api/auth/entra/callback # (lokal: http://localhost:3000/api/auth/entra/callback). # 2. Nach dem Erstellen: "Application (client) ID" -> ENTRA_CLIENT_ID, # "Directory (tenant) ID" -> ENTRA_TENANT_ID (beide auf der Overview-Seite # der App-Registrierung sichtbar). # 3. "Certificates & secrets" -> "New client secret" erzeugen -> der # angezeigte WERT (nicht die Secret-ID!) -> ENTRA_CLIENT_SECRET. Nur # einmal sichtbar, bei Verlust ein neues Secret erzeugen. # 4. ENTRA_REDIRECT_URI = exakt dieselbe URL wie die in Schritt 1 hinterlegte # Redirect-URI (muss zeichengenau uebereinstimmen). # 5. "Enterprise applications" in Entra ID zeigt die App danach automatisch # als Unternehmensapplikation (wird beim App-Registration-Schritt # automatisch mit angelegt) — dort laesst sich optional der Nutzerkreis # (z.B. nur bestimmte Gruppen) einschraenken. # 6. Dieselbe App versendet auch die Shop-E-Mails (SMTP_USER oben): dafuer # die Anwendungsberechtigung "Office 365 Exchange Online -> SMTP.SendAsApp" # mit Administratorzustimmung ergaenzen und in Exchange das Postfach # freigeben (README "E-Mail-Versand"). Laeuft das Secret ab, sind # Anmeldung UND Mailversand betroffen. ENTRA_TENANT_ID= ENTRA_CLIENT_ID= ENTRA_CLIENT_SECRET= ENTRA_REDIRECT_URI=http://localhost:3000/api/auth/entra/callback # Verzeichnis fuer hochgeladene Bilder (Inhalte-Bild in admin/inhalte, # Produkt-/Erntenfotos in admin/produkte bzw. admin/ernten — siehe # app/api/admin/upload/route.ts und app/uploads/[...path]/route.ts). Die App # liest/schreibt hier ganz normal per node:fs; sie weiss nichts davon, WAS # dieses Verzeichnis in Produktion ist (siehe unten). # # Lokal reicht der Default "./uploads" (relativ zum Projektverzeichnis, per # .gitignore ausgeschlossen — beim ersten Upload automatisch angelegt). # # In docker-compose.yml ist UPLOAD_DIR bereits fest auf /app/uploads gesetzt # und ueber ein benanntes Docker-Volume persistiert (siehe dort) — hier # nichts weiter noetig, dieser Wert wirkt nur fuer lokales "npm run dev". # # In Produktion per Kubernetes (siehe k8s/05-app-deployment.yaml) zeigt # UPLOAD_DIR stattdessen auf einen Volume-Mount, damit hochgeladene Bilder # einen Pod-Neustart/-Redeploy ueberleben — welche Art Volume das konkret # ist (SMB/NFS/hostPath/...), entscheidet die Betreiberin beim Anlegen des # PersistentVolume; UPLOAD_DIR muss nur exakt auf den gewaehlten mountPath # zeigen. UPLOAD_DIR=./uploads # Ablage der Rechnungs-PDFs, nach Jahr und Kunde sortiert: # invoices/2026/mustermann-erika/RE-2026-A3F9C1B2.pdf # Lokal reicht der Default "./invoices" (per .gitignore ausgeschlossen). # # MUSS ein anderes Verzeichnis sein als UPLOAD_DIR: auf die Uploads zeigt # eine oeffentliche Route (app/uploads/[...path]), die JEDE Datei darunter # ohne Anmeldung ausliefert. Laegen Rechnungen dort, koennte jede Person, die # den Pfad erraet, fremde Rechnungen samt Anschrift herunterladen. # # In Produktion ein eigenes, gesichertes Volume: Rechnungen unterliegen in # Deutschland zehn Jahren Aufbewahrungspflicht (§ 147 AO), und das PDF liegt # NUR hier — die Datenbank kennt lediglich Nummer, Betrag und Pfad. Siehe # k8s/04-uploads-pvc.yaml und den Sicherungs-CronJob k8s/07-backup-cronjob.yaml. INVOICE_DIR=./invoices # Umsatzsteuersatz in GANZEN Prozent, ausgewiesen auf jeder Rechnung. # 7 = ermaessigter Satz (Honig als Lebensmittel, Anlage 2 zum UStG) # 19 = voller Satz # 0 = Kleinunternehmerregelung; die Rechnung traegt dann statt der # Steuerzeilen den Pflichthinweis nach § 19 UStG # # Die Shop-Preise sind BRUTTOPREISE (gegenueber Verbrauchern ist der # Gesamtpreis inkl. Umsatzsteuer anzugeben, § 1 PAngV). Die Steuer wird daraus # HERAUSGERECHNET, nicht aufgeschlagen — der Endpreis bleibt genau der, den die # Kundschaft im Warenkorb gesehen und bezahlt hat. # # Der Satz wird an jeder Rechnung festgeschrieben: eine Aenderung hier wirkt # nur auf kuenftige Rechnungen, bereits ausgestellte behalten ihren Satz. VAT_RATE_PERCENT=7 # Satz je Kategorie — der Steuersatz haengt an der Art der Ware. Fehlt hier # ein Wert, gilt VAT_RATE_PERCENT von oben. Ein am einzelnen Produkt # gepflegter Satz (Admin -> Produkte) schlaegt beides. # # PROPOLIS bitte pruefen: als Lebensmittel waeren 7% richtig, als # Nahrungsergaenzungs- oder Kosmetikprodukt 19%. Voreingestellt ist der volle # Satz, weil er im Zweifel der sicherere ist. VAT_RATE_PERCENT_HONIG=7 VAT_RATE_PERCENT_KERZEN=19 VAT_RATE_PERCENT_PROPOLIS=19 # Weitere Bienenprodukte (Kategorie SONSTIGES, z. B. Salben, Wachstuecher) VAT_RATE_PERCENT_SONSTIGES=19 # Threads, die ffmpeg beim Umrechnen eines hochgeladenen Videos benutzen darf # (Admin -> Inhalte -> Hero-Video). Leer lassen heisst 2, erlaubt sind 1 bis 8. # # NICHT hochsetzen, um den Upload zu beschleunigen, ohne gleichzeitig das # Speicherlimit des Pods zu pruefen: x264 haelt pro Thread eigene Bildpuffer, # bei 4K-Material rund 12 MB je Bild. Ohne Deckel richtet sich ffmpeg nach den # Kernen des KUBERNETES-KNOTENS statt nach dem CPU-Anteil des Pods — auf einem # grossen Knoten sprengt das sowohl den Speicher des Pods als auch die # Liveness-Probe, und der Container wird mitten im Upload neu gestartet. # Ausfuehrliche Begruendung in src/lib/video-transcode.ts. VIDEO_TRANSCODE_THREADS=