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/v1alle unterhttps://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(öffentlichesgetPublicUrl). Deshalb muss diese Domain öffentlich per HTTPS erreichbar sein —next/imageerlaubt nurhttpsauf/storage/v1/object/public/**(siehenext.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)
SUPABASE_PUBLIC_URLmuss öffentlich + HTTPS sein — sie steckt in den Foto-URLs der Karten. Interner Hostname (http://kong:8000) funktioniert nicht für die Bildanzeige.- SMTP-Kollision: der Supabase-Stack belegt
SMTP_*selbst (GoTrue). Die App liest deshalbTEAMVIS_SMTP_*(siehe Override) — nicht vermischen. - Reihenfolge: Schema erst einspielen, wenn Storage einmal gestartet ist
(Migration
0044legt den Bucket instorage.bucketsan).bootstrap.shwartet darauf. - Keys sind JWTs:
ANON_KEY/SERVICE_ROLE_KEYsind mitJWT_SECRETsignierte Tokens (gen-keys.mjs). WirdJWT_SECRETgeändert, müssen beide Keys neu erzeugt werden. - Backups: jetzt liegt die DB beim Kunden. Postgres-Volume + Storage-
Volume sichern (
pg_dump+volumes/storage). Beim externen Supabase war das deren Sache. - 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:
- Basis-Schema fehlte — die Migrationen legen
employees/admin_usersnicht an (nur additiveadd column if not exists).bundle-migrationsbootstrappt eine leere DB also nicht. Gelöst: neue Migrationsupabase/migrations/0000_base_schema.sql(idempotent, auf Bestandsinstanzen ein No-Op). Betrifft auch denNEUKUNDE.md-Weg. - Rollen-Passwörter —
supabase/postgressetzt nursupabase_adminaufPOSTGRES_PASSWORD;authenticator/supabase_storage_adminbrauchen es manuell (Variante B). Variante A erledigt das. - service_role-Grants — frisch erzeugte Tabellen geben
service_rolekeine Rechte; auf echtem Supabase passiert das automatisch. Grants +bypassrlsnö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 |