# ✦ 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. --- ## 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` `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).