# ✦ 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](FEATURES.md)**, geplante Erweiterungen in **[ROADMAP.md](ROADMAP.md)**. --- ## Schnellstart Zwei Betriebsarten, dieselbe Codebasis. Beide brauchen zuerst eine `.env`: ```bash node scripts/setup.mjs # legt .env an und erzeugt ein zufälliges APP_SECRET ``` > Ohne Node zur Hand? Einfach `.env.example` nach `.env` kopieren und `APP_SECRET` von 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: ```bash 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: ```bash npm run build npm start # → http://localhost:3000 ``` ### Variante B — Alles in Docker ```bash 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: ```bash docker compose --profile full logs -f app ``` Stoppen: ```bash 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: ```env 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`](.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_SECRET` nachträ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_URL` nachträ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 über > `http://localhost`, **nicht** über eine LAN-IP ohne TLS. `APP_URL` muss dabei exakt der > Adresse entsprechen, unter der du die App aufrufst. ## Git-Verbindung einrichten 1. 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` (alternativ `read_repository`) | | Gitea / Forgejo | `repository:read`, `issue:read`, `user:read` | 2. 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. 3. Mit **Testen** prüfen — Nebula meldet den erkannten Benutzer zurück. 4. Beim Anlegen oder Bearbeiten eines Projekts die Verbindung auswählen und über **Repos laden** das Repository aussuchen (oder die URL direkt eintippen). 5. 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: ```powershell .\scripts\backup.ps1 # → .\backups\nebula-.sql .\scripts\restore.ps1 -File .\backups\nebula-....sql ``` ```bash ./scripts/backup.sh # → ./backups/nebula-.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: > > ```powershell > 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 test` | Unit-Tests (Vitest), unter einer Sekunde | | `npm run test:watch` | Tests im Beobachtungsmodus | | `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` | `/api/search?q=` | Volltextsuche über die eigenen Notizen | | `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: ```bash 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_SECRET` nicht gesetzt, warnt die App beim Start und nutzt einen unsicheren Entwicklungsschlüssel. - Ohne `Origin`- **und** `Sec-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](LICENSE).