Machs Dir Selbst Solar
DIY-Solar-Lernplattform — Videos, Tutorials und Anleitungen plus drei Planungs-Apps für Endkunden, die ihre PV-Anlage selbst planen und installieren
DIY-Solar-Plattform für Endkunden, die ihre PV-Anlage in Eigenregie planen und größtenteils selbst installieren möchten — im Geist einer Lernplattform wie Udemy, nur fokussiert auf Photovoltaik. Lerninhalte (Videos, Tutorials, Komponentenkunde, Schritt-für-Schritt-Anleitungen) und drei Planungs-Apps bilden zusammen den DIY-Werkzeugkasten: Builder als kundenseitiger Anfrage-Editor mit Karten-Dachflächen-Editor, Configurator für das Detail- Angebot samt Materialauswahl und Annahme, und ein internes Planner- Dashboard mit weitgehender Automatisierung für die wenigen verbleibenden Support-Touchpoints. Die Apps als Nx-Monorepo mit gemeinsamem Supabase- Backend — und einer Preview-Pipeline, in der jedes Pull Request vollautomatisch eine eigene Datenbank, drei eigene Container und drei Subdomains bekommt.
01 Problem
Solario wollte eine zweite Produktlinie für eine ganz andere Zielgruppe aufbauen: DIY-affine Endkunden, die sich gerne tief selbst einarbeiten, Videos schauen, Komponenten vergleichen und am Ende ihre Anlage selbst planen und (mit lokalem Handwerker für die elektrische Inbetriebnahme) selbst montieren wollen. Damit das wirtschaftlich funktioniert, musste der Personalaufwand pro Anfrage drastisch sinken — die Kunden sollten das meiste über Lerninhalte und Self-Service-Tools allein durchlaufen können. Drei Nutzergruppen mit drei sehr unterschiedlichen Anforderungen mussten an einer geteilten Datenbasis arbeiten: Endkunden, die ihre Anlage selbst auf der Karte modellieren und eine grobe Preis- und Materialschätzung wollen; dieselben Endkunden später bei der Detail-Konfiguration, in der sie Material wählen und das Angebot annehmen; und interne Planer, deren Arbeit so weit wie möglich automatisiert werden sollte.
02 Lösung
Nx-Monorepo mit drei eigenständig deploybaren Angular-21-Apps und einem Supabase-Backend, eingebettet in eine Content-Plattform mit Lernvideos und Schritt-für-Schritt-Anleitungen, die Kunden vor und während der Planung konsumieren. PR-basierte Preview-Infrastruktur: jedes Pull Request bekommt automatisch eine isolierte Supabase-Branch + drei Coolify-Deploys mit der Preview-Datenbank verdrahtet. Reviewer sehen Änderungen pro App unter eigener Subdomain, ohne dass parallele PRs sich gegenseitig kontaminieren.
03 Ergebnis
Drei Apps mit gemeinsamer Codebasis und vollständiger Datenisolation pro PR. Die DIY-Linie funktioniert mit deutlich geringerem Personalaufwand pro Anfrage als der klassische Solario-Vertrieb — vor allem, weil Endkunden die Anfrage- und Konfigurationsschritte selbst durchlaufen, die Lerninhalte im Vorfeld die meisten Fragen beantworten, und nur noch qualifizierte, in sich konsistente Anfragen beim Solario-Planer landen. Reviewer können Kunden- User-Flows in der jeweils aktuellen PR-Version testen, ohne irgendetwas lokal aufsetzen zu müssen.
Drei Apps, eine Datenbasis — eingebettet in eine Lernplattform
Die Plattform besteht aus drei Angular-21-Apps, die jeweils einen klar abgegrenzten Nutzerkreis bedienen und sich substantielle Teile der Domänenlogik teilen. Sie sind die Werkzeugschicht innerhalb einer größeren Content-Plattform — DIY-Kunden konsumieren in der Regel erst die Lerninhalte (Komponentenkunde, Auslegungsregeln, gesetzliche Rahmenbedingungen, Schritt-für-Schritt-Videos zur Eigenmontage), bevor sie überhaupt anfangen, ihre Anlage im Builder zu modellieren. Das verschiebt die Kundenerwartung: wer hier ankommt, will nicht beraten werden, sondern Werkzeuge.
Builder — die kundenseitige Anfrage-App. Ein interessierter Endkunde gibt seine Adresse ein, modelliert seine Dachflächen direkt im Karten-Editor (Polygon-Zeichen über Satellitenbild), legt Modulflächen pro Segment fest und wählt grob, was er anfragen möchte — Module, Wechselrichter, Speicher. Heraus kommt eine grobe Preisspanne und eine Stückliste der Komponenten und Werkzeuge, die er für ein DIY-Vorgehen brauchen würde. Wer aus den Videos und Anleitungen schon weiß, was eine MC4-Verbindung ist und wie ein String-Layout funktioniert, kommt hier ohne weitere Hilfe durch. Damit hat Solario eine in sich konsistente, realistische Anfrage — und der Kunde eine ehrliche Erwartung.
Configurator — die Detail-Angebots-App, in die der Kunde nach Bearbeitung durch Solario einsteigt. Hier liegt das ausgearbeitete Angebot: konkrete Modulreihen, konkreter Wechselrichter, optionale Komponenten mit Aufpreis, detaillierte Statistiken zur Wirtschaftlichkeit. Der Kunde kann Material wechseln (z. B. Premium-Modul statt Basis), sieht die Auswirkungen sofort an Preis, Ertrag und Payback, und kann das Angebot direkt annehmen — ohne Termin, ohne Telefonat.
Planner — das interne Tool für die Solario-Planer. Es ist auf Automatisierung ausgelegt: aus der Builder-Anfrage erzeugt der Planner einen Vorschlag für das Detail-Angebot, fügt fehlende technische Details aus den Stammdaten hinzu (welche Verkabelung passt, welcher Wechselrichter zu welchem String-Layout, welche Zuschüsse gelten regional), und der Planer prüft nur noch und stößt es ab. Ein einzelner Planer bedient damit ein Anfragevolumen, das im klassischen Vertrieb mehrere Berater binden würde. Stammdatenpflege (Hersteller, Module, Wechselrichter, Elektrobetriebe, Zuschüsse, Strompreis-Annahmen) findet auch hier statt.
Geteilt werden alle drei über libs/economics (Cashflow-Berechnung, Annuität, Strompreis-Eskalation, Payback) und libs/utils. nx affected baut und testet nur das, was eine Änderung wirklich berührt — auf vier Targets pro App (lint, test, build, e2e) ist das der Unterschied zwischen einer angenehmen und einer unzumutbaren CI.
export class EconomicsUtils {
static DEFAULT_YEARS_FOR_CALCULATION = 25;
static ELECTRICITY_PRICE_YEARLY_INCREASE_PERCENTAGE = 0.04;
static calculate_payback_period(profit_list_euro: number[], total_cost_euro: number): number {
let years = 0;
let cumulative = 0;
while (total_cost_euro > cumulative && years < profit_list_euro.length) {
cumulative += profit_list_euro[years];
years += 1;
}
const remaining = cumulative - total_cost_euro;
if (remaining > 0 && years > 0) {
return years - (remaining / profit_list_euro[years - 1]);
}
return years;
}
} Was die Preview-Infrastruktur wirklich tut
Das herausragende Stück Infrastruktur in diesem Projekt ist nicht eine einzelne App, sondern die preview.yml. Sie macht aus jedem Pull Request eine vollständige, isolierte Test-Umgebung: eine eigene Postgres-Datenbank, drei eigene Container, drei eigene URLs.
Im Detail durchläuft der Workflow für jedes PR diese Schritte:
- Supabase-Branch sicherstellen. Der Workflow ruft die Supabase Management API, sucht die zur PR-Branch passende Preview-Branch oder erstellt sie. Er pollt bis zu 15 Minuten auf
ACTIVE_HEALTHYund liest danach Branch-spezifische Credentials aus (DB-Pass, Anon-Key, JWT-Secret). - Edge-Function-Secrets synchronisieren. Preview-Branches erben Funktionen aus dem Parent-Projekt, aber nicht deren Secrets. Ohne diesen Schritt würde
Deno.env.get(...)in jeder Functionundefinedzurückgeben. - Dashboard-only-Schemas bootstrappen. Das
supabase_functions-Schema und derhttp_request-Trigger existieren in Production, weil sie dort einmal per Dashboard-Klick angelegt wurden. Auf einem Branch fehlen sie — der Workflow installiert einen No-op-Stub mit identischer Signatur, damit Migrations-Trigger nicht fehlschlagen. - Vault-JWT-Secret setzen. Custom-JWT-Signing-Funktionen lesen
app.jwt_secretaus dem Postgres Vault. Auf einer frischen Branch fehlt der Eintrag — der Workflow legt ihn mit dem Branch-eigenen JWT-Secret an, sodass PostgREST/GoTrue die selbst-ausgestellten Tokens verifizieren können. - Migrationen und Edge-Functions ausrollen.
supabase db push --include-allundsupabase functions deploysynchronisieren die Branch mit dem PR-HEAD. Beiopenedoderreopenedläuft zusätzlichseed.sql, beisynchronizenicht (der Seed ist nicht idempotent). - Affected-Apps bauen und deployen.
nx show projects --affected --type=appliefert die Liste der zu deployenden Apps. Für jede gebaut wird ein Docker-Image mit PR-spezifischen Build-Args, ins GHCR gepusht und Coolifys/api/v1/deploymit dem neuen Tag getriggert. - PR-Kommentar aktualisieren. Ein Sticky-Comment listet die drei Preview-URLs. Bei
openedgeht parallel eine Nextcloud-Talk-Nachricht raus — Team weiß, dass der Preview steht. - Teardown bei
closed. Supabase-Branch löschen, drei Coolify-Previews perDELETE /api/v1/applications/<uuid>/previews/<pr>abräumen.
Tiefe Einblicke
Build-Args statt Runtime-Substitution
Werte, die sich pro PR ändern (Supabase-URL, Anon-Key, App-URLs), gehen als Docker-Build-Args ins Image. Jedes PR bekommt damit ein eigenes, vollständig immutables Artefakt — kein Drift zwischen „dem Container, den ich teste” und „der Konfiguration, die gerade aktiv ist”. Werte, die alle Previews teilen können, bleiben Runtime-Env-Vars.
- name: Build, push to GHCR, trigger Coolify deploy
env:
SUPABASE_URL: ${{ steps.supabase.outputs.url }}
SUPABASE_ANON_KEY: ${{ steps.supabase.outputs.anon }}
PLANNER_URL: ${{ steps.urls.outputs.planner }}
run: |
image="ghcr.io/$repo/${name}:pr-${PR_NUMBER}"
docker buildx build \
--build-arg "SUPABASE_URL=${SUPABASE_URL}" \
--build-arg "SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY}" \
--build-arg "PLANNER_URL=${PLANNER_URL}" \
--tag "$image" --push .
curl -sS -X POST -H "Authorization: Bearer $COOLIFY_TOKEN" \
"$COOLIFY_URL/api/v1/deploy?uuid=$uuid&pr=$PR_NUMBER&docker_tag=pr-${PR_NUMBER}" Supabase-Branch warten — mit echtem Fehlermodell
Branch-Provisioning ist asynchron. Der Workflow pollt nicht blind, sondern unterscheidet ACTIVE_HEALTHY (fertig) von MIGRATIONS_FAILED (sofort abbrechen). 15 Minuten Deadline, weil ein voller Cold-Start inklusive Migrationen bis zu 10 Minuten dauern kann. Plus zwei defensive Vor-Checks: ohne Production-Branch in der Konfiguration würde Supabase den ersten POST automatisch zur Default-Branch befördern — wir bestehen darauf, dass eine Production-Branch existiert und nicht zufällig die der PR ist.
deadline=$(( $(date +%s) + 900 )) # 15m
while : ; do
branch=$(curl -sS -H "Authorization: Bearer $TOKEN" \
"https://api.supabase.com/v1/branches/$branch_id")
status=$(echo "$branch" | jq -r '.status // empty')
case "$status" in
ACTIVE_HEALTHY|FUNCTIONS_DEPLOYED|MIGRATIONS_PASSED)
break ;;
MIGRATIONS_FAILED|FUNCTIONS_FAILED)
echo "::error::Preview branch failure: $status"
exit 1 ;;
esac
[[ $(date +%s) -ge $deadline ]] && { echo "::error::timeout"; exit 1; }
sleep 10
done Bootstrap für Dashboard-only-Schemas
Supabase legt das supabase_functions-Schema und die http_request-Trigger-Funktion an, wenn man im Dashboard „Database Webhooks” anklickt. Branches erben das nicht. Frische Preview-Branches mit CREATE TRIGGER ... supabase_functions.http_request in ihren Migrationen fallen also sofort um. Lösung: ein No-op-Stub mit identischer Signatur, der NEW zurückgibt — Trigger sind glücklich, und auf Previews soll sowieso kein Webhook in Production feuern.
CREATE SCHEMA IF NOT EXISTS supabase_functions;
DO $bootstrap$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE n.nspname = 'supabase_functions' AND p.proname = 'http_request'
) THEN
EXECUTE $body$
CREATE FUNCTION supabase_functions.http_request() RETURNS trigger
LANGUAGE plpgsql AS $fn$ BEGIN RETURN NEW; END $fn$
$body$;
END IF;
END $bootstrap$; Affected-aware Builds bei Synchronize
Beim ersten Öffnen eines PR bauen wir alle drei Apps — der Reviewer soll ein vollständiges URL-Set bekommen. Bei jedem späteren Commit (synchronize-Event) fragt der Workflow nx show projects --affected --type=app und baut nur die Apps, deren Abhängigkeitsgraph wirklich von dem Commit berührt wurde. Drei Builds spart das im typischen Fall auf einen oder zwei zurück.
- name: Compute affected apps
id: affected
run: |
if [[ "${{ github.event.action }}" != "synchronize" ]]; then
# Bei opened/reopened: alle drei Apps bauen
{ echo "builder=true"; echo "planner=true"; echo "configurator=true"; } \
>> "$GITHUB_OUTPUT"
exit 0
fi
affected=$(pnpm exec nx show projects --affected --type=app)
for app in builder planner configurator; do
if echo "$affected" | grep -qx "$app"; then
echo "${app}=true" >> "$GITHUB_OUTPUT"
else
echo "${app}=false" >> "$GITHUB_OUTPUT"
fi
done Sauberer Teardown beim PR-Close
Coolifys eingebauter Cleanup-Pfad gilt nur für Apps, die direkt aus einer Git-Source gebaut werden — Docker-Image-Apps müssen explizit aufgeräumt werden. Der Teardown-Job ruft DELETE /api/v1/applications/<uuid>/previews/<pr> und akzeptiert 404 als „war schon weg”. Außerdem löscht er die Supabase-Branch über DELETE /v1/branches/<id> — mit einer doppelten Sicherheitsklausel (is_default != true), damit eine Fehlkonfiguration nie die Production-Branch erwischen kann.
# Suspenders: is_default != true garantiert, dass wir nie die Prod-Branch wählen.
branch=$(echo "$branches" | jq -c \
--arg g "$PR_BRANCH" \
'[.[] | select(.git_branch == $g and .is_default != true)] | .[0] // empty')
if [[ -z "$branch" ]]; then
echo "No Supabase preview branch for $PR_BRANCH — nothing to delete"
exit 0
fi
branch_id=$(echo "$branch" | jq -r '.id')
curl -sS --fail -X DELETE \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
"https://api.supabase.com/v1/branches/$branch_id" > /dev/null Was sich nach einem Jahr Betrieb gezeigt hat
Drei Beobachtungen, die wir am Tag 1 nicht so klar gesehen hätten:
Die Preview-Pipeline ist Onboarding-Werkzeug. Neue Entwickler in das Setup zu heben dauert ungefähr null Minuten — sie öffnen ein PR, kriegen drei URLs, klicken sich rein. Die wahre Stärke der Pipeline ist nicht Code-Review, sondern dass externe Beteiligte (Solario-Vertrieb, Designer) ohne lokales Setup an einer realen Version arbeiten können.
Edge-Function-Secrets sind das stille Stolperthema. Auf der Hälfte der Probleme, die wir bei Previews gesehen haben, war der Auslöser ein neues Secret in apps/supabase/functions/.env, das nicht in der Preview-Sync-Liste landete. Wir haben das mittlerweile als CODEOWNERS-Regel: wer .env im Functions-Ordner ändert, kriegt automatisch den Preview-Workflow zur Review.
Build-Args eliminieren eine ganze Klasse von „funktioniert lokal aber nicht in Preview”-Bugs. In der ersten Version haben wir Werte zur Build-Zeit aus Env-Vars in environment.preview.ts substituiert (ein apply-preview-env.sh-Skript, das sed über Platzhalter laufen lässt). Das funktioniert, ist aber fragil: jedes neue Env muss in zwei Listen synchron gehalten werden. Build-Args sind ein einziger Ort.
05 Entscheidungen
Schlüssel-Entscheidungen
Ein Nx-Monorepo, drei deploybare Apps
Statt drei separater Repositories mit kopiertem Code: ein Repo, drei Apps in `apps/{planner,configurator,builder}`, geteilte Domäne in `libs/`. `nx affected` sorgt dafür, dass Builds und Tests nur laufen, wo sie müssen — sonst würde jedes PR die volle Pipeline auslösen.
Supabase Branching für isolierte Preview-DBs
Jedes PR bekommt eine eigene Postgres-Datenbank, automatisch aus den Migrationen aufgebaut. Reviewer können Daten einfügen, ohne dass dadurch andere PRs beeinflusst werden — und ohne dass „Test-Daten" am Ende in die Produktion sickern. Beim Schließen des PR wird die Branch automatisch gelöscht.
Coolify-Deploys pro PR und App, gesteuert per API
Drei vorab angelegte Coolify-Anwendungen mit Preview-Flag. Der GitHub-Workflow baut pro App ein Docker-Image mit den PR-spezifischen Build-Args, pusht es ins GHCR und triggert den Coolify-Deploy mit dem gerade gebauten Image-Tag. Beim Schließen des PR ruft ein Teardown-Job explizit `DELETE /api/v1/applications/<uuid>/previews/<pr>` — Docker- Image-Apps werden vom Coolify-GitHub-App-Webhook nicht automatisch aufgeräumt.
Build-Args statt Runtime-Substitution
Pro-PR-spezifische Werte (Supabase-URL, Anon-Key, App-URLs) gehen als Docker-Build-Args ins Image — eine eigene Build pro PR, einmal pushen, Coolify zieht das Tag. Werte, die alle Previews teilen (Google-Maps-Key, Meta-Pixel) bleiben Runtime-Env-Vars im Container. Damit kann es keinen Race zwischen parallelen PRs auf eine gemeinsame Env-Var geben.
Drei Apps, ein Auth-Modell
Endkunden (im Builder und Configurator) und interne Planer haben unterschiedliche Rollen, aber das gleiche Auth-System (Supabase Auth). Die Apps unterscheiden sich nur in den Routen und Komponenten — die Berechtigungsprüfung sitzt in der Datenbank (RLS). Eine vergessene Permission im Frontend kann keinen Datenleak erzeugen.
Affected-aware Preview-Deploys
Bei einem `synchronize`-Event (neuer Commit im PR) baut der Workflow nur die Apps neu, deren transitive Abhängigkeiten sich geändert haben. Bei `opened` oder `reopened` werden alle drei Apps gebaut, damit der Reviewer mit einem vollständigen URL-Set startet. Spart in der Realität meistens zwei von drei Builds ein.
07 Was hängenbleibt
- 01 Eine Preview-Infrastruktur lohnt sich, sobald drei Personen am Code arbeiten und Reviewer reale Daten sehen wollen. Vorher ist es eine teure Spielerei.
- 02 Build-Args sind sauberer als Runtime-Substitution, wenn die Werte pro Deployment ohnehin nur einmal wechseln. Das per-PR-getaggte Image ist automatisch der „Source of Truth" — kein Drift zwischen Container und Env.
- 03 `nx affected` ist der Unterschied zwischen einer 2-minütigen und einer 20-minütigen CI bei vier Apps. Ohne wäre der Monorepo-Ansatz nicht tragfähig.
- 04 Supabase Branching macht das harte Problem (isolierte DB pro PR) trivial. Die echten Stolpersteine sind die Dinge, die Branches nicht automatisch erben: Edge-Function-Secrets, Vault-Einträge, Dashboard-only-Schemas.
- 05 Coolifys API ist nicht hübsch, aber für CI-Automatisierung ausreichend dokumentiert. Wer Docker-Image-Apps nutzt, muss Cleanup selbst aufrufen — der GitHub-App-Webhook reagiert nur auf Git-Source-Apps.