Dritte Ansicht neben Raster und Gruppierung: eine Spalte je Status, Karten per Ziehen verschiebbar. Der Wechsel wird sofort angezeigt und erst danach gespeichert; scheitert das Speichern, springt die Karte zurueck. Details, die den Unterschied machen: - Gezogen wird am Griff links, nicht an der ganzen Karte - so bleibt der Titel ein normaler Link. - Das Ziehen startet erst nach sechs Pixeln Bewegung, sonst liesse sich die Karte nicht mehr anklicken. Auf Touchgeraeten nach kurzem Halten. - Die mitgezogene Karte haengt in einem DragOverlay am Cursor, das Original bleibt blass stehen - sonst wuerde die Spalte unter dem Finger springen. - Bestaetigt der Server die Verschiebung, wird die oertliche Ausnahme wieder entfernt, damit sie nicht veraltet. Die Reihenfolge innerhalb einer Spalte folgt der gewaehlten Sortierung. Frei sortieren hiesse, eine Position je Projekt zu speichern - bewusst nicht gebaut. Nachgewiesen im Browser: Karte aus "Idee" nach "Fertig" gezogen, Zielspalte hebt sich hervor und zeigt "Hier ablegen", danach steht das Projekt serverseitig auf DONE. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✦ Nebula
Das Kontrollzentrum für deine Vibe-Coding Projekte.
Ideen festhalten · Status pflegen · Notizen sammeln · Git-Repos live im Blick
Next.js 15 · TypeScript · Tailwind CSS 4 · Prisma · PostgreSQL · Docker
Was ist Nebula?
Vibe Coding produziert schnell viele halbfertige Projekte: ein Repo hier, eine Idee dort, irgendwo eine Notiz in einer Textdatei. Nebula bündelt das an einem Ort.
Jedes Projekt bekommt einen Status (von der rohen Idee bis „Fertig"), beliebig viele Notizen und optional ein verknüpftes Repository. Nebula holt sich von dort per API Branches, Tags, Commits, Releases und offene Issues/PRs — egal ob GitHub, GitLab oder eine selbst gehostete Gitea-Instanz.
Die Anmeldung läuft über Benutzername und Passwort oder Passkey (WebAuthn). Nebula kennt zwei Betriebsarten: Einzelbetrieb für ein Konto oder Mehrbenutzerbetrieb, in dem jede Person ihren eigenen, streng getrennten Bereich hat. Umschalten geht jederzeit in den Admin-Einstellungen.
Alle Daten liegen in deiner lokalen PostgreSQL. Es gibt keinen Cloud-Dienst dahinter, Access Tokens werden verschlüsselt abgelegt.
Eine vollständige Auflistung aller Funktionen steht in FEATURES.md, geplante Erweiterungen in ROADMAP.md.
Schnellstart
Zwei Betriebsarten, dieselbe Codebasis. Beide brauchen zuerst eine .env:
node scripts/setup.mjs # legt .env an und erzeugt ein zufälliges APP_SECRET
Ohne Node zur Hand? Einfach
.env.examplenach.envkopieren undAPP_SECRETvon Hand auf einen langen Zufallswert setzen.
Variante A — Standalone (empfohlen zum Entwickeln)
Die App läuft nativ auf dem Rechner, nur PostgreSQL kommt aus dem Container:
docker compose up -d db # Postgres 17 auf localhost:5432
npm install
npm run db:migrate # Schema anlegen
npm run db:seed # optional: drei Beispielprojekte
npm run dev # → http://localhost:3000
Für den Produktivbetrieb ohne Docker:
npm run build
npm start # → http://localhost:3000
Variante B — Alles in Docker
docker compose --profile full up -d --build # → http://localhost:3000
Der App-Container spielt beim Start automatisch die Migrationen ein und wartet dabei auf die Datenbank. Logs mitlesen:
docker compose --profile full logs -f app
Stoppen:
docker compose --profile full down # Container weg, Daten bleiben
docker compose --profile full down -v # inkl. Datenbank-Volume
Variante C — Eigene, bereits laufende PostgreSQL
Docker wird gar nicht gebraucht. In der .env nur den Connection-String setzen:
DATABASE_URL="postgresql://benutzer:passwort@dbhost:5432/nebula?schema=public"
Dann npm install && npm run db:migrate && npm run dev.
Voraussetzungen
| Variante | Benötigt |
|---|---|
| Standalone | Node.js ≥ 20.11, npm, PostgreSQL ≥ 14 (lokal oder via Docker) |
| Docker | Docker Engine ≥ 24 mit Compose v2 |
Konfiguration
Alle Variablen sind in .env.example dokumentiert.
| Variable | Bedeutung | Default |
|---|---|---|
APP_NAME |
Anzeigename: Seitentitel, Kopfzeile, Passkey-Dialog | Nebula |
APP_TAGLINE |
Kleine Zeile unter dem Namen | Vibe Coding |
APP_URL |
Öffentliche Adresse — bestimmt die WebAuthn-RP-ID | http://localhost:3000 |
DATABASE_URL |
PostgreSQL-Verbindung (Prisma) | postgresql://nebula:nebula@localhost:5432/nebula |
APP_SECRET |
Schlüssel für die Token-Verschlüsselung (AES-256-GCM) | — unbedingt setzen |
SESSION_TTL_DAYS |
Lebensdauer einer Anmeldung in Tagen | 30 |
PORT |
Port der Anwendung | 3000 |
APP_PORT |
Host-Port-Mapping des App-Containers | 3000 |
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB |
Zugangsdaten des DB-Containers | nebula |
POSTGRES_PORT |
Host-Port der Datenbank | 5432 |
ADMINER_PORT |
Host-Port von Adminer (--profile tools) |
8080 |
GIT_API_TIMEOUT_MS |
Timeout für Git-API-Aufrufe | 15000 |
GIT_ALLOW_LOOPBACK |
Git-Instanz auf demselben Rechner erlauben | false |
GIT_BLOCK_PRIVATE |
Zugriff ins private LAN sperren | false |
GIT_ALLOWED_HOSTS |
Strengster Modus: nur diese Hosts erlauben | leer |
APP_NAME, APP_TAGLINE und APP_URL werden zur Laufzeit gelesen. Dasselbe
Docker-Image lässt sich also ohne Neubau umbenennen oder unter einer anderen Adresse
betreiben — Container neu starten genügt.
APP_SECRETnachträglich ändern macht bereits gespeicherte Git-Tokens unlesbar. Sie müssen dann in den Verbindungen neu hinterlegt werden — Konten, Passwörter und Passkeys bleiben unberührt.
APP_URLnachträglich ändern (anderer Hostname) macht bestehende Passkeys ungültig, weil sie an die RP-ID gebunden sind. Passwörter funktionieren weiter.
Erste Anmeldung
Beim ersten Aufruf landest du auf /setup. Dort legst du das Administratorkonto an und
wählst die Betriebsart:
- Einzelbetrieb — genau ein Konto, keine Registrierung, keine Benutzerliste.
- Mehrbenutzerbetrieb — beliebig viele Konten. Projekte, Notizen und Git-Verbindungen sind pro Konto strikt getrennt; auch ein Administrator sieht fremde Projekte nicht.
Nach der Einrichtung ist /setup gesperrt. Bereits vorhandene Projekte ohne Besitzer
(etwa aus den Seed-Daten) werden dem neuen Konto zugeordnet.
Passkey einrichten: unter Mein Konto → Passkeys → Passkey hinzufügen. Danach genügt auf der Anmeldeseite ein Klick auf „Mit Passkey anmelden" — ohne Benutzernamen, weil Nebula auffindbare Credentials nutzt.
Passkeys brauchen einen sicheren Kontext. Sie funktionieren über
https://und überhttp://localhost, nicht über eine LAN-IP ohne TLS.APP_URLmuss dabei exakt der Adresse entsprechen, unter der du die App aufrufst.
Git-Verbindung einrichten
-
Im Git-System einen Personal Access Token mit Leserechten erzeugen:
System Scopes GitHub Fine-grained Token: Contents: Read, Metadata: Read, Issues: Read, Pull requests: Read GitLab read_api(alternativread_repository)Gitea / Forgejo repository:read,issue:read,user:read -
In Nebula unter Git-Verbindungen → Verbindung hinzufügen eintragen: System wählen, Basis-URL (z. B.
https://git.meine-firma.de, ohne/api) und Token. -
Mit Testen prüfen — Nebula meldet den erkannten Benutzer zurück.
-
Beim Anlegen oder Bearbeiten eines Projekts die Verbindung auswählen und über Repos laden das Repository aussuchen (oder die URL direkt eintippen).
-
Auf der Projektseite Jetzt synchronisieren — Branches, Tags, Commits und Releases werden geholt und als Snapshot gespeichert.
Der Snapshot bleibt in der Datenbank, das Dashboard funktioniert also auch offline. Ohne Token gehen nur öffentliche Repositories, mit strengem Rate-Limit.
Automatisch statt auf Knopfdruck
Unter Admin-Einstellungen → Automatischer Abgleich lässt sich der Sync in den Hintergrund verlegen: Abstand von 15 Minuten bis täglich, Anzahl Projekte je Durchlauf, und wahlweise ohne fertige und archivierte Projekte. Standardmäßig ist er aus, weil jeder Durchlauf API-Kontingent verbraucht.
Der Zeitgeber läuft im Serverprozess — kein zusätzlicher Container, kein externer Cron. Nach Fehlschlägen wird ein Repository mit wachsendem Abstand zurückgehalten, damit ein abgelaufener Token nicht dauerhaft Kontingent verbrennt. Einzelne Projekte lassen sich im Projektdialog ausnehmen; manuell synchronisieren geht immer.
Wer lieber von außen taktet, lässt den Automatismus aus und ruft
POST /api/admin/sync/run per eigenem Cron auf.
Export, Import und Sicherung
Projekte mitnehmen
| Weg | Wo | Inhalt |
|---|---|---|
| Alle Projekte als JSON | Dashboard → Datenbank-Symbol | Stammdaten, Notizen, Repo-Verknüpfung |
| Ein Projekt als JSON | Projektseite → Download-Symbol | dasselbe, für ein Projekt |
| Ein Projekt als Markdown | Projektseite → Download-Symbol | lesbar, inklusive letztem Git-Stand |
| Import | Dashboard → Datenbank-Symbol | JSON einlesen, mit Vorschau |
Access Tokens sind nie enthalten. Die Git-Verbindung wird nur beschrieben (Name, System, Basis-URL); nach dem Import muss das Token neu hinterlegt werden. Verweist eine importierte Datei auf eine hier unbekannte Verbindung, wird das Projekt ohne Verknüpfung angelegt und in der Vorschau angezeigt.
Beim Import wird über den Slug abgeglichen. Für Treffer gibt es drei Strategien:
- Überspringen — Vorhandene bleiben unverändert
- Zusätzlich anlegen — Kopie mit freiem Slug (
nebula→nebula-2) - Ersetzen — Stammdaten überschreiben, Notizen austauschen
Vor dem Schreiben zeigt eine Vorschau, was passieren würde. Erst dann wird geschrieben.
Sicherung
Zwei Wege, mit unterschiedlichem Zweck:
JSON-Sicherung (Admin-Einstellungen → Sicherung) enthält Einstellungen, Konten,
Verbindungen, Projekte und Notizen. Gut zum Umziehen. Passwörter liegen als scrypt-Hash
vor, Git-Tokens als Chiffrat — ohne dasselbe APP_SECRET sind sie wertlos. Trotzdem
vertraulich behandeln.
Datenbank-Sicherung über pg_dump — das, was man zum Wiederherstellen einer kaputten
Instanz braucht:
.\scripts\backup.ps1 # → .\backups\nebula-<datum>.sql
.\scripts\restore.ps1 -File .\backups\nebula-....sql
./scripts/backup.sh # → ./backups/nebula-<datum>.sql
./scripts/restore.sh backups/nebula-....sql
Die Skripte lesen Benutzer und Datenbankname aus der .env und führen pg_dump im
Container aus — auf dem Rechner selbst muss nichts installiert sein.
Eine Sicherung ist erst dann eine Sicherung, wenn du sie einmal zurückgespielt hast. Das geht ohne Risiko in eine Wegwerf-Datenbank:
docker exec nebula-db psql -U nebula -d postgres -c "CREATE DATABASE nebula_test" .\scripts\restore.ps1 -File .\backups\nebula-....sql -Database nebula_test -Force
Projektstruktur
nebula/
├── docker/entrypoint.sh Migrationen + Serverstart im Container
├── docker-compose.yml db (immer) · app (--profile full) · adminer (--profile tools)
├── Dockerfile Multi-Stage-Build auf Basis des Next-Standalone-Outputs
├── prisma/
│ ├── schema.prisma Datenmodell
│ ├── migrations/ versionierte SQL-Migrationen
│ └── seed.ts Beispieldaten
├── scripts/setup.mjs .env erzeugen inkl. APP_SECRET
└── src/
├── app/
│ ├── page.tsx Dashboard (Board, Filter, Kennzahlen)
│ ├── projects/[id]/ Projektdetail: Beschreibung, Notizen, Git
│ ├── connections/ Verwaltung der Git-Verbindungen
│ └── api/ REST-Endpunkte (Projekte, Notizen, Verbindungen, Sync)
├── components/ UI-Komponenten (Board, Karten, Panels, Dialoge)
├── lib/
│ ├── crypto.ts AES-256-GCM für Access Tokens
│ ├── db.ts Prisma-Client
│ ├── git/ Provider-Adapter: github.ts · gitlab.ts · gitea.ts
│ ├── sync.ts Repo-Snapshot holen und speichern
│ └── validation.ts Zod-Schemas für alle Eingaben
└── types/ gemeinsame Typen für Server und Client
npm-Skripte
| Skript | Zweck |
|---|---|
npm run dev |
Entwicklungsserver mit Hot Reload |
npm run build / npm start |
Produktionsbuild und -start (ohne Docker) |
npm run setup |
.env anlegen inkl. generiertem APP_SECRET |
npm run db:migrate |
Migrationen einspielen (prisma migrate deploy) |
npm run db:migrate:dev |
Neue Migration aus Schemaänderungen erzeugen |
npm run db:seed |
Beispielprojekte anlegen |
npm run db:studio |
Prisma Studio öffnen |
npm run db:reset |
Datenbank zurücksetzen (löscht alles) |
npm run typecheck |
TypeScript prüfen |
npm run docker:db |
Nur die Datenbank starten |
npm run docker:up / docker:down |
Kompletten Stack starten / stoppen |
npm run docker:tools |
Adminer auf http://localhost:8080 |
API
Alle Endpunkte liefern und erwarten JSON.
Bis auf /api/health und /api/auth/* erfordern alle Endpunkte eine Anmeldung und liefern
ausschließlich Daten des angemeldeten Kontos.
| Methode | Pfad | Zweck |
|---|---|---|
GET |
/api/health |
Status inkl. Datenbankverbindung (Docker-Healthcheck) |
POST |
/api/auth/setup |
Administratorkonto anlegen (nur beim ersten Mal) |
POST |
/api/auth/login /api/auth/logout /api/auth/register |
Anmeldung |
GET |
/api/auth/session |
Aktuelle Sitzung und Instanzzustand |
POST |
/api/auth/passkey/{login,register}/{options,verify} |
WebAuthn |
PATCH |
/api/account/profile |
Eigenes Profil |
POST |
/api/account/password |
Eigenes Passwort setzen |
GET DELETE |
/api/account/sessions |
Eigene Sitzungen / überall abmelden |
PATCH DELETE |
/api/account/passkeys/:id |
Passkey umbenennen / entfernen |
GET PATCH |
/api/admin/settings |
Betriebsart, Registrierung, Abgleich (Admin) |
POST |
/api/admin/sync/run |
Abgleich sofort anstoßen (Admin) |
GET POST |
/api/admin/users |
Konten auflisten / anlegen (Admin) |
PATCH DELETE |
/api/admin/users/:id |
Konto ändern / löschen (Admin) |
GET POST |
/api/projects |
Projekte auflisten / anlegen |
GET |
/api/projects/export |
Alle eigenen Projekte als JSON |
GET |
/api/projects/:id/export |
Ein Projekt (?format=md für Markdown) |
POST |
/api/projects/import |
Import, mit dryRun für die Vorschau |
GET |
/api/admin/backup |
Vollständige Sicherung als JSON (Admin) |
GET PATCH DELETE |
/api/projects/:id |
Projekt lesen / ändern / löschen |
POST |
/api/projects/:id/sync |
Repo-Daten vom Git-System holen |
GET POST |
/api/projects/:id/notes |
Notizen des Projekts |
PATCH DELETE |
/api/notes/:id |
Notiz ändern / löschen |
GET POST |
/api/connections |
Git-Verbindungen |
PATCH DELETE |
/api/connections/:id |
Verbindung ändern / löschen |
POST |
/api/connections/:id/check |
Verbindung und Token testen |
GET |
/api/connections/:id/repos |
Zugängliche Repositories auflisten |
Beispiel — Idee per Kommandozeile anlegen:
curl -X POST http://localhost:3000/api/projects \
-H "Content-Type: application/json" \
-d '{"name":"Neue Idee","status":"IDEA","tags":["experiment"]}'
Sicherheit
| Bereich | Umsetzung |
|---|---|
| Anmeldung | Passwort-Hashing mit scrypt (N=32768, r=8, p=1), serverseitige Sitzungen — in der Datenbank liegt nur der SHA-256-Hash des Cookie-Tokens |
| Passkeys | WebAuthn über SimpleWebAuthn, Challenges einmalig verwendbar und in der DB abgelegt, Signaturzähler wird fortgeschrieben |
| Cookies | HttpOnly, SameSite=Lax, Secure automatisch bei https |
| CSRF | Jede schreibende Anfrage muss vom eigenen Origin kommen (Sec-Fetch-Site / Origin); zusätzlich wird Content-Type: application/json erzwungen, was den Formular-Trick ohne Preflight ausschließt |
| CSP | Strikte Policy mit Per-Request-Nonce und strict-dynamic, dazu frame-ancestors 'none', object-src 'none', base-uri 'none' |
| Weitere Header | X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy |
| SSRF | Die Basis-URL einer Git-Verbindung ist Benutzereingabe. Vor jedem ausgehenden Aufruf werden Hostname und alle aufgelösten IPs geprüft (auch nach Redirects); Loopback, Link-Local samt Cloud-Metadaten, Multicast und CGNAT sind gesperrt |
| Datentrennung | Jede Abfrage filtert auf den Besitzer. Fremde IDs liefern 404, nicht 403 — die Antwort verrät also nicht einmal, dass es den Datensatz gibt |
| Rate-Limiting | Anmeldung pro IP und Konto, ausgehende Git-Aufrufe, WebAuthn-Optionen; Konto sperrt sich nach 8 Fehlversuchen für 15 Minuten |
| Eingaben | Zod-Schemas an jeder API-Grenze, Body auf 256 KB begrenzt |
| Git-Tokens | AES-256-GCM, Schlüssel per scrypt aus APP_SECRET. Werden nie an den Browser ausgeliefert — die API zeigt nur ghp_…9f3a |
| Sitzungsende | Deaktivieren, Löschen oder ein neues Passwort beenden sofort alle Sitzungen des Kontos |
Grenzen, die du kennen solltest:
- Ist
APP_SECRETnicht gesetzt, warnt die App beim Start und nutzt einen unsicheren Entwicklungsschlüssel. - Ohne
Origin- undSec-Fetch-Site-Header wird eine schreibende Anfrage durchgelassen. Das betrifft nur Nicht-Browser-Clients (curl, Skripte) und ist bewusst so, damit die API skriptbar bleibt — ein CSRF-Vektor ist es nicht, weil jeder Browser mindestens einen der beiden Header sendet. - Das Rate-Limiting liegt im Arbeitsspeicher und wird beim Neustart zurückgesetzt.
- Es gibt kein HSTS und keine TLS-Terminierung. Für den Betrieb im Internet gehört ein Reverse Proxy mit gültigem Zertifikat davor — der ist ohnehin nötig, damit Passkeys funktionieren.
- Passwort-Zurücksetzen per E-Mail gibt es nicht; ein Administrator setzt Passwörter neu.
Fehlersuche
| Symptom | Ursache / Lösung |
|---|---|
Can't reach database server |
Läuft docker compose up -d db? Stimmt DATABASE_URL? |
The table ... does not exist |
Migrationen fehlen: npm run db:migrate |
Sync meldet 401 |
Token abgelaufen oder falsche Scopes — unter Git-Verbindungen neu setzen |
Sync meldet 404 |
Owner/Repo prüfen; bei GitLab gehört die volle Gruppenkette in „Owner" |
Sync meldet 403 Rate-Limit |
Ohne Token ist das Limit sehr niedrig — Token hinterlegen |
| Tokens plötzlich ungültig | APP_SECRET wurde geändert — Tokens neu eintragen |
| Port 3000 belegt | APP_PORT in der .env ändern (Docker) bzw. npm run dev -- -p 3001 |
package.json#prisma is deprecated |
Harmlose Warnung von Prisma 6.19. Die Umstellung auf prisma.config.ts steht aus, weil dann die .env nicht mehr automatisch geladen wird — vor Prisma 7 zu erledigen |
| Passkey-Knopf fehlt / „kein sicherer Kontext" | APP_URL zeigt weder auf https:// noch auf http://localhost |
| Passkey wird nicht akzeptiert | APP_URL weicht von der aufgerufenen Adresse ab, oder der Hostname wurde geändert (RP-ID) |
Nach docker compose down -v ist alles weg |
Das Volume enthielt Konten und Projekte — -v löscht es |
| Kein Zugriff auf die eigene Gitea im LAN | Läuft sie auf demselben Rechner? Dann GIT_ALLOW_LOOPBACK=true setzen |
Getestet mit
Stand 25.07.2026 gegen echte Systeme verifiziert — beide Betriebsarten, alle drei Git-Adapter (GitHub, GitLab und Gitea jeweils gegen die öffentliche Instanz).
| Version | |
|---|---|
| Node.js | 26.5.0 |
| npm | 11.17.0 |
| Next.js | 15.5.21 |
| Prisma | 6.19.3 |
| PostgreSQL | 17 (Alpine) |
| Docker / Compose | 29.6.2 / v5.3.1 |
Lizenz
MIT — siehe LICENSE.