Files
2026-06-25 19:54:40 +02:00

261 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TeamVis — All-in-One Self-Hosting (App + Supabase)
## Schnellstart: `install.sh` (ein Schritt)
Für eine neue Instanz gibt es einen interaktiven Installer, der alles in einem
Rutsch erledigt und beim Start fragt, woher die Datenbank kommt:
```bash
bash deploy/selfhost/install.sh
```
- **Modus 1 — bestehende/eigene Supabase** (Cloud oder self-hosted): es läuft nur
die App (+ optional Caddy/TLS). Entspricht `docs/NEUKUNDE.md`, nur automatisiert.
- **Modus 2 — alles mitinstallieren:** schlanker Supabase-Stack als Container
(Postgres + PostgREST + Storage) + App + Caddy, **Single-Domain** (App,
`/rest/v1`, `/storage/v1` alle unter `https://team.kunde.de` — eine Domain, ein
Zertifikat). Kein vorab eingerichtetes Supabase nötig.
Das Skript erzeugt Schlüssel/Secrets, schreibt `.env` + `docker-compose.yml` +
`Caddyfile`, spielt das Schema ein, macht in Modus 2 den **Rollen-Bootstrap +
Grants automatisch** (der manuelle Block unter „Variante B" entfällt damit) und
legt den ersten Admin an. Voraussetzungen: docker + compose v2, node 20+,
openssl, DNS auf den Host, Ports 80/443 frei, `docker login git.zoesch.de`.
Variablen lassen sich vorab per ENV setzen (überspringt die Abfrage), z. B.
`APP_DOMAIN=team.kunde.de ADMIN_EMAIL=chef@kunde.de MODE=2 bash deploy/selfhost/install.sh`.
> Die folgenden Abschnitte beschreiben die **manuellen** Varianten (zum
> Verständnis / für Sonderfälle). Für den Normalfall reicht `install.sh`.
---
## Manuell (Hintergrund)
**Option/Status:** ausgearbeitet **und end-to-end auf zfx-vps getestet**
(Lean-Datenebene). Karte, vCard, Storage-Upload/-Abruf, anon-RLS und der
Spalten-Revoke (0045) laufen gegen self-hosted Postgres + PostgREST +
Storage. Der Testlauf hat drei Bootstrap-Lücken aufgedeckt — alle gelöst,
siehe [Befunde](#befunde-aus-dem-testlauf-2026-06-07).
Diese Variante bringt mit **einem Compose-Stack** sowohl die TeamVis-App als
auch eine eigene Supabase-Instanz hoch — der Kunde braucht **kein** vorab
eingerichtetes Supabase (weder Cloud noch self-hosted). Gegensatz zum
Standard-Onboarding (`docs/NEUKUNDE.md`), das eine **externe** Supabase
voraussetzt.
---
## Was TeamVis von Supabase wirklich braucht
Analyse des Codes (`lib/supabase-server.ts`, alle `*.actions.ts`):
| Supabase-Komponente | Von TeamVis genutzt? | Wofür |
|---|---|---|
| **Postgres** | ✅ Pflicht | alle Daten |
| **PostgREST** (`/rest/v1`) | ✅ Pflicht | sämtliche DB-Reads/Writes (`@supabase/supabase-js`) |
| **Storage** (`/storage/v1`) | ✅ Pflicht | Buckets `employee-photos` + Branding, **öffentliche** URLs (`getPublicUrl`) |
| GoTrue / Auth (`/auth/v1`) | ❌ | TeamVis nutzt **keine** Supabase-Auth (Admin = bcrypt+iron-session, Portal = Magic-Link) |
| Realtime | ❌ | — |
| imgproxy (Bild-Transforms) | ❌ | Karten nutzen `next/image`, keine Storage-Transforms |
| Studio / Meta | ⚪ optional | bequemes DB-/SQL-UI für den Betreiber |
**Folge:** technisch genügen **Postgres + PostgREST + Storage + ein
Pfad-Router**. Auth/Realtime/imgproxy sind verzichtbar.
---
## Architektur
```
┌─────────────── Caddy (Auto-HTTPS) ───────────────┐
Browser ── 443 ───▶ │ team.kunde.de → teamvis:3000 │
(Karten, Admin) │ supabase.kunde.de → kong:8000 │
└───────────────┬──────────────────┬───────────────┘
│ │
┌──────────▼─────┐ ┌────────▼─────────────────┐
│ teamvis (App) │ │ Supabase-Stack (kong → │
│ Next.js :3000 │──▶│ rest + storage + db …) │
└────────────────┘ └──────────────────────────┘
```
- Die **Foto-URLs der Karten zeigen auf `supabase.kunde.de`** (öffentliches
`getPublicUrl`). Deshalb muss diese Domain **öffentlich per HTTPS**
erreichbar sein — `next/image` erlaubt nur `https` auf
`/storage/v1/object/public/**` (siehe `next.config.ts`, bereits
vorbereitet: jeder https-Host auf diesem Pfad ist zulässig).
- Das **App-Image ist unverändert** das SaaS-Image — alle Supabase-Werte
kommen zur Laufzeit aus ENV (`lib/runtime-env.ts`). Kein Rebuild pro Kunde.
---
## Variante A — offizieller Supabase-Stack + Override (empfohlen)
Lehnt sich an den **getesteten** `supabase/docker`-Stack an und legt TeamVis
per Override darüber. Vorteil: korrekte, aufeinander abgestimmte Image-Pins
(Postgres/PostgREST/Storage/Kong) ohne eigenes Versions-Risiko.
### Schritte
```bash
# 0) Voraussetzungen auf dem Server: docker + compose, node 20+, openssl,
# DNS für team.kunde.de und supabase.kunde.de auf diese IP, Ports 80/443 frei.
# 1) Supabase-Stack holen
git clone --depth 1 https://github.com/supabase/supabase
cd supabase/docker
cp .env.example .env
# 2) TeamVis-Override + Caddy + Zusatz-ENV hineinlegen
REPO=/pfad/zum/team-stwhas
cp $REPO/deploy/selfhost/docker-compose.override.yml .
cp $REPO/deploy/selfhost/Caddyfile.example ./Caddyfile # Domains anpassen!
cat $REPO/deploy/selfhost/.env.teamvis.example >> .env # Werte ausfüllen
# 3) In .env setzen/ersetzen:
# POSTGRES_PASSWORD → starkes Passwort
# SITE_URL=https://team.kunde.de
# SUPABASE_PUBLIC_URL=https://supabase.kunde.de
# API_EXTERNAL_URL=https://supabase.kunde.de
# SESSION_SECRET → openssl rand -base64 48
# (JWT_SECRET/ANON_KEY/SERVICE_ROLE_KEY macht bootstrap.sh in Schritt 4)
# 4) Bootstrap: Keys + Stack + Schema + erster Admin
cp $REPO/deploy/selfhost/bootstrap.sh .
TEAMVIS_REPO=$REPO ADMIN_EMAIL=chef@kunde.de bash bootstrap.sh
```
`bootstrap.sh` erledigt: Schlüssel erzeugen (ersetzt die unsicheren
Demo-Keys), `docker compose up -d`, auf gesunde DB warten,
TeamVis-Schema (Migrations-Bündel) einspielen, ersten Admin anlegen.
### Optional schlanker (Footprint senken)
Der Volldownload startet ~10 Container. Nicht genutzte abschalten —
in der `.env` bzw. per `docker compose` weglassen: **studio, auth
(GoTrue), realtime, imgproxy, analytics/vector, meta, supavisor/pooler**.
Übrig bleiben **db, rest, storage, kong** (+ teamvis, caddy). Das ist die
empfohlene Ziel-Konfiguration für eine Einzel-Mandanten-Appliance —
sollte aber im selben Probelauf mitgetestet werden (Kong-Routen + Storage
hängen minimal an Auth-Annahmen).
---
## Variante B — komplett schlank (eigener Mini-Stack)
Nur `supabase/postgres:15.8.1.085` + `postgrest/postgrest:v14.12` +
`supabase/storage-api:v1.60.4` + Caddy als Pfad-Router (kein Kong, kein
GoTrue/Realtime/imgproxy). **Genau das wurde getestet** (siehe Befunde) und
läuft — braucht aber einen Rollen-Bootstrap, den der offizielle Stack
automatisch macht. Caddy-Router statt Kong:
```caddyfile
:8000 {
handle_path /rest/v1/* { reverse_proxy rest:3000 }
handle_path /storage/v1/* { reverse_proxy storage:5000 }
}
```
**Pflicht-Bootstrap nach dem ersten Start** (einmalig, als `supabase_admin`,
Passwort = `POSTGRES_PASSWORD`):
```sql
-- 1) Rollen-Passwörter (das Image setzt nur supabase_admin auf POSTGRES_PASSWORD)
alter role authenticator with login password '<POSTGRES_PASSWORD>';
alter role supabase_storage_admin with login password '<POSTGRES_PASSWORD>';
alter role supabase_auth_admin with login password '<POSTGRES_PASSWORD>';
-- 2) service_role-Rechte (auf echtem Supabase automatisch vorhanden)
alter role service_role bypassrls;
grant all on all tables in schema public to service_role;
grant all on all sequences in schema public to service_role;
grant all on all routines in schema public to service_role;
alter default privileges in schema public grant all on tables to service_role;
alter default privileges in schema public grant all on sequences to service_role;
```
Reihenfolge: Passwörter (1) **vor** dem ersten Start von `rest`/`storage`,
die Grants (2) **nach** dem Schema-Load. Wegen dieses Mehraufwands ist für
die *erste* Kundeninstallation **Variante A empfohlen** (dort entfällt der
ganze Block).
---
## Stolpersteine (wichtig)
1. **`SUPABASE_PUBLIC_URL` muss öffentlich + HTTPS sein** — sie steckt in den
Foto-URLs der Karten. Interner Hostname (`http://kong:8000`) funktioniert
**nicht** für die Bildanzeige.
2. **SMTP-Kollision:** der Supabase-Stack belegt `SMTP_*` selbst (GoTrue).
Die App liest deshalb `TEAMVIS_SMTP_*` (siehe Override) — nicht
vermischen.
3. **Reihenfolge:** Schema erst einspielen, wenn Storage einmal gestartet ist
(Migration `0044` legt den Bucket in `storage.buckets` an). `bootstrap.sh`
wartet darauf.
4. **Keys sind JWTs:** `ANON_KEY`/`SERVICE_ROLE_KEY` sind mit `JWT_SECRET`
signierte Tokens (`gen-keys.mjs`). Wird `JWT_SECRET` geändert, müssen beide
Keys neu erzeugt werden.
5. **Backups:** jetzt liegt die DB beim Kunden. Postgres-Volume + Storage-
Volume sichern (`pg_dump` + `volumes/storage`). Beim externen Supabase war
das deren Sache.
6. **Modul-Rollout:** frische Instanz startet minimal (nur Visitenkarten).
Danach unter **Admin → Funktionen** gestaffelt freischalten
(siehe `docs/NEUKUNDE.md` §5a).
---
## Footprint (grob)
| | Container | RAM (Daumenregel) |
|---|---|---|
| Variante A voll | ~12 | ~23 GB |
| Variante A schlank | 6 (db, rest, storage, kong, teamvis, caddy) | ~11.5 GB |
| Externe Supabase (NEUKUNDE.md) | 2 (teamvis, caddy) | ~0.4 GB |
---
## Befunde aus dem Testlauf (2026-06-07)
Lean-Stack (db + rest + storage + Caddy-Router + app) auf zfx-vps hochgezogen,
nur an localhost gebunden. Drei Bootstrap-Lücken gefunden:
1. **Basis-Schema fehlte** — die Migrationen legen `employees`/`admin_users`
nicht an (nur additive `add column if not exists`). `bundle-migrations`
bootstrappt eine leere DB also nicht. **Gelöst:** neue Migration
`supabase/migrations/0000_base_schema.sql` (idempotent, auf
Bestandsinstanzen ein No-Op). Betrifft auch den `NEUKUNDE.md`-Weg.
2. **Rollen-Passwörter**`supabase/postgres` setzt nur `supabase_admin` auf
`POSTGRES_PASSWORD`; `authenticator`/`supabase_storage_admin` brauchen es
manuell (Variante B). Variante A erledigt das.
3. **service_role-Grants** — frisch erzeugte Tabellen geben `service_role`
keine Rechte; auf echtem Supabase passiert das automatisch. Grants +
`bypassrls` nötig (Variante B). Variante A erledigt das.
**Nach den Fixes verifiziert (alles grün):**
| Test | Ergebnis |
|---|---|
| Schema-Bündel (mit 0000) auf leerer DB | 0 Fehler, 23 Tabellen |
| `/api/health` | `ok:true`, `db.ok:true` |
| Öffentliche Karte `/<slug>` | HTTP 200 |
| vCard `/api/vcard/<slug>` | HTTP 200 |
| anon-REST: aktive MA lesen | ✅ (RLS erlaubt) |
| anon-REST: Geheim-Spalte `phone_mobile` | `permission denied` ✅ (0045 greift) |
| service_role-REST | ✅ (nach Grants) |
| Storage-Upload + öffentlicher Abruf | HTTP 200, Inhalt korrekt |
**Noch offen für „voll kundenreif":** ein Lauf von **Variante A** (offizieller
Stack — bestätigt, dass Punkt 2+3 dort automatisch wegfallen) sowie ein Test
des Admin-Logins + Portal-Magic-Links über die echten HTTPS-Domains (Caddy/TLS
war im localhost-Test nicht aktiv).
## Dateien hier
| Datei | Zweck |
|---|---|
| `docker-compose.override.yml` | TeamVis-App + Caddy über dem Supabase-Stack |
| `.env.teamvis.example` | Zusatz-ENV für die Supabase-.env |
| `Caddyfile.example` | TLS-Ingress (App- + Supabase-Domain) |
| `gen-keys.mjs` | erzeugt JWT_SECRET + ANON_KEY + SERVICE_ROLE_KEY |
| `bootstrap.sh` | Keys + Stack + Schema + Admin in einem Rutsch |