# Neukunden-Onboarding (TeamVis-Instanz) Reproduzierbares Runbook für eine **frische** TeamVis-Instanz (eigener Mandant: eigene Supabase, eigene Domain). Das Container-Image ist mandanten-neutral — alle Kunden laufen auf demselben Image, unterschieden nur durch ENV + DB + Branding. Dieses Dokument führt von „nichts" bis „live". Reihenfolge ist verbindlich: **DB → ENV → Container → erster Admin → Branding → Smoke-Test.** --- ## 0. Voraussetzungen - Eine **Supabase-Instanz** (Cloud-Projekt _oder_ self-hosted). Du brauchst: - Projekt-URL (`https://xxxx.supabase.co` bzw. `https://supabase.kunde.de`) - `anon`-Key (öffentlich) - `service_role`-Key (geheim, nur Server) - Eine **Domain** (z. B. `team.kunde.de`) mit DNS auf den Docker-Host. - Ein **Docker-Host** mit Zugriff auf die Registry `git.zoesch.de/zfx-services/teamvis` (Registry-Login vorhanden) und einem Reverse-Proxy (Caddy/Traefik/nginx). - Lokal dieses Repo + `node` (für `scripts/`-Helfer). --- ## 1. Datenbank-Schema einspielen Alle Tabellen, RLS-Policies, Storage-Buckets (`branding-assets` **und** `employee-photos`) und GRANTs kommen aus `supabase/migrations/`. Es gibt zwei Wege: > Die Basis-Tabellen `employees` + `admin_users` legt seit `0000_base_schema.sql` > die erste Migration an (`create table if not exists`). Auf einer **leeren** > DB bootstrappt das Bündel damit vollständig; auf einer **bestehenden** > Instanz ist `0000` ein No-Op. ### Variante A — Schema-Bündel (ohne supabase-CLI/psql-Zugang, empfohlen für Erst-Setup) ```bash node scripts/bundle-migrations.mjs > /tmp/teamvis-schema.sql ``` Den Inhalt von `/tmp/teamvis-schema.sql` einmal in den **Supabase-Studio SQL-Editor** einfügen und ausführen. Das Bündel enthält alle Migrationen in korrekter Reihenfolge. > Wichtig: Das Bündel ist für eine **leere** DB gedacht (Erst-Lauf). Für > spätere Schema-Updates auf einer bestehenden Instanz nur die **neue** > Einzel-Migration ausführen, nicht das ganze Bündel. ### Variante B — supabase-CLI (wenn DB-Zugang verfügbar) ```bash supabase link --project-ref supabase db push ``` ### Kontrolle In Supabase Studio prüfen, dass existieren: - Tabellen: `employees`, `admin_users`, `admin_invites`, `site_settings`, … (Organigramm-, Compliance-, Lead-, License-Tabellen) - Storage-Buckets: `branding-assets` **und** `employee-photos` (beide public) - `site_settings`: GRANTs sind spaltenweise (Geheimnis-Spalten **nicht** für `anon`, siehe Migration `0043`) --- ## 2. ENV-Datei vorbereiten Die App liest zur **Laufzeit** (kein Build-Time-Inlining → ein Image für alle Mandanten). Lege auf dem Host `/opt//app/.env` an: ```env # ── Supabase ─────────────────────────────────────────── NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY= SUPABASE_SERVICE_ROLE_KEY= # ── Session (Pflicht, >= 32 Zeichen, pro Instanz EINMALIG erzeugen) ── SESSION_SECRET= # ── Öffentliche URL (für QR-Codes / vCard-Links) ─────── NEXT_PUBLIC_SITE_URL=https://team.kunde.de # ── SMTP (optional; fehlt es, landen Magic-Links nur im Server-Log) ── SMTP_HOST=mail.kunde.de SMTP_PORT=587 SMTP_SECURE=false SMTP_USER= SMTP_PASS= SMTP_FROM_EMAIL=noreply@team.kunde.de SMTP_FROM_NAME=TeamVis ``` `SESSION_SECRET` erzeugen: `openssl rand -base64 48`. **Niemals** zwischen Mandanten teilen. > Self-hosted Supabase: `next/image` lädt Foto-URLs aus > `/storage/v1/object/public/**` jedes HTTPS-Hosts — kein zusätzliches > `SUPABASE_IMAGE_HOSTS` nötig (siehe `next.config.ts`). --- ## 3. Container deployen `/opt//app/docker-compose.yml`: ```yaml services: app: image: git.zoesch.de/zfx-services/teamvis:0.11.5 # aktuelle Version pinnen container_name: -teamvis restart: unless-stopped env_file: .env ports: - "127.0.0.1:3001:3000" # Port pro Instanz eindeutig wählen healthcheck: test: ["CMD", "node", "-e", "fetch(\"http://127.0.0.1:3000/\").then(r => process.exit(r.status < 500 ? 0 : 1)).catch(() => process.exit(1))"] interval: 30s timeout: 5s retries: 3 start_period: 20s ``` ```bash docker compose pull && docker compose up -d docker compose logs -f app # auf "Ready" warten ``` Reverse-Proxy (Caddy-Beispiel) auf den lokalen Port mappen — Caddy holt das TLS-Zertifikat automatisch: ``` team.kunde.de { reverse_proxy 127.0.0.1:3001 } ``` --- ## 4. Ersten Admin anlegen `admin_invites` setzen einen bestehenden Admin voraus — bei einer frischen Instanz gibt es keinen. Das Bootstrap-Script legt ihn direkt an (bcrypt cost 12, identisch zur App-Logik): ```bash # vom lokalen Repo aus, ENV aus .env.local oder per dotenv-cli: npx dotenv -e .env.local -- node scripts/create-admin.mjs chef@kunde.de --name "Max Chef" # Passwort wird verdeckt abgefragt (min. 10 Zeichen) ``` Alternativ ENV direkt setzen (`NEXT_PUBLIC_SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY` der Kunden-DB) und das Script ohne dotenv aufrufen. Danach Login unter `https://team.kunde.de/admin/login`. Weitere Admins/Viewer dann bequem über **Admin → Einladungen** im Backend. --- ## 5. Branding & Inhalte Alles White-Label über das Backend, kein Code/Rebuild nötig: 1. **Admin → Branding** — Firmenname, Logo, Favicon, Farben, Footer/Impressum-Link. 2. **Admin → Mitarbeiter** — erste Karten anlegen (Foto, Kontaktdaten, Sichtbarkeits-Flags). Import per CSV möglich. 3. Optional: **Organigramm**, **Compliance-Frameworks**, **Telefonanlage**, **Wallet** (Apple/Google) je nach Modul-Bedarf — Freischaltung siehe nächster Abschnitt. --- ## 5a. Funktionen gestaffelt freischalten Eine frische Instanz startet **bewusst minimal**: nur die Visitenkarten-Basis (`business_cards`) ist aktiv. Alle weiteren Module schaltest du unter **Admin → Funktionen** nach und nach frei. Empfehlung für die Erstinstallation — erst die Basis abnehmen, dann Phase für Phase, damit bei Problemen die Fehlerquelle eng bleibt: 1. **Phase 1 — Go-Live-Kern:** Organigramm, Compliance (keine externe Abhängigkeit). 2. **Phase 2 — Self-Service & Erfassung:** Empfang, KI, Card-Scanner, NFC (braucht SMTP für Magic-Links bzw. einen AI-Provider unter Admin → AI-Provider). 3. **Phase 3 — Externe Systeme:** Wallet, Telefonanlage, Webhooks (Zertifikate, 3CX, Webhook-Empfänger — höchstes Debug-Risiko, zuletzt). Jedes Modul ist einzeln schaltbar; bei Bedarf feiner vorgehen. Bei gesetzter Lizenz ist diese die Obergrenze — nicht lizenzierte Module bleiben gesperrt. > **Hinweis Bestandsinstanzen:** Beim Update **auf** v0.12.0 muss auf bereits > laufenden Instanzen `docs/sql/0.12.0-enable-modules.sql` **vor** dem > Image-Deploy laufen (SQL-first), sonst verschwinden bislang aktive Module. > Neuinstallationen sind nicht betroffen. --- ## 6. Smoke-Test (Abnahme) - [ ] `https://team.kunde.de/` zeigt eine Karte (öffentlich, ohne Login) - [ ] vCard-Download `…/api/vcard/` liefert `.vcf` - [ ] QR-Code auf der Karte zeigt auf die richtige `NEXT_PUBLIC_SITE_URL` - [ ] Admin-Login funktioniert, Mitarbeiterliste lädt - [ ] Self-Service-Portal: Magic-Link wird versendet (oder steht im Log, falls kein SMTP) - [ ] Foto-Upload landet im Bucket `employee-photos` und wird auf der Karte angezeigt - [ ] Security-Header gesetzt: `curl -sI https://team.kunde.de | grep -i x-frame-options` - [ ] Geheimnis-Check: mit dem **anon**-Key liefert eine Abfrage der Geheimnis-Spalten von `site_settings` `permission denied` --- ## 7. Updates & Versionsschema TeamVis folgt Semantic Versioning (`MAJOR.MINOR.PATCH`): | Sprung | Bedeutung | Was zu tun ist | |---|---|---| | **Patch** (`x.y.0 → x.y.1`) | Bugfix, kein Schema-Change | Tag bumpen → `docker compose pull && up -d` | | **Minor** (`x.0 → x.1`) | Feature, Schema-Change möglich | **erst** neue Migration einspielen, **dann** Container-Update | | **Major** (`1 → 2`) | Breaking Change | Migration-Notes lesen, Backup, dann Update | Patch + Minor sind DB-rückwärtskompatibel (altes Image läuft mit neuer DB). Image-Tag in Production **immer pinnen** (`teamvis:0.11.6`), nicht `:latest` — sonst zieht ein `compose pull` ungewollt einen Major-Sprung. **Standard-Update:** ```bash # 1. Backup (siehe Abschnitt 8) # 2. Release-Notes / CHANGELOG.md prüfen # 3. fehlende Migration(en) aus supabase/migrations/ einspielen (nur die neuen!) # 4. Tag im docker-compose.yml bumpen docker compose pull && docker compose up -d --force-recreate ``` **Rollback:** Image-Tag im Compose zurücksetzen + `up -d --force-recreate`. Achtung: war eine nicht-rückwärtskompatible Migration dabei, muss zusätzlich der DB-Dump zurückgespielt werden — Daten zwischen Update und Rollback sind dann verloren. Darum der Pre-Update-Dump. --- ## 8. Backup & Aufbewahrung Zwei kritische Bereiche: **Postgres-DB** (alle Datensätze, Audit-Log) und der **Storage-Bucket `employee-photos`**. - **DB (Supabase Cloud):** tägliche Auto-Backups + PITR ab Pro-Plan. Zusätzlich eigener Dump empfohlen: ```bash pg_dump "$DATABASE_URL" --no-owner --no-acl --format=custom > teamvis-$(date +%Y%m%d).dump ``` - **Storage:** `npx supabase storage cp "ss:///employee-photos" /backup/photos --recursive` (self-hosted: Storage ist ein Verzeichnis → `rsync`/ZFS-Snapshot). - **Audit-Log ist GoBD-relevant (10 Jahre Aufbewahrung):** monatlich per `Verwaltung → Protokoll → Export` als CSV ins unveränderbare Langzeit-Archiv. - **`.env` wird NICHT mitgesichert** — Secrets gehören in einen Passwort-Safe. - **Restore-Test** mindestens jährlich (Dump in Test-DB, Container dagegen starten, Login + Karten prüfen). Ein nie-restored Backup ist kein Backup. --- ## Bekannte Lücken / TODO - Kein automatischer Migration-Runner gegen self-hosted DBs ohne PG-Passwort → Schema-Bündel via Studio (Variante A) ist der dokumentierte Weg. - Vor aktivem Lizenz-Verkauf (Stripe live) sind Gewerbeanmeldung + USt nötig — privates Impressum trägt nur eine Info-/Showcase-Seite.