TeamVis Self-Host-Bundle v0.31.0
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
# 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 <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/<kunde>/app/.env` an:
|
||||
|
||||
```env
|
||||
# ── Supabase ───────────────────────────────────────────
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon-key>
|
||||
SUPABASE_SERVICE_ROLE_KEY=<service-role-key>
|
||||
|
||||
# ── Session (Pflicht, >= 32 Zeichen, pro Instanz EINMALIG erzeugen) ──
|
||||
SESSION_SECRET=<openssl rand -base64 48>
|
||||
|
||||
# ── Ö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=<user>
|
||||
SMTP_PASS=<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/<kunde>/app/docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
image: git.zoesch.de/zfx-services/teamvis:0.11.5 # aktuelle Version pinnen
|
||||
container_name: <kunde>-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/<slug>` zeigt eine Karte (öffentlich, ohne Login)
|
||||
- [ ] vCard-Download `…/api/vcard/<slug>` 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.
|
||||
@@ -0,0 +1,141 @@
|
||||
# TeamVis Self-Hosting — Quickstart (von der nackten VM bis live)
|
||||
|
||||
Diese Anleitung führt von einer **komplett frischen Linux-VM** bis zur
|
||||
laufenden TeamVis-Instanz. Sie nutzt den Ein-Schritt-Installer
|
||||
`deploy/selfhost/install.sh` (Hintergrund & manuelle Varianten:
|
||||
`deploy/selfhost/README.md`, externe Supabase: `docs/NEUKUNDE.md`).
|
||||
|
||||
Zwei Betriebsarten zur Auswahl (fragt der Installer ab):
|
||||
|
||||
- **Modus 2 — alles mitinstallieren** (empfohlen für „nur eine VM"): TeamVis
|
||||
**und** eine schlanke Supabase laufen als Container auf derselben VM, unter
|
||||
**einer Domain**. Kein externes Supabase nötig.
|
||||
- **Modus 1 — eigene/bestehende Supabase**: du hast schon ein Supabase-Projekt
|
||||
(Cloud oder self-hosted); es läuft nur die App.
|
||||
|
||||
Diese Anleitung beschreibt **Modus 2** (der Komplettfall).
|
||||
|
||||
---
|
||||
|
||||
## 0. Voraussetzungen
|
||||
|
||||
**Du brauchst vorab:**
|
||||
|
||||
| Was | Details |
|
||||
|---|---|
|
||||
| **VM** | Ubuntu 22.04+/Debian 12+, **2 GB RAM**+ (Modus 2 ≈ 1–1,5 GB), ~10 GB Disk. Root- bzw. sudo-Zugang. |
|
||||
| **Domain** | z. B. `team.kunde.de`, mit **DNS-A-Record auf die öffentliche IP der VM**. Caddy holt das TLS-Zertifikat dann automatisch. |
|
||||
| **Offene Ports** | eingehend **80 und 443** (Let's Encrypt + Web). |
|
||||
| **Zugangs-Token** | ein **Token** für `git.zoesch.de` — **vom TeamVis-Anbieter**. Damit ziehst du das Container-Image **und** das Installer-Bundle. **Kein** Quellcode-Zugang. |
|
||||
|
||||
> So ist die Auslieferung getrennt: Mit dem Token bekommst du nur das **Bundle**
|
||||
> (Installer + Migrationen, kein Quellcode) und das **Container-Image** — der
|
||||
> eigentliche App-Quellcode bleibt unzugänglich. Ein und derselbe Token (Benutzer
|
||||
> `teamvis-pull`) deckt beides ab. (Anbieter-Details: `docs/DISTRIBUTION.md`.)
|
||||
|
||||
---
|
||||
|
||||
## 1. Grundpakete installieren
|
||||
|
||||
Auf der frischen VM (als root oder mit `sudo`):
|
||||
|
||||
```bash
|
||||
# Docker + Compose-Plugin
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
sudo usermod -aG docker "$USER" # danach einmal aus-/einloggen (oder: newgrp docker)
|
||||
|
||||
# Node 20 + git + openssl
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
|
||||
sudo apt-get install -y nodejs git openssl
|
||||
|
||||
# Kontrolle
|
||||
docker --version && docker compose version && node --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. An der Registry anmelden (Token)
|
||||
|
||||
Benutzer ist `teamvis-pull`, Passwort der Token vom Anbieter:
|
||||
|
||||
```bash
|
||||
docker login git.zoesch.de
|
||||
# Benutzer: teamvis-pull · Passwort: <Token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Installer-Bundle holen
|
||||
|
||||
Mit demselben Token (im Klon-Link):
|
||||
|
||||
```bash
|
||||
git clone https://teamvis-pull:<Token>@git.zoesch.de/zfx-services/teamvis-selfhost.git
|
||||
cd teamvis-selfhost
|
||||
```
|
||||
|
||||
(Enthält Installer, Helfer-Skripte und Migrationen — **keinen** App-Quellcode.
|
||||
Die App selbst kommt als fertiges Image aus der Registry, wird also nicht
|
||||
gebaut. Kein git? Stattdessen das Release-Tarball des Anbieters entpacken.)
|
||||
|
||||
---
|
||||
|
||||
## 4. Installer ausführen
|
||||
|
||||
```bash
|
||||
bash deploy/selfhost/install.sh
|
||||
```
|
||||
|
||||
Der Installer fragt der Reihe nach:
|
||||
|
||||
1. **Kurzname** der Instanz (z. B. `kunde`) — nur für Verzeichnis/Compose-Name.
|
||||
2. **Domain** der App (`team.kunde.de`).
|
||||
3. **Image-Version** (Default = aktuelle Version, einfach Enter).
|
||||
4. **Admin-E-Mail** + **Name** (der erste Backend-Login).
|
||||
5. **Zielverzeichnis** (Default: `./kunde`).
|
||||
6. **SMTP** (optional — leer lassen ist ok; dann landen Login-/Magic-Links nur
|
||||
im Container-Log statt im Postfach. Lässt sich später in der `.env` nachtragen).
|
||||
7. **Datenbank-Modus** → hier **`2`** wählen.
|
||||
8. Am Ende: **Admin-Passwort** (mind. 10 Zeichen, verdeckt).
|
||||
|
||||
Danach erledigt das Skript automatisch: Schlüssel/Secrets erzeugen, `.env` +
|
||||
`docker-compose.yml` + `Caddyfile` schreiben, Datenbank starten,
|
||||
Rollen-Bootstrap + Rechte, Schema einspielen, App + Caddy starten, ersten Admin
|
||||
anlegen. Dauer: wenige Minuten (Image-Download).
|
||||
|
||||
---
|
||||
|
||||
## 5. Erster Login & Abnahme
|
||||
|
||||
1. `https://team.kunde.de/admin/login` öffnen → mit Admin-E-Mail + Passwort
|
||||
anmelden (beim ersten Login wird ein neues Passwort gesetzt).
|
||||
2. **Admin → Branding**: Firmenname, Logo, Farben, Impressum.
|
||||
3. **Admin → Mitarbeiter**: erste Karte anlegen (oder CSV-Import).
|
||||
4. **Admin → Funktionen**: weitere Module gestaffelt freischalten — eine frische
|
||||
Instanz startet bewusst nur mit den **Visitenkarten**.
|
||||
|
||||
**Smoke-Test:**
|
||||
- [ ] `https://team.kunde.de/<slug>` zeigt eine Karte (ohne Login)
|
||||
- [ ] vCard-Download `…/api/vcard/<slug>` liefert `.vcf`
|
||||
- [ ] Foto-Upload im Backend erscheint auf der Karte
|
||||
- [ ] (mit SMTP) Self-Service-Portal verschickt einen Login-Code
|
||||
|
||||
---
|
||||
|
||||
## 6. Betrieb
|
||||
|
||||
- **Updates:** Image-Tag in `kunde/docker-compose.yml` bumpen, bei Minor-Sprüngen
|
||||
vorher die neue(n) Migration(en) einspielen → `docker compose pull && up -d`.
|
||||
Details: `docs/NEUKUNDE.md` §7.
|
||||
- **Backup (wichtig, jetzt liegt die DB bei dir):** das Postgres-Volume
|
||||
(`db-data`) **und** `kunde/volumes/storage` sichern. Die `.env` gehört in einen
|
||||
Passwort-Safe (enthält alle Secrets), **nicht** ins Backup-Repo.
|
||||
|
||||
---
|
||||
|
||||
## Wenn etwas klemmt
|
||||
|
||||
- `docker compose pull` schlägt fehl → `docker login git.zoesch.de` nachholen.
|
||||
- TLS kommt nicht → DNS-A-Record + offene Ports 80/443 prüfen (Caddy braucht
|
||||
beide für Let's Encrypt).
|
||||
- Logs ansehen: `cd kunde && docker compose logs -f app` bzw. `caddy`.
|
||||
Reference in New Issue
Block a user