Supabase was only ever the host: the application has talked to PostgreSQL
directly through pg/Kysely for a while. So the move is mostly about
supplying what the platform used to supply.
Proved before building anything. All 65 migrations replay onto an empty
database, and the result matches production exactly — 183 columns, 25
policies, 68 indexes, 84 constraints, identical sets, no diff. The only
function missing from the rebuild turned out to matter, see below.
What the platform supplied, deploy/db-init now does:
- alpenwerk_app, explicitly NOBYPASSRLS. The whole access model is 21
RLS policies; a role that bypasses them would leave everything
working while showing too much, and nobody would notice.
- pgcrypto and pg_trgm. uuid-ossp was available on Supabase but is
used nowhere — no column default, no function calls uuid_generate_*.
- anon, authenticated and service_role as NOLOGIN placeholders. No
policy names them; they only carry grants the platform handed out,
and a data dump referencing them would fail to restore without them.
- A stub `auth` schema. The end state needs none of it — checked: no
foreign key, no policy, no column default refers to it. The June
2026 migrations do, and rewriting those would be falsifying history;
they describe what was true then.
The gap the comparison found: rls_auto_enable() and the ensure_rls event
trigger existed only in the running database, created by hand, in no
migration. That is the net which forces RLS on every newly created
table — the reason a forgotten policy yields an empty table instead of
an open one. A rebuild from migrations would silently not have had it:
everything works, and the next new table is unprotected. Now a migration
(20260819100000), verified by creating a table on the rebuild and
confirming RLS came on by itself.
Data moves separately, via scripts/umzug-von-supabase.sh: schema from
the migrations, then pg_dump --data-only --disable-triggers for the rows.
Without --disable-triggers every foreign key trips over load order. RLS
does not interfere — none of the 19 tables uses FORCE ROW LEVEL
SECURITY, so the owner writes through. The dump is deliberately left on
disk afterwards.
psql and node come from two `tools`-profile services rather than being
installed on the host, so the server needs nothing but Docker. The db
service publishes no port at all — reachable only inside the compose
network.
SUPABASE_DB_URL is renamed MIGRATE_DATABASE_URL, since after this it
describes something else entirely; the old name still works so existing
.env files keep running. Both were exercised, as was the error when
neither is set.
The deploy workflow is set to manual-only. Its preconditions were never
met — no secrets, and whether the job container can reach the host's
Docker daemon is untested — and failing on every push teaches people to
ignore red runs. It also needs updating for the new database service
before it could work at all.
Not verified: none of this has run in an actual container. There is no
Docker daemon on this machine. What is verified is the part that
decides whether it can work — the schema, on a real empty database.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
78 lines
3.0 KiB
Bash
78 lines
3.0 KiB
Bash
#!/usr/bin/env bash
|
|
# Holt den Datenbestand von Supabase und spielt ihn in den db-Container.
|
|
#
|
|
# ═══ Der Ablauf, und warum er so aussieht ═══
|
|
#
|
|
# Das **Schema** kommt nicht aus dem Abzug, sondern aus den Migrationen:
|
|
# scripts/migrate.mjs baut es im Container von Grund auf. Nachgewiesen ist,
|
|
# dass dabei Spalte für Spalte, Index für Index, Policy für Policy dasselbe
|
|
# herauskommt wie in der Produktion. Der Weg über die Migrationen hat zwei
|
|
# Vorteile gegenüber einem Schema-Abzug: die Buchführung stimmt danach von
|
|
# selbst, und was Supabase an eigenen Rechten und Eigentümern hineingeschrieben
|
|
# hat, kommt gar nicht erst mit.
|
|
#
|
|
# Übernommen werden nur die **Daten**, mit --disable-triggers: sonst stolpert
|
|
# jede Fremdschlüsselprüfung über die Ladereihenfolge. (RLS steht dem nicht im
|
|
# Weg — keine der 19 Tabellen hat FORCE ROW LEVEL SECURITY, der Eigentümer
|
|
# schreibt also durch.)
|
|
#
|
|
# Auf dem Host wird **nur Docker** gebraucht: psql und pg_dump kommen aus dem
|
|
# Dienst `psql`, Node aus dem Dienst `migrate` — beide im Profil `tools`, das
|
|
# bei einem gewöhnlichen `docker compose up` nicht mitläuft.
|
|
#
|
|
# Aufruf im Deploy-Verzeichnis:
|
|
#
|
|
# SUPABASE_DB_URL='postgresql://…@…supabase.com:5432/postgres' \
|
|
# ./scripts/umzug-von-supabase.sh
|
|
#
|
|
# Der Abzug bleibt als Datei liegen und wird nicht gelöscht: geht das
|
|
# Einspielen schief, ist der Bestand sonst nur noch bei Supabase.
|
|
|
|
set -euo pipefail
|
|
|
|
: "${SUPABASE_DB_URL:?SUPABASE_DB_URL fehlt — der direkte Zugang zur alten Datenbank (Supabase: Port 5432)}"
|
|
|
|
ABZUG="${ABZUG:-supabase-daten-$(date +%Y%m%d-%H%M%S).sql}"
|
|
|
|
echo "── 1/5 Datenbank starten und abwarten"
|
|
docker compose up -d db
|
|
docker compose ps db
|
|
|
|
echo
|
|
echo "── 2/5 Schema aus den Migrationen aufbauen"
|
|
docker compose run --rm migrate
|
|
|
|
echo
|
|
echo "── 3/5 Daten von Supabase abziehen → ${ABZUG}"
|
|
# Läuft im psql-Abbild, damit pg_dump nicht auf dem Host installiert sein muss.
|
|
# --entrypoint überschreibt den psql-Aufruf des Dienstes.
|
|
docker compose run --rm --no-deps --entrypoint pg_dump psql \
|
|
"${SUPABASE_DB_URL}" \
|
|
--data-only \
|
|
--disable-triggers \
|
|
--schema=public \
|
|
--no-owner \
|
|
--no-privileges \
|
|
> "${ABZUG}"
|
|
echo " $(wc -l < "${ABZUG}") Zeilen, $(du -h "${ABZUG}" | cut -f1)"
|
|
|
|
echo
|
|
echo "── 4/5 Einspielen"
|
|
docker compose run --rm -T psql < "${ABZUG}"
|
|
|
|
echo
|
|
echo "── 5/5 Gegenprobe — Zeilenzahlen hier und dort"
|
|
echo " neue Datenbank:"
|
|
docker compose run --rm -T psql -qAt -c "
|
|
select ' '||table_name||': '||(xpath('/row/c/text()',
|
|
query_to_xml('select count(*) as c from public.'||quote_ident(table_name), false, true, '')))[1]::text
|
|
from information_schema.tables
|
|
where table_schema='public' and table_type='BASE TABLE' order by table_name"
|
|
|
|
echo
|
|
echo "Fertig. Nächster Schritt: DATABASE_URL in .env auf den Container zeigen"
|
|
echo "lassen (siehe DEPLOYMENT.md), dann docker compose up -d --build"
|
|
echo
|
|
echo "Der Abzug bleibt unter ${ABZUG} liegen — erst löschen, wenn die Anwendung"
|
|
echo "gegen die neue Datenbank nachweislich läuft."
|