261 lines
12 KiB
Markdown
261 lines
12 KiB
Markdown
# 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 | ~2–3 GB |
|
||
| Variante A schlank | 6 (db, rest, storage, kong, teamvis, caddy) | ~1–1.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 |
|