Files

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 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.

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

# 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:

: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):

-- 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örtersupabase/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