Infrastruktur · Selbst hosten

Supabase — Datenbank, Anmeldung und KI-Gedächtnis auf dem eigenen Server

Tabellen, eine API ohne eigenen Server-Code, Konten mit Rechten pro Zeile, Dateien und ein Wissensspeicher, den eine KI durchsuchen kann. Alles in einer Postgres-Datenbank auf Deinem eigenen Server.

Worum geht's?

Wer eine kleine Anwendung baut, braucht fast immer dasselbe: eine Datenbank, eine Schnittstelle, Anmeldung, einen Ort für Dateien. Supabase liefert das in einem Paket, rund um PostgreSQL. In dieser Schulung betreibst Du es selbst und baust für die fiktive Nordwerk GmbH ein Backend mit Kundenanfragen, Teamkonten und Firmenwissen.

Der Kern ist Stufe 8: Die Zugriffsregeln stehen in der Datenbank, und wenn sie greifen, antwortet die API meist nicht mit einem Fehler, sondern mit einer leeren Liste. Stufe 6 zeigt vorher, was ohne Regeln passiert.

Danach wird die Datenbank zum Gedächtnis für eine KI: Firmentexte als Vektoren, eine Suche, die sechs von sechs Testfragen richtig zuordnet, und ein n8n-Ablauf, der mit Quellenangabe antwortet.

Selbstlern-Kurs zum Durchklicken. Dein Fortschritt wird lokal gespeichert.

? Für wen ist das interessant?

🧩
Wer mit KI Apps baut
Claude Code oder Codex schreiben die Oberfläche, Supabase ist das Backend mit Regeln, die auch fehlerhaften Code aufhalten.
🧠
Wer Firmenwissen für KI aufbereitet
Ein Vektorspeicher, den n8n, eigene Skripte und Anwendungen gemeinsam nutzen.
🔒
Wer Daten im Haus halten will
Alles auf dem eigenen Server, nichts offen im Netz, Sicherung mit geprüftem Rückweg.
🐳
Nach n8n und Docling
Der nächste Baustein im Lernpfad lokale KI im Betrieb.

↗ Was Du vorher brauchst

  1. Einen Server mit SSH-Zugang und 4 GB RAM oder mehr: Self-Hosting & VPS, SSH.
  2. Docker mit Compose: Docker.
  3. Für die Stufen 10 und 11: LM Studio mit einem Embedding-Modell und n8n auf demselben Server.

📚 Die zwölf Stufen

ℹ Stand der Dinge

Aufgebaut und geprüft am 24. September 2026 mit dem offiziellen Docker-Stack self-hosted/v0.8.2 (PostgreSQL 17.6, pgvector 0.8.2, Studio vom 07.09.2026) auf einem VPS mit Ubuntu 24.04 LTS. Embeddings mit bge-m3 aus LM Studio, Antworten in n8n mit GLM 5.3 über das AuxData-Gateway.

Supabase entwickelt sich schnell, Beschriftungen im Studio ändern sich. Was bleibt, sind die Begriffe: Table Editor, SQL Editor, Policies, publishable und secret key, Bucket, RPC. Danach suchen, nicht nach dem genauen Wortlaut.

Stufe 1

Was Supabase ist

Eine Postgres-Datenbank mit allem, was eine Anwendung drumherum braucht.

1 Postgres und sieben Dienste drumherum

Supabase ist keine eigene Datenbank. Im Kern läuft ein ganz normales PostgreSQL. Supabase stellt Dienste daneben, die man sonst einzeln einrichten müsste:

🗄️
Datenbank
PostgreSQL 17 mit Erweiterungen wie pgvector. Alles, was Du über SQL weißt, gilt hier.
🖥️
Studio
Die Weboberfläche: Tabellen, SQL-Editor, Nutzer, Regeln, Dateien.
🔌
Automatische API
Jede Tabelle ist sofort über HTTP lesbar und schreibbar (PostgREST). Kein eigener Server-Code nötig.
🪪
Anmeldung
Konten, Passwörter, Tokens (Auth). Die Datenbank weiß bei jeder Anfrage, wer fragt.
📁
Dateiablage
Buckets für Bilder und Dokumente (Storage), mit denselben Zugriffsregeln wie die Tabellen.
⚡
Realtime und Functions
Änderungen live an Browser schicken, eigene kleine Server-Funktionen. In dieser Schulung nur am Rand.

Das Wichtigste daran: Die Zugriffsregeln stehen in der Datenbank selbst (Row Level Security). Egal, ob eine Anfrage vom Browser, von n8n oder aus einem Skript kommt, Postgres prüft sie jedes Mal. Stufe 8 zeigt, warum das der Kern von Supabase ist.

2 Wann Supabase passt und wann nicht

Du willst …Passt Supabase?
Daten einer kleinen App oder eines Formulars ablegen, mit Konten und Rechten pro ZeileJa, genau dafür.
Firmenwissen für eine KI durchsuchbar machen, das mehrere Werkzeuge teilenJa, mit pgvector (Stufe 10 und 11).
Eine einzelne Liste für einen n8n-AblaufNein, die Datentabellen von n8n reichen (n8n, Stufe 9).
Eine Anmeldung für viele Dienste im UnternehmenNein, dafür ist Keycloak gebaut. Supabase Auth verwaltet die Konten einer Anwendung.
Dokumente mit einer KI befragen, ohne selbst etwas zu bauenEher nicht. Open WebUI und LibreChat haben eine eingebaute Dokumentensuche mit eigenem Vektorspeicher.

3 Was wir bauen

Die fiktive Nordwerk GmbH kennst Du vielleicht aus dem n8n-Kurs: ein kleiner Hersteller von Werkstattmöbeln. In dieser Schulung bekommt Nordwerk ein eigenes Backend:

  1. Eine Produktliste und eine Tabelle für Kundenanfragen (Stufe 4 und 5).
  2. Ein Kontaktformular darf Anfragen anlegen, aber nichts lesen. Nora und Paul aus dem Team sehen alle Anfragen und übernehmen sie (Stufe 6 bis 8).
  3. Fotos zu Reklamationen in einem privaten Bucket (Stufe 9).
  4. Das Firmenwissen als Vektoren, damit eine Frage wie „Wie lange dauert die Lieferung?“ den passenden Absatz findet (Stufe 10). n8n beantwortet damit Fragen mit Quellenangabe (Stufe 11).

Supabase gibt es als Cloud-Dienst (supabase.com) und zum Selbstbetreiben. Diese Schulung betreibt es selbst, auf Deinem Server. Die Oberfläche ist in beiden Fällen fast gleich, die Befehle zum Installieren und Sichern gelten nur für den eigenen Server.

✓ Kurz geprüft

Stufe 2

Voraussetzungen und Sicherheit

Was vor dem ersten Befehl stehen muss, und drei Regeln, die Du nicht brechen solltest.

1 Deine Ausgangslage

  1. Ein Linux-Server mit SSH-Zugang und mindestens 4 GB RAM, besser 8. Die Übungsinstanz braucht mit allen elf Containern rund 1,4 GB im Leerlauf (gemessen am 24.09.2026). Wie Du einen Server mietest und absicherst: Self-Hosting & VPS und SSH.
  2. Docker mit Compose und Git auf diesem Server. Prüfen mit docker compose version und git --version. Neu mit Containern? Dann zuerst die Docker-Schulung.
  3. Für Stufe 10: Python 3.9 oder neuer auf Deinem Rechner und ein Embedding-Modell, zum Beispiel text-embedding-bge-m3 in LM Studio.
  4. Für Stufe 11: ein n8n auf demselben Server, wie es der n8n-Kurs einrichtet, und ein Zugang zu einem Sprachmodell.
  5. Die Übungsdateien aus Befehle zum Kopieren: SQL für jede Stufe, Beispieldaten, das Einlese-Skript und den n8n-Ablauf.

2 Drei Regeln, bevor Du anfängst

🔒 Nichts davon gehört offen ins Internet. Studio, API und Datenbank lauschen in dieser Schulung nur auf 127.0.0.1. Du erreichst sie über einen SSH-Tunnel. Docker umgeht dabei die Firewall des Servers: Ein Port, der in der Compose-Datei ohne 127.0.0.1 steht, ist von überall erreichbar, auch wenn UFW ihn sperrt.
🔑 Alle Standard-Geheimnisse ersetzen. Die mitgelieferte .env.example enthält Beispielpasswörter und Beispielschlüssel, die jeder kennt. Stufe 3 erzeugt neue, bevor Supabase zum ersten Mal startet.
🗝️ Zwei Schlüssel, zwei Welten. Der öffentliche Schlüssel (sb_publishable_…) darf in eine Webseite, weil die Zugriffsregeln ihn begrenzen. Der geheime Schlüssel (sb_secret_…) umgeht alle Regeln. Er gehört nur auf Server, nie in einen Browser, ein Git-Repository oder einen Chat.
↩️ Zurück geht immer. sh reset.sh im Ordner /opt/supabase löscht Container und alle Daten der Übungsinstanz. Das Skript fragt vorher nach und bietet an, auch die .env zurückzusetzen. Danach kannst Du bei Stufe 3 neu anfangen.

✓ Kurz geprüft

Stufe 3

Installieren

Die offizielle Compose-Datei, eigene Schlüssel, alle Ports nur lokal. Auf dem Server, im Terminal.

1 Die Dateien holen

📍
Ausgangslage
Du bist per SSH als root auf dem Server angemeldet.
👉
Deine Handlung
Die sechs Befehle unten nacheinander.
✅
Das siehst Du danach
ls /opt/supabase zeigt unter anderem docker-compose.yml, .env, run.sh und den Ordner utils.
🛟
Wenn es anders aussieht
git clone meldet einen Fehler? Die Versionsbezeichnung muss exakt stimmen. Welche aktuell ist, steht in der offiziellen Anleitung unter Self-Hosting with Docker.
1Stack holen, feste Version
cd /opt
git clone --depth 1 --branch self-hosted/v0.8.2 https://github.com/supabase/supabase supabase-src
mkdir supabase && cp -rf supabase-src/docker/. supabase
cd supabase
cp .env.example .env
printf 'ref=self-hosted/v0.8.2\n' > .supabase-version
Wir nehmen eine feste Version (hier self-hosted/v0.8.2, die aktuelle am 24.09.2026) statt des neuesten Stands. So entscheidest Du, wann sich etwas ändert.

2 Alle Ports nur lokal

Die offizielle Datei öffnet die Ports 8000 (Studio und API), 5432 und 6543 (Datenbank) für alle Adressen. Statt sie zu ändern, legst Du eine kleine Zusatzdatei daneben. Supabase kennt dafür einen eigenen Mechanismus: Zusatzdateien heißen docker-compose.<name>.yml und werden mit sh run.sh config add <name> eingebunden.

2/opt/supabase/docker-compose.lokal.yml
# Alle Ports nur auf 127.0.0.1 (Zugriff per SSH-Tunnel).
# Docker umgeht die Firewall: ein Port ohne 127.0.0.1 wäre aus dem Internet erreichbar.
services:
  api-gw:
    ports: !override
      - 127.0.0.1:8000:8000/tcp
  supavisor:
    ports: !override
      - 127.0.0.1:5432:5432
      - 127.0.0.1:6543:6543
!override ersetzt die Portliste der Originaldatei vollständig, statt Einträge anzuhängen. Beim Update der Originaldatei bleibt Deine Zusatzdatei unberührt.

3 Eigene Schlüssel

3Zusatzdatei einbinden, Schlüssel erzeugen, Datei schützen
sh run.sh config add lokal
sh utils/generate-keys.sh --update-env
sh utils/add-new-auth-keys.sh --update-env
chmod 600 .env
grep -c 'this_password_is_insecure' .env      # muss 0 ergeben
Die beiden Skripte schreiben Passwörter und Schlüssel in die .env. Ohne --update-env geben sie die Werte nur aus und schreiben nichts, das ist uns beim Aufbau passiert. Die letzte Zeile zählt, wie oft das Beispielpasswort noch in der Datei steht.

4 Starten

4Bestätigungsmail abschalten, laden, starten
sed -i 's/^ENABLE_EMAIL_AUTOCONFIRM=false/ENABLE_EMAIL_AUTOCONFIRM=true/' .env
docker compose pull
sh run.sh start
docker compose ps
Die erste Zeile erlaubt Anmeldungen ohne Bestätigungsmail, weil die Übungsinstanz keinen Mailversand hat. Für echten Betrieb gehört dort ein SMTP-Server hinein und der Wert zurück auf false.
📍
Ausgangslage
Die .env enthält Deine eigenen Schlüssel.
👉
Deine Handlung
Die vier Befehle oben.
✅
Das siehst Du danach
Nach ein bis zwei Minuten meldet docker compose ps elf Container als healthy. Bei den Ports stehen nur Adressen mit 127.0.0.1.
🛟
Wenn es anders aussieht
Ein Container bleibt unhealthy? docker compose logs <name> lesen. Startet das API-Gateway nicht, hilft laut Anleitung oft ein neues Klonen: Die Dateien brauchen Linux-Zeilenenden.

5 Der Tunnel und der erste Blick

5Auf Deinem Rechner (PowerShell oder Terminal)
ssh -N -L 8000:127.0.0.1:8000 root@DEIN-SERVER
Solange dieses Fenster offen ist, erreichst Du Supabase unter http://localhost:8000. DEIN-SERVER steht für Adresse oder SSH-Alias.

Der Browser fragt nach Benutzername und Passwort. Beides steht in der .env. Anzeigen lassen kannst Du es auf dem Server mit sh run.sh secrets.

Supabase Studio nach der Anmeldung: Default Project, Adresse http://localhost:8000, Kacheln Framework, Server, Direct, ORM, MCP, API Keys
Die Startseite des Studios: ein Projekt pro Installation, links die Bereiche Tabellen, SQL, Datenbank, Anmeldung, Dateien.

✓ Kurz geprüft

Stufe 4

Studio und die erste Tabelle

Eine Produktliste per CSV-Import, ganz ohne SQL.

1 Tabelle anlegen

📍
Ausgangslage
Studio offen, links Table Editor.
👉
Deine Handlung
New table. Name: produkte. Den Haken bei Enable Row Level Security (RLS) gesetzt lassen.
✅
Das siehst Du danach
Rechts öffnet sich das Formular Create a new table under public mit zwei vorbereiteten Spalten id und created_at.
🛟
Wenn es anders aussieht
Der Knopf fehlt? Oben links muss das Schema public ausgewählt sein.
Formular Create a new table under public mit Name, Description, Enable Row Level Security angehakt, Hinweis Policies are required to query data
RLS ist vorausgewählt, und das Studio sagt gleich, was das bedeutet: Ohne Regel liefert die Tabelle ein leeres Array. Das siehst Du in Stufe 6 selbst.

2 Daten aus einer CSV-Datei

📍
Ausgangslage
Das Formular ist offen, der Name eingetragen.
👉
Deine Handlung
Unter Columns auf Import data from CSV, die Datei produkte.csv hineinziehen, unten Save.
✅
Das siehst Du danach
Die Vorschau zeigt vier Zeilen und drei Spalten: nummer, name, beschreibung. Zurück im Formular ersetzen sie die beiden vorbereiteten Spalten.
🛟
Wenn es anders aussieht
Umlaute kaputt? Die Datei muss als UTF-8 gespeichert sein. Die Kopfzeile darf nur Buchstaben, Ziffern, - und _ enthalten.
Seitenleiste Add content to new table mit der Datei produkte.csv und einer Vorschau von vier Zeilen
Die Vorschau vor dem Import: 4 Zeilen, 3 Spalten, alle als Text erkannt.

Jetzt meldet das Studio Warning: No primary keys selected. Ohne Primärschlüssel kann man Zeilen später nicht ändern oder löschen. Setze in der Zeile nummer den Haken bei Primary und speichere die Tabelle mit Save.

Formular nach dem CSV-Import mit der gelben Warnung No primary keys selected und der Spalte nummer vom Typ text
Die gelbe Warnung verschwindet, sobald nummer als Primärschlüssel markiert ist. Die Artikelnummer ist eindeutig, eine zusätzliche id braucht es nicht.

✓ Kurz geprüft

Stufe 5

SQL und Beziehungen

Die Anfragen-Tabelle als SQL: mit Pflichtfeldern, erlaubten Werten und einem Verweis auf die Produkte.

1 Warum SQL

Das Studio ist bequem, aber eine SQL-Datei kannst Du aufheben, weitergeben und auf einer zweiten Instanz genauso ausführen. Für alles, was Bestand haben soll, ist SQL der bessere Weg. Deshalb liegt jede weitere Stufe als SQL-Datei in demo/.

📍
Ausgangslage
Studio offen, links SQL Editor.
👉
Deine Handlung
Den Inhalt von schema.sql einfügen, oben rechts Run (oder Strg+Enter).
✅
Das siehst Du danach
Unten meldet der Editor Erfolg, ohne Ergebniszeilen. Im Table Editor steht die neue Tabelle anfragen mit drei Beispielzeilen.
🛟
Wenn es anders aussieht
relation "public.produkte" does not exist: Stufe 4 fehlt oder die Tabelle heißt anders. Der Verweis braucht genau produkte(nummer).
1demo/schema.sql
-- Stufe 5 · Die Tabelle anfragen
-- Im Studio: SQL Editor → New query → einfügen → Run.
-- Setzt voraus, dass die Tabelle produkte aus Stufe 4 existiert.

create table public.anfragen (
  id             bigint generated always as identity primary key,
  erstellt_am    timestamptz not null default now(),
  name           text not null,
  email          text not null,
  anliegen       text not null,
  produkt        text references public.produkte (nummer),
  kategorie      text check (kategorie in ('Angebot', 'Support', 'Beschwerde', 'Sonstiges')),
  status         text not null default 'offen' check (status in ('offen', 'in Arbeit', 'erledigt')),
  bearbeitet_von uuid references auth.users (id)
);

create index anfragen_status_idx on public.anfragen (status);

insert into public.anfragen (name, email, anliegen, produkt, kategorie) values
  ('Jana Weber', 'jana.weber@example.com', 'Was kostet die WB-180 mit Buchenplatte? Wir bräuchten drei Stück.', 'WB-180', 'Angebot'),
  ('Tim Brandt', 'tim.brandt@example.com', 'Die Tür am TS-90 schließt nicht mehr richtig.', 'TS-90', 'Support'),
  ('Eva Lorenz', 'eva.lorenz@example.com', 'Liefert ihr auch nach Österreich?', null, 'Sonstiges');
check lässt nur die genannten Werte zu, references nur Produktnummern, die es gibt. bearbeitet_von verweist auf die Nutzerkonten aus Stufe 7. Diese Regeln prüft die Datenbank bei jedem Schreiben, egal woher es kommt.
Table Editor mit der Tabelle anfragen: Spalten id, erstellt_am, name, email, anliegen und vier Zeilen
Die Tabelle im Table Editor. Oben rechts zählt der Knopf RLS policies die Regeln, die Stufe 8 hinzufügt.

2 Eine Abweichung, die Du kennen solltest

⚠️ Per SQL angelegte Tabellen haben kein RLS. Das Studio setzt den Haken von selbst, create table nicht. anfragen ist in diesem Moment für jeden mit dem öffentlichen Schlüssel lesbar und änderbar. Das ist Absicht, damit Stufe 6 es zeigen kann. Stufe 8 schließt die Lücke.

Unter Database → Policies bietet das Studio an, RLS für alle künftigen Tabellen automatisch einzuschalten (Automatically enable Row Level Security (RLS) on new tables → Set up trigger). Für eigene Projekte ist das eine gute Voreinstellung.

✓ Kurz geprüft

Stufe 6

Die automatische API

Jede Tabelle ist sofort über HTTP erreichbar. Was das mit einer ungeschützten Tabelle bedeutet, zeigt diese Stufe.

1 Lesen mit dem öffentlichen Schlüssel

Die folgenden Befehle laufen auf dem Server. Sie tun genau das, was später eine Webseite mit dem öffentlichen Schlüssel tun kann.

1Schlüssel in Variablen legen (auf dem Server)
cd /opt/supabase
P=$(grep ^SUPABASE_PUBLISHABLE_KEY= .env | cut -d= -f2-)
U=http://127.0.0.1:8000
2Beide Tabellen lesen
curl -s "$U/rest/v1/produkte?select=nummer,name" -H "apikey: $P"
curl -s "$U/rest/v1/anfragen?select=id,name,status" -H "apikey: $P"
3Ausgabe (gemessen am 24.09.2026)
[]
[{"id":1,"name":"Jana Weber","status":"offen"},
 {"id":2,"name":"Tim Brandt","status":"offen"},
 {"id":3,"name":"Eva Lorenz","status":"offen"}]
produkte hat RLS, aber noch keine Regel, also sieht niemand etwas, ohne Fehlermeldung. anfragen hat kein RLS, also sieht jeder alles, inklusive E-Mail-Adressen.

2 Ändern ohne Anmeldung

4Eine Anfrage als erledigt markieren, ohne angemeldet zu sein
curl -s -X PATCH "$U/rest/v1/anfragen?id=eq.3" -H "apikey: $P" \
  -H "Content-Type: application/json" -H "Prefer: return=representation" \
  -d '{"status":"erledigt"}'
Es klappt. Die Antwort enthält die geänderte Zeile mit "status":"erledigt". Mit derselben Methode ließe sich jede Zeile löschen. Setze den Status danach im Table Editor wieder auf offen.
⚠️ Genau so entstehen die Datenlecks, über die man bei Supabase-Projekten liest: eine Tabelle ohne RLS und ein öffentlicher Schlüssel, der in jeder Webseite steht. Der Security Advisor im Studio meldet das als Fehler.
Security Advisor mit einem Fehler RLS Disabled in Public für eine Tabelle
Der Security Advisor mit dem Fehler RLS Disabled in Public, hier für eine Testtabelle. Nach Stufe 8 zeigt er für die Nordwerk-Tabellen keine Fehler und keine Warnungen mehr.

3 Die Adresse verrät die Abfrage

Filter und Spaltenauswahl stehen in der URL: ?select=id,name wählt Spalten, ?status=eq.offen filtert, ?order=erstellt_am.desc sortiert. Ohne apikey antwortet die API mit Unauthorized. Die Startadresse /rest/v1/ selbst antwortet dem öffentlichen Schlüssel mit 403. Das ist kein Defekt, die Liste aller Tabellen ist nur dem geheimen Schlüssel vorbehalten.

✓ Kurz geprüft

Stufe 7

Anmeldung

Nora bekommt ein Konto. Aus dem Konto wird ein Token, und das Token sagt der Datenbank, wer fragt.

1 Registrieren und anmelden

1Nora registriert sich (auf dem Server, Variablen aus Stufe 6)
curl -s -X POST "$U/auth/v1/signup" -H "apikey: $P" -H "Content-Type: application/json" \
  -d '{"email":"nora@nordwerk.example","password":"Werkbank-2026"}'
Die Antwort enthält ein access_token, gültig 3600 Sekunden, und den Nutzer mit der Rolle authenticated. Ohne die Einstellung aus Stufe 3 käme stattdessen der Hinweis auf eine Bestätigungsmail.
2Später wieder anmelden und das Token merken
T=$(curl -s -X POST "$U/auth/v1/token?grant_type=password" -H "apikey: $P" \
  -H "Content-Type: application/json" \
  -d '{"email":"nora@nordwerk.example","password":"Werkbank-2026"}' | python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])")
echo "$T" | cut -c1-25

Lege auf dieselbe Weise einen Kollegen an: paul@nordwerk.example mit dem Passwort Hobelbank-2026. Stufe 8 braucht zwei Personen.

Nutzerliste im Studio mit nora@nordwerk.example und Anbieter Email
Unter Authentication → Users steht Nora mit ihrer Nutzer-ID. Über Add user lassen sich Konten auch hier anlegen.

2 Was im Token steckt

Das Token ist ein signierter Ausweis (JWT). Er enthält Noras Nutzer-ID und die Rolle authenticated. Schickt eine Anfrage es im Kopf Authorization: Bearer … mit, weiß Postgres, wer fragt. In den Zugriffsregeln heißt die ID dann auth.uid(). Ohne Token ist die Rolle anon.

💡 In einer echten Webseite erledigt das die Bibliothek supabase-js: Anmeldung, Token merken, Token mitschicken, Token erneuern. Die curl-Befehle zeigen, was dabei unter der Haube passiert.

✓ Kurz geprüft

Stufe 8

Zugriffsregeln (RLS)

Das Formular darf anlegen, das Team darf lesen und übernehmen. Und die leisen Antworten, wenn eine Regel greift.

1 Die Regeln

📍
Ausgangslage
Stufe 6 und 7 sind erledigt.
👉
Deine Handlung
Den Inhalt von rls.sql im SQL Editor ausführen.
✅
Das siehst Du danach
Unter Database → Policies stehen vier Regeln: drei für anfragen, eine für produkte.
🛟
Wenn es anders aussieht
policy … already exists: Die Datei lief schon einmal. Regeln einzeln mit drop policy "name" on public.anfragen; entfernen und neu anlegen.
1demo/rls.sql
-- Stufe 8 · Row Level Security
-- Ohne diese Datei darf jeder mit dem öffentlichen Schlüssel alle Anfragen lesen, ändern und löschen.

alter table public.anfragen enable row level security;

-- Das Kontaktformular (ohne Anmeldung) darf nur neue, offene Anfragen anlegen. Lesen darf es nichts.
create policy "formular legt an" on public.anfragen
  for insert to anon
  with check (status = 'offen' and bearbeitet_von is null);

-- Angemeldete Mitarbeitende sehen alle Anfragen ...
create policy "team liest" on public.anfragen
  for select to authenticated
  using (true);

-- ... übernehmen freie Anfragen und bearbeiten ihre eigenen. Eintragen dürfen sie dabei nur sich selbst.
create policy "team bearbeitet" on public.anfragen
  for update to authenticated
  using (bearbeitet_von is null or bearbeitet_von = (select auth.uid()))
  with check (bearbeitet_von = (select auth.uid()));

-- Die Produktliste ist öffentlich lesbar.
create policy "alle lesen produkte" on public.produkte
  for select to anon, authenticated
  using (true);
using bestimmt, welche vorhandenen Zeilen eine Rolle sehen oder ändern darf. with check prüft, wie eine Zeile nach dem Schreiben aussehen muss. (select auth.uid()) in Klammern rechnet die Nutzer-ID einmal pro Abfrage statt einmal pro Zeile.
Policies-Seite: anfragen mit formular legt an (INSERT, anon), team bearbeitet (UPDATE, authenticated), team liest (SELECT, authenticated), produkte mit alle lesen produkte
Die Regeln im Studio. Für jede Tabelle siehst Du Befehl und Rolle auf einen Blick.
📝 Beim Aufbau gefunden: Die erste Fassung erlaubte dem Team, jede Anfrage zu ändern (using (true)). Der Security Advisor meldete das als Warnung RLS Policy Always True. Die jetzige Fassung lässt nur freie oder eigene Anfragen zu.

2 Jede Regel einmal ausprobieren

Gemessen am 24.09.2026. Die Tabelle zeigt, was die API bei welchem Versuch antwortet:

WerVersuchAntwort
ohne AnmeldungAnfragen lesen[], kein Fehler
ohne AnmeldungProdukte lesenalle vier Produkte
ohne AnmeldungAnfrage 3 auf erledigt setzen200 und []: Nichts wurde geändert, aber es kommt keine Fehlermeldung
ohne Anmeldungneue Anfrage mit Prefer: return=minimal201, angelegt
ohne Anmeldungneue Anfrage mit return=representation401, new row violates row-level security policy, nichts angelegt
ohne Anmeldungneue Anfrage mit Status erledigt401, abgelehnt
NoraAnfragen lesenalle Anfragen
Paulfreie Anfrage 2 übernehmen200, übernommen
NoraPauls Anfrage 2 ändern200 und [], nichts geändert
Norafremde Nutzer-ID eintragen403, abgelehnt
Noraeine Anfrage löschen200 und []: Es gibt keine Löschregel, also wird nichts gelöscht
2Zwei der Versuche zum Nachmachen
# Das Kontaktformular legt eine Anfrage an (ohne Anmeldung)
curl -s -w "%{http_code}\n" -X POST "$U/rest/v1/anfragen" -H "apikey: $P" \
  -H "Content-Type: application/json" -H "Prefer: return=minimal" \
  -d '{"name":"Ali Kaya","email":"ali@example.com","anliegen":"Gibt es die WB-120 auch in Eiche?","produkt":"WB-120"}'

# Nora liest alle Anfragen (Token T aus Stufe 7)
curl -s "$U/rest/v1/anfragen?select=id,name,status" -H "apikey: $P" -H "Authorization: Bearer $T"
⚠️ Die zwei Fallen dieser Stufe. Erstens: Eine Regel, die nicht greift, meldet selten einen Fehler. Lesen, Ändern und Löschen liefern dann einfach eine leere Liste. Wer „es passiert nichts“ sieht, sollte zuerst die Regeln prüfen. Zweitens: Ein Formular darf nicht return=representation verlangen. Die neue Zeile zurückzugeben, wäre ein Lesezugriff, und den hat das Formular nicht. Dann scheitert das ganze Anlegen.

✓ Kurz geprüft

Stufe 9

Dateien ablegen

Ein privater Bucket für Fotos zu Reklamationen, geschützt mit denselben Regeln.

1 Bucket und Regeln

📍
Ausgangslage
RLS aus Stufe 8 steht.
👉
Deine Handlung
Den Inhalt von storage.sql im SQL Editor ausführen.
✅
Das siehst Du danach
Unter Storage erscheint der Bucket anhaenge, oben rechts Policies 2.
🛟
Wenn es anders aussieht
duplicate key: Den Bucket gibt es schon. Die erste Anweisung weglassen und nur die Regeln ausführen.
1demo/storage.sql
-- Stufe 9 · Ein privater Bucket für Anhänge (Fotos zu Reklamationen)
-- Dasselbe geht im Studio unter Storage → New bucket; die Richtlinien stehen dort unter Policies.

insert into storage.buckets (id, name, public)
values ('anhaenge', 'anhaenge', false);

create policy "team lädt hoch" on storage.objects
  for insert to authenticated
  with check (bucket_id = 'anhaenge');

create policy "team liest anhänge" on storage.objects
  for select to authenticated
  using (bucket_id = 'anhaenge');
Dateien sind für Supabase Zeilen in der Tabelle storage.objects. Deshalb gelten für sie dieselben Regeln wie für Deine Tabellen.

2 Hochladen und abholen

2Nora lädt ein Foto hoch (hier eine Textdatei als Platzhalter)
printf "Foto der Tür am TS-90" > tuer.txt
curl -s -X POST "$U/storage/v1/object/anhaenge/anfrage-2/tuer.txt" -H "apikey: $P" \
  -H "Authorization: Bearer $T" -H "Content-Type: text/plain" --data-binary @tuer.txt
Antwort: {"Key":"anhaenge/anfrage-2/tuer.txt", …}. Derselbe Befehl ohne Token wird mit new row violates row-level security policy abgelehnt.
3Abholen: angemeldet oder mit befristetem Link
curl -s "$U/storage/v1/object/authenticated/anhaenge/anfrage-2/tuer.txt" -H "apikey: $P" -H "Authorization: Bearer $T"
curl -s -X POST "$U/storage/v1/object/sign/anhaenge/anfrage-2/tuer.txt" -H "apikey: $P" \
  -H "Authorization: Bearer $T" -H "Content-Type: application/json" -d '{"expiresIn":60}'
Der zweite Befehl liefert eine signedURL, die 60 Sekunden lang ohne Anmeldung funktioniert. So gibst Du eine Datei gezielt weiter, ohne den Bucket zu öffnen.
Storage-Ansicht mit dem Bucket anhaenge, Ordner anfrage-2 und Datei tuer.txt
Die Datei im Studio. Oben rechts Policies 2: die beiden Regeln aus storage.sql.
💡 Die öffentliche Adresse /storage/v1/object/public/anhaenge/… antwortet bei einem privaten Bucket mit Bucket not found, obwohl es den Bucket gibt. Das ist gewollt: Nach außen verrät Supabase nicht, welche privaten Buckets existieren.
⚡ Realtime und Edge Functions laufen in Deiner Instanz mit, diese Schulung nutzt sie nur am Rand. Realtime schickt Änderungen einer Tabelle live an angemeldete Browser, etwa eine neue Anfrage in die Ansicht des Teams. Edge Functions sind kleine TypeScript-Funktionen im Ordner volumes/functions/, erreichbar unter /functions/v1/<name>.

✓ Kurz geprüft

Stufe 10

Firmenwissen als Vektoren

Texte als Zahlenreihen speichern, damit eine Frage den passenden Absatz findet, auch mit anderen Worten.

1 Was ein Vektor hier ist

Ein Embedding-Modell macht aus einem Text eine lange Reihe von Zahlen, bei bge-m3 sind es 1024. Texte mit ähnlicher Bedeutung bekommen ähnliche Zahlenreihen. „Wie lange dauert es, bis meine Werkbank da ist?“ landet so nahe bei „Die Lieferzeit beträgt zwei bis drei Wochen“, obwohl kein Wort gleich ist. pgvector speichert solche Reihen in Postgres und findet die ähnlichsten.

1demo/vektor.sql
-- Stufe 10 · Firmenwissen als Vektoren
-- 1024 Werte passen zu bge-m3 (LM Studio: text-embedding-bge-m3). Ein anderes Modell braucht
-- eine andere Zahl, und alte Vektoren müssen neu berechnet werden.

create extension if not exists vector with schema extensions;

create table public.wissen (
  id        bigint generated always as identity primary key,
  quelle    text not null,
  inhalt    text not null,
  embedding extensions.vector(1024) not null
);

create index wissen_embedding_idx on public.wissen
  using hnsw (embedding extensions.vector_cosine_ops);

alter table public.wissen enable row level security;

create policy "team liest wissen" on public.wissen
  for select to authenticated
  using (true);

-- Sucht die Abschnitte, die einer Frage am ähnlichsten sind.
-- aehnlichkeit: 1 = gleiche Bedeutung, 0 = nichts gemeinsam.
create or replace function public.match_wissen (
  frage      extensions.vector(1024),
  anzahl     int   default 3,
  mindestens float default 0.5
)
returns table (id bigint, quelle text, inhalt text, aehnlichkeit float)
language sql stable
set search_path = ''
as $$
  select w.id, w.quelle, w.inhalt,
         1 - (w.embedding operator(extensions.<=>) frage) as aehnlichkeit
  from public.wissen w
  where 1 - (w.embedding operator(extensions.<=>) frage) >= mindestens
  order by w.embedding operator(extensions.<=>) frage
  limit anzahl;
$$;
vector(1024) muss zum Modell passen. Der Index hnsw hält die Suche auch bei vielen tausend Abschnitten schnell. match_wissen gibt nur Treffer ab einer Mindestähnlichkeit zurück, damit eine Frage ohne passende Antwort auch keine bekommt.

2 Einlesen von Deinem Rechner

📍
Ausgangslage
vektor.sql ist ausgeführt, der Tunnel aus Stufe 3 läuft, LM Studio hat text-embedding-bge-m3 geladen und den Server gestartet.
👉
Deine Handlung
Die Übungsdateien in einen Ordner legen (wissen_einlesen.py und der Ordner wissen/). In PowerShell die Zeilen unten, den geheimen Schlüssel aus der .env auf dem Server einsetzen.
✅
Das siehst Du danach
Das Skript listet sieben Abschnitte und endet mit 7 Abschnitte eingelesen (text-embedding-bge-m3, 1024 Werte).
🛟
Wenn es anders aussieht
nicht erreichbar: Tunnel oder LM-Studio-Server läuft nicht. Das Modell liefert 768 Werte: anderes Modell geladen. HTTP 401: Schlüssel unvollständig kopiert.
2PowerShell, im Ordner mit dem Skript
$env:SUPABASE_URL = "http://localhost:8000"
$env:SUPABASE_SECRET_KEY = "sb_secret_…"          # aus /opt/supabase/.env
$env:EMBED_URL = "http://localhost:1234/v1"       # LM Studio
$env:EMBED_MODEL = "text-embedding-bge-m3"
python wissen_einlesen.py
Das Skript nutzt nur die Standardbibliothek von Python. Mit --leeren löscht es vorher alle Abschnitte, sonst hängt es an. Den geheimen Schlüssel braucht es, weil die Tabelle wissen keine Schreibregel hat: Einlesen ist Sache des Betreibers, nicht der Nutzer.

Statt LM Studio geht jede Schnittstelle, die /v1/embeddings im OpenAI-Format anbietet und bge-m3 liefert, zum Beispiel das AuxData-Gateway mit EMBED_URL=https://auxdata.ai/v1, EMBED_MODEL=bge-m3 und Deinem Schlüssel in EMBED_KEY. Gemessen am 24.09.2026: Die Vektoren beider Quellen stimmen zu 99,94 % überein, Du kannst sie mischen.

3 Suchen, und warum das Modell zählt

Sechs Testfragen aus fragen.txt, als Nora über die API gegen match_wissen gestellt. So sah das Ergebnis aus:

Fragebester TrefferÄhnlichkeit
Wie lange dauert es, bis meine Werkbank da ist?lieferung.md: Spedition, zwei bis drei Wochen0,564
Mein Schrank ist beschädigt angekommen, was muss ich tun?garantie.md: Fotos und Rechnungsnummer schicken0,516
Wie viel Gewicht hält die Werkbank aus?produkte.md: WB-180 trägt 500 kg0,740
Kommt ihr auch zum Aufbauen nach München?lieferung.md: Aufbau im Umkreis von 150 km0,590
Wie pflege ich die Holzplatte?pflege.md: Arbeitsplattenöl0,637
Ist der Schrank abschließbar?produkte.md: TS-90, abschließbar0,548
Was kostet ein Rollwagen?kein Treffer über 0,5–

Sechs von sechs richtig, und die Preisfrage, zu der das Firmenwissen nichts sagt, bekommt bewusst keinen Treffer. Ohne Anmeldung liefert die Suche [], weil wissen nur für das Team lesbar ist.

📝 Gegenprobe mit einem anderen Modell. Supabase bringt in den Edge Functions ein eigenes kleines Modell mit (gte-small, 384 Werte). Mit denselben Texten traf es nur drei von sechs Fragen, und alle Ähnlichkeiten lagen zwischen 0,85 und 0,89, richtige wie falsche. Das Modell ist auf Englisch trainiert. Für deutsche Texte braucht es ein mehrsprachiges Modell wie bge-m3.
💡 Die Grenze von 0,5 ist ein Erfahrungswert für diese Texte und dieses Modell. Der schwächste richtige Treffer lag bei 0,516. Mit eigenen Texten lohnt es sich, ein paar Fragen durchzuprobieren, bevor man die Grenze festlegt. Wer später das Modell wechselt, muss alle Abschnitte neu einlesen: Vektoren zweier Modelle passen nicht zusammen.

✓ Kurz geprüft

Stufe 11

n8n anbinden

Ein Formular nimmt eine Frage an, n8n sucht im Firmenwissen und lässt ein Sprachmodell mit Quellenangabe antworten.

1 n8n ins Supabase-Netz

n8n und Supabase laufen auf demselben Server, aber in getrennten Docker-Netzen. Über localhost erreicht n8n Supabase nicht: localhost ist im Container der Container selbst. Die Lösung ist ein gemeinsames Netz. Dann heißt die Adresse http://api-gw:8000, nach dem Namen des Gateways in der Supabase-Compose-Datei.

1/opt/n8n/compose.yml ergänzen, dann docker compose up -d
services:
  n8n:
    # … wie bisher …
    networks: [default, supabase]

networks:
  supabase:
    external: true
    name: supabase_default
supabase_default ist das Netz, das Supabase beim Start anlegt. Supabase muss also zuerst laufen. Beim Anhalten von Supabase meldet Compose danach Network supabase_default Resource is still in use. Das ist harmlos: n8n hängt noch darin.

2 Der Zugang und der Ablauf

📍
Ausgangslage
n8n ist im Supabase-Netz, in n8n gibt es einen Zugang zu einem Sprachmodell (im n8n-Kurs: AuxData-Gateway).
👉
Deine Handlung
In n8n Credentials → Create credential → Supabase API: Host http://api-gw:8000, Service Role Secret = Dein geheimer Schlüssel. Dann einen neuen Workflow öffnen und den Inhalt von n8n-wissenssuche.json mit Strg+V einfügen. In den Knoten Frage in Vektor, Wissen suchen und GLM über AuxData jeweils den passenden Zugang auswählen.
✅
Das siehst Du danach
Neun Knoten: Formular, zwei HTTP-Aufrufe, Fundstellen bündeln, eine Abzweigung, die Antwortkette mit Modell und zwei Abschlussseiten.
🛟
Wenn es anders aussieht
Ein Knoten ist rot markiert? Meist fehlt der Zugang. Bei 404 im Modell-Knoten: Use Responses API ausschalten (n8n-Kurs, Stufe 7).
  1. Frage stellen: ein Formular, nur für angemeldete n8n-Nutzer.
  2. Frage in Vektor: ruft /v1/embeddings mit dem Modell bge-m3 auf. Es muss dasselbe Modell sein wie beim Einlesen.
  3. Wissen suchen: ruft /rest/v1/rpc/match_wissen auf. Jede SQL-Funktion ist unter /rpc/<name> erreichbar.
  4. Fundstellen bündeln: nummeriert die Treffer, damit das Modell sie zitieren kann. Always Output Data am Suchknoten sorgt dafür, dass es auch ohne Treffer weitergeht.
  5. Antwort schreiben: das Modell darf nur die Abschnitte benutzen und muss sie in eckigen Klammern nennen. Ohne Treffer gibt es keine Antwort vom Modell, sondern die Seite Nichts gefunden.
🔑 Der Zugang Supabase API verwendet den geheimen Schlüssel und umgeht damit RLS. Das ist für einen Ablauf auf dem Server in Ordnung. Wer daraus einen öffentlich erreichbaren Chat baut, lässt n8n nur das zurückgeben, was die fragende Person sehen darf.

Beim Aufbau durchgespielt, mit n8n 2.40.5 und GLM 5.3. Auf die Frage „Wie lange dauert es, bis meine Werkbank da ist?“ zeigte die Antwortseite nach rund drei Sekunden:

„Die Lieferzeit innerhalb Deutschlands beträgt in der Regel zwei bis drei Wochen ab Auftragsbestätigung [1]. Die Ware wird dabei bis zur Bordsteinkante geliefert [1].“
Quellen: lieferung.md, produkte.md, garantie.md

„Was kostet ein Rollwagen?“ führte zur Seite Nichts gefunden, ohne dass das Sprachmodell überhaupt gefragt wurde. Die Quellenliste nennt alle Treffer über der Mindestähnlichkeit; welcher davon die Antwort trägt, zeigt die Nummer in eckigen Klammern.

3 Andere Wege

  1. Docling zerlegt PDFs in sauberen Text (Docling-Kurs). Dessen Markdown in den Ordner wissen/ gelegt, liest wissen_einlesen.py Handbücher und Produktblätter genauso ein.
  2. Der Knoten „Supabase Vector Store“ in n8n erledigt Einlesen und Suchen in einem, erwartet aber eine Tabelle und Funktion nach den Namensregeln von LangChain (documents, match_documents). Der Weg über HTTP-Aufrufe zeigt, was passiert, und passt zu jeder eigenen Tabelle.
  3. Claude Code oder Codex schreiben Dir eine kleine Web-App gegen dieselbe API, mit supabase-js und dem öffentlichen Schlüssel. Die Regeln aus Stufe 8 schützen die Daten dann auch vor Fehlern im erzeugten Code.
🔒 Der MCP-Zugang im Studio bleibt aus. Das Studio kann KI-Agenten über MCP direkt an die Datenbank lassen. In der selbst betriebenen Fassung ist dieser Weg ab Werk gesperrt, und das Freischalten nennt die Konfiguration selbst danger zone: Die Route hat keine Passwortabfrage. Einem Agenten gibst Du besser SQL-Dateien, die Du liest, bevor Du sie ausführst.

✓ Kurz geprüft

Stufe 12

Sichern und Betrieb

Zwei Arten von Sicherung, ein Rückweg, der wirklich ausprobiert wurde, und was im Alltag zählt.

1 Zwei Arten zu sichern

1demo/backup.sh (nach /opt/supabase kopieren)
#!/bin/sh
# backup.sh — sichert die Supabase-Übungsinstanz in /opt/supabase/backups/<Zeitpunkt>/
#
#   sh backup.sh              # im laufenden Betrieb: Dump Deiner Tabellen (Schema public)
#   sh backup.sh --komplett   # hält Supabase kurz an und sichert alles: Datenbankdateien,
#                             # Storage-Dateien, Funktionen, .env und das Volume db-config
#
# Der Dump reicht, um eine versehentlich gelöschte Tabelle zurückzuholen.
# Vor einem Update braucht es die komplette Sicherung, sonst gibt es keinen Rückweg.
set -eu
cd /opt/supabase
ZIEL="backups/$(date +%Y-%m-%d-%H%M)"
mkdir -p "$ZIEL"

docker exec supabase-db pg_dump -U postgres -d postgres -n public -Fc > "$ZIEL/public.dump"
echo "Dump: $ZIEL/public.dump ($(du -h "$ZIEL/public.dump" | cut -f1))"

if [ "${1:-}" = "--komplett" ]; then
  sh run.sh stop
  tar czf "$ZIEL/volumes.tar.gz" volumes
  docker run --rm -v supabase_db-config:/quelle:ro -v "$PWD/$ZIEL":/ziel alpine \
    tar czf /ziel/db-config.tar.gz -C /quelle .
  cp .env "$ZIEL/env.sicherung"
  sh run.sh start
  echo "Komplett: $ZIEL ($(du -sh "$ZIEL" | cut -f1))"
fi
chmod -R go-rwx backups
Der Dump läuft im Betrieb und enthält Deine Tabellen im Schema public. Die komplette Sicherung hält Supabase an und packt alles ein: Datenbankdateien, Dateiablage, Funktionen, die .env und das Volume db-config, das interne Schlüssel der Datenbank enthält. Beim Aufbau: 9,3 MB, Supabase war 55 Sekunden weg.

2 Eine Tabelle zurückholen

2Versehentlich geleert? Aus dem Dump zurück (Beispiel: wissen)
D=backups/2026-09-24-1500        # Deine Sicherung
docker exec -i supabase-db pg_restore -U postgres -d postgres --data-only -t wissen < $D/public.dump
Beim Aufbau ausprobiert: delete from wissen, dann dieser Befehl, danach waren alle sieben Abschnitte samt Vektoren wieder da.

3 Der Rückweg für alles

3Alles auf den Stand einer kompletten Sicherung
cd /opt/supabase
D=backups/2026-09-24-1500
sh run.sh stop
mv volumes volumes.vorher
tar xzf $D/volumes.tar.gz
docker run --rm -v supabase_db-config:/ziel -v "$PWD/$D":/quelle:ro alpine \
  sh -c "rm -rf /ziel/* && tar xzf /quelle/db-config.tar.gz -C /ziel"
sh run.sh start
Beim Aufbau ausprobiert: Eine Tabelle, die nach der Sicherung angelegt wurde, war danach weg, alle Daten und Nutzerkonten von vorher waren da, Nora konnte sich anmelden. volumes.vorher löschst Du erst, wenn alles läuft.
⚠️ Vor jedem Update die komplette Sicherung. Supabase bringt ein eigenes Skript update.sh mit. Wir haben es in dieser Schulung nicht ausprobiert. Welche Version aktuell ist und was sich ändert, steht in der CHANGELOG.md und in der offiziellen Anleitung. Ohne komplette Sicherung gibt es keinen Rückweg: Die Datenbank wird beim Start einer neuen Version umgebaut.

4 Im Alltag

4Drei Blicke
docker stats --no-stream --format "{{.Name}} {{.MemUsage}}" | grep supabase
docker compose logs --since 24h | grep -i error | tail
du -sh backups/*
  1. Sicherungen weg vom Server: scp -r root@DEIN-SERVER:/opt/supabase/backups ./supabase-sicherungen. Eine Sicherung auf derselben Platte hilft nicht, wenn der Server verloren geht.
  2. Security Advisor nach jeder Änderung an Tabellen oder Regeln öffnen und Rerun linter.
  3. Mailversand einrichten, bevor echte Menschen sich registrieren, und die automatische Bestätigung wieder abschalten.
  4. Öffentlich erreichbar wird die API erst mit Domain, HTTPS und Reverse Proxy (Self-Hosting). Das Studio bleibt auch dann besser hinter dem Tunnel.
💡 Wer so ein Backend für den eigenen Betrieb aufbauen will, mit echten Daten und Anbindung an bestehende Systeme: Das ist Stoff der Live-Schulung Automatisierung, siehe Karte links unten.

✓ Kurz geprüft

Bonus

Befehle zum Kopieren

Alles aus den zwölf Stufen an einer Stelle, dazu die Übungsdateien.

1 Übungsdateien

  1. produkte.csv: Produktliste für Stufe 4
  2. schema.sql: Tabelle anfragen, Stufe 5
  3. rls.sql: Zugriffsregeln, Stufe 8
  4. storage.sql: Bucket und Regeln, Stufe 9
  5. vektor.sql: pgvector, Tabelle wissen, Suchfunktion, Stufe 10
  6. wissen_einlesen.py: Einlese-Skript, Stufe 10
  7. fragen.txt: sechs Testfragen, Stufe 10
  8. n8n-wissenssuche.json: n8n-Ablauf, Stufe 11
  9. backup.sh: Sicherung, Stufe 12
  10. Ordner wissen/: produkte.md, lieferung.md, garantie.md, pflege.md

2 Installation

1Stack holen (/opt)
cd /opt
git clone --depth 1 --branch self-hosted/v0.8.2 https://github.com/supabase/supabase supabase-src
mkdir supabase && cp -rf supabase-src/docker/. supabase
cd supabase
cp .env.example .env
printf 'ref=self-hosted/v0.8.2\n' > .supabase-version
2/opt/supabase/docker-compose.lokal.yml
# Alle Ports nur auf 127.0.0.1 (Zugriff per SSH-Tunnel).
# Docker umgeht die Firewall: ein Port ohne 127.0.0.1 wäre aus dem Internet erreichbar.
services:
  api-gw:
    ports: !override
      - 127.0.0.1:8000:8000/tcp
  supavisor:
    ports: !override
      - 127.0.0.1:5432:5432
      - 127.0.0.1:6543:6543
3Schlüssel
sh run.sh config add lokal
sh utils/generate-keys.sh --update-env
sh utils/add-new-auth-keys.sh --update-env
chmod 600 .env
grep -c 'this_password_is_insecure' .env      # muss 0 ergeben
4Starten
sed -i 's/^ENABLE_EMAIL_AUTOCONFIRM=false/ENABLE_EMAIL_AUTOCONFIRM=true/' .env
docker compose pull
sh run.sh start
docker compose ps
5Tunnel (auf Deinem Rechner)
ssh -N -L 8000:127.0.0.1:8000 root@DEIN-SERVER

3 Im Betrieb

6Schlüssel für curl (auf dem Server)
cd /opt/supabase
P=$(grep ^SUPABASE_PUBLISHABLE_KEY= .env | cut -d= -f2-)
U=http://127.0.0.1:8000
7n8n ins Supabase-Netz (/opt/n8n/compose.yml)
services:
  n8n:
    # … wie bisher …
    networks: [default, supabase]

networks:
  supabase:
    external: true
    name: supabase_default
8Wissen einlesen (PowerShell)
$env:SUPABASE_URL = "http://localhost:8000"
$env:SUPABASE_SECRET_KEY = "sb_secret_…"          # aus /opt/supabase/.env
$env:EMBED_URL = "http://localhost:1234/v1"       # LM Studio
$env:EMBED_MODEL = "text-embedding-bge-m3"
python wissen_einlesen.py
9Übungsinstanz komplett zurücksetzen
cd /opt/supabase && sh reset.sh
Fragt nach und löscht dann Container und alle Daten der Übungsinstanz. Die Sicherungen in backups/ bleiben liegen.

4 Weiterlesen

  1. Self-Hosting with Docker: offizielle Anleitung, aktuelle Version
  2. Row Level Security: alle Möglichkeiten der Regeln
  3. pgvector: Indizes, Abstandsmaße, Grenzen
  4. n8n · Docling · LM Studio · Docker · Self-Hosting
Lernkontrolle

Quiz

7 Fragen quer durch die Stufen. Ab 6 richtigen Antworten schaltest du dein Zertifikat frei.

Frage 1 von 7
…
Abschluss

Dein Zertifikat

Glückwunsch! Trage deinen Namen ein und drucke das Zertifikat (oder speichere als PDF).

Das ist eine Vorschau. Das Zertifikat wird freigeschaltet, sobald Du im Quiz mindestens 6 von 7 Fragen richtig beantwortet hast. Bis dahin trägt es das Wasserzeichen „Muster“ und lässt sich nicht drucken.
FL
Florian Ludwig
AI Consultant · Kutzschbach INNOVATIONS
Zertifikat
Supabase — Datenbank, Anmeldung und KI-Gedächtnis auf dem eigenen Server
Hiermit wird bestätigt, dass
Dein Name hier
den Workshop „Supabase — Datenbank, Anmeldung und KI-Gedächtnis auf dem eigenen Server" erfolgreich abgeschlossen hat.
—Datum
★GEPRÜFT
Florian LudwigAI Consultant · Kutzschbach INNOVATIONS