Files
nebula/README.md
T
florian.wilde e9f01ba8fa
CI / pruefen (pull_request) Canceled after 0s
docs(git): jeden ausgehenden Aufruf einzeln mit seinem Recht belegen
Die Faelle A und B beantworteten die Frage auf Ebene der Scopes, nicht
auf Ebene der Aufrufe. Wer nachpruefen wollte, ob ein Recht wirklich
noetig ist, musste die drei Clients selbst lesen.

Jetzt steht in der README je System eine vollstaendige Tabelle: Aufruf,
Zweck, benoetigtes Recht. Die Zuordnung bei GitHub folgt der offiziellen
Berechtigungsuebersicht - dass /tags und /stats/commit_activity unter
Metadata fallen und nicht unter Contents, ist nicht zu erraten. Fuer
GitLab ist mit Quellenangabe belegt, dass es unterhalb von 'api' keinen
Schreibzugriff auf Issues gibt.

Ausserdem war offen, woher die Kommentare an den Issues stammen: Nebula
schreibt keine. Der Schreibzugriff besteht aus genau zwei Aufrufen. Ein
Test klopft dafuer die Clients ab - kaeme ein dritter dazu, wuerde die
Empfehlung 'nimm ein lesendes Token' still falsch, ohne dass jemand
einen Fehler saehe.

Der Hinweis am Eingabefeld nannte Actions nicht, obwohl der CI-Zustand
bei GitHub genau daran haengt.

Refs #10
2026-08-18 12:57:02 +02:00

30 KiB

✦ 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.

Neben dem Projektbrett gibt es Aufgaben über alle Projekte hinweg (nach Fälligkeit geordnet), einen Wochenrückblick und eine fortlaufende Zeitleiste aus dem Aktivitätsverlauf. Aufgaben lassen sich als Git-Issue anlegen; wird das Issue geschlossen, hakt Nebula die Aufgabe ab — sofort per Webhook oder verzögert beim nächsten Abgleich.

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. Für den Dauerbetrieb auf einem Server — TLS, Firewall, Sicherungen und Updates — gibt es DEPLOYMENT.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.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:

npm run docker:db            # Postgres 17 auf 127.0.0.1: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 Absolute Lebensdauer einer Anmeldung in Tagen 30
SESSION_IDLE_HOURS Nach so vielen Stunden ohne Nutzung läuft die Sitzung ab (0 = aus) 8
PORT Port der Anwendung 3000
APP_PORT Host-Port-Mapping des App-Containers 127.0.0.1:3000
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB Zugangsdaten des DB-Containers nebula
POSTGRES_PORT Host-Port der Datenbank — wirkt nur mit docker-compose.dev.yml, sonst hat die DB keinen 127.0.0.1:5432
ADMINER_PORT Host-Port von Adminer (--profile tools) 127.0.0.1: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
NOTIFY_TIMEOUT_MS Timeout beim Zustellen von Benachrichtigungen 10000
SMTP_HOST … SMTP_FROM SMTP-Zugang; ohne SMTP_HOST steht der Weg „E-Mail" nicht zur Verfügung 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 erzeugen — mit den Rechten aus dem Abschnitt Welche Rechte der Token braucht.
  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.

Welche Rechte der Token braucht

Nebula liest fast alles nur. Geschrieben wird an genau zwei Stellen: wenn du eine Aufgabe als Issue anlegst, und wenn eine Aufgabe auf DONE wandert und Nebula das verknüpfte Issue schließt. Alles andere sind GET-Aufrufe. Deshalb lohnt es, zwei Fälle zu unterscheiden:

Fall A — nur Abgleich. Branches, Tags, Commits, Releases, CI-Zustand, README, Heatmap, offene Issues und PRs anzeigen. Rein lesend.

Fall B — zusätzlich die Issue-Anbindung. Eine Aufgabe als Issue anlegen; wird das Issue geschlossen, hakt Nebula die Aufgabe ab und schließt sie umgekehrt mit.

System Fall A (nur lesen) Fall B zusätzlich
GitHub Fine-grained Token: Metadata: Read, Contents: Read, Issues: Read, Pull requests: Read, Actions: Read Issues: Read and write
GitLab read_api api — es gibt nichts dazwischen
Gitea / Forgejo read:repository, read:issue, read:user write:issue

Jeder Aufruf einzeln

Die folgenden Tabellen sind vollständig: mehr als das ruft Nebula nicht nach draußen. Sie sind aus src/lib/git/github.ts, gitlab.ts und gitea.ts erhoben — wer prüfen will, ob hier etwas fehlt, greppt dort nach getJson, getJsonSafe und sendJson.

GitHub — https://api.github.com, Header Authorization: Bearer …

Aufruf Wofür Benötigtes Recht
GET /rate_limit Verbindungstest keins
GET /user Kontoname beim Testen zurückmelden keins
GET /user/repos Auswahlliste hinter Repos laden Metadata: Read
GET /repos/{o}/{r} Stammdaten, Default-Branch, Sichtbarkeit Metadata: Read
GET …/languages Sprachverteilung Metadata: Read
GET …/tags Tags Metadata: Read
GET …/stats/commit_activity Commit-Heatmap über ein Jahr Metadata: Read
GET …/branches Branch-Liste Contents: Read
GET …/commits die letzten 100 Commits Contents: Read
GET …/releases Releases Contents: Read
GET …/readme README-Vorschau Contents: Read
GET …/pulls?state=open offene Pull Requests Pull requests: Read
GET …/issues?state=open offene Issues Issues: Read
GET …/issues/{nr} Abgleich: ist das verknüpfte Issue noch offen? Issues: Read
GET …/actions/runs CI-Zustand des Default-Branch Actions: Read
POST …/issues Aufgabe als Issue anlegen Issues: Read and write
PATCH …/issues/{nr} Issue schließen oder wieder öffnen Issues: Read and write

Dass /tags und /stats/commit_activity unter Metadata fallen und nicht unter Contents, ist nicht zu erraten — es steht so in GitHubs Berechtigungsübersicht.

GitLab — {Basis-URL}/api/v4, Header PRIVATE-TOKEN: …

Aufruf Wofür Benötigtes Recht
GET /projects?per_page=1 Verbindungstest read_api
GET /user Kontoname beim Testen zurückmelden read_user (in read_api enthalten)
GET /projects?membership=true Auswahlliste hinter Repos laden read_api
GET /projects/{id}?statistics=true Stammdaten, Default-Branch, Sichtbarkeit read_api
GET …/repository/branches · /tags · /commits Snapshot und Heatmap read_api
GET …/repository/tree · /repository/files/{datei} README (zwei Aufrufe, GitLab hat keinen README-Endpunkt) read_api
GET …/releases · /languages Releases, Sprachverteilung read_api
GET …/merge_requests?state=opened offene Merge Requests read_api
GET …/issues?state=opened offene Issues read_api
GET …/issues/{nr} Abgleich: ist das verknüpfte Issue noch offen? read_api
GET …/pipelines CI-Zustand des Default-Branch read_api
POST …/issues Aufgabe als Issue anlegen api
PUT …/issues/{nr} (state_event) Issue schließen oder wieder öffnen api

Gitea / Forgejo — {Basis-URL}/api/v1, Header Authorization: token …

Aufruf Wofür Benötigtes Recht
GET /version Verbindungstest keins — der Aufruf darf fehlschlagen, Nebula wertet ihn nur ergänzend aus
GET /user Kontoname beim Testen zurückmelden read:user
GET /user/repos bzw. GET /repos/search Auswahlliste hinter Repos laden read:repository
GET /repos/{o}/{r} Stammdaten, Default-Branch, Sichtbarkeit read:repository
GET …/branches · /tags · /commits · /releases · /languages Snapshot und Heatmap read:repository
GET …/contents · /contents/{datei} README (zwei Aufrufe, wie bei GitLab) read:repository
GET …/commits/{branch}/status CI-Zustand des Default-Branch read:repository
GET …/pulls?state=open offene Pull Requests read:repository — Gitea zählt PRs zum Repository-Scope, nicht zu den Issues
GET …/issues?state=open&type=issues offene Issues read:issue
GET …/issues/{nr} Abgleich: ist das verknüpfte Issue noch offen? read:issue
POST …/issues Aufgabe als Issue anlegen write:issue
PATCH …/issues/{nr} Issue schließen oder wieder öffnen write:issue

write:issue schließt read:issue mit ein — Gitea versteht write: als „lesen und schreiben". Beide anzuhaken schadet nicht, nötig ist es nicht.

Was Nebula nicht tut

  • Keine Kommentare. Nebula schreibt nie einen Issue-Kommentar, weder beim Anlegen noch beim Schließen. Was im Rumpf des angelegten Issues steht, ist die Aufgabenbeschreibung plus eine Zeile Rückverweis — danach fasst Nebula den Text nicht mehr an.
  • Kein Code. Nichts wird committet, gepusht, gemergt oder getaggt. Nebula ist eine Übersicht, keine Entwicklungsumgebung.
  • Keine Webhooks anlegen. Den Haken trägst du selbst im Git-System ein — genau deshalb verlangt Nebula dafür keine Administrationsrechte.
  • Keine Labels, keine Meilensteine, keine Zuweisungen.
  • Keine Adminrechte, keine Organisations- oder Benutzerverwaltung.

Kommentare und Labels, die an einem Issue hängen, stammen also entweder aus der Weboberfläche deines Git-Systems oder von einem anderen Werkzeug mit eigenem Token — nicht von Nebula.

Der GitLab-Sonderfall

GitLab kennt keinen schmalen Schreib-Scope für Issues. Laut Access token scopes gibt es nur api — „complete read and write access to the API" — und darunter direkt read_api. Wer bei GitLab Aufgaben als Issue anlegen will, vergibt damit ein Token, das auch Code ändern darf.

Wenn dir das zu weit geht: bleib bei read_api und verzichte auf die Issue-Anbindung. Der Abgleich läuft vollständig, und geschlossene Issues werden weiterhin erkannt und haken ihre Aufgabe ab — nur der Rückweg von Nebula ins Issue fällt weg. Ein eigenes Dienstkonto, das nur Zugriff auf die betroffenen Projekte hat, ist der andere Ausweg.

Im Zweifel klein anfangen

Ein fehlendes Recht führt zu einem 403 mit dem Hinweis „Token hat nicht die noetigen Scopes"; die Meldung steht am Repository auf der Projektseite. Fang mit Fall A an und leg gezielt nach, wenn du die Issue-Anbindung wirklich benutzt — statt vorsorglich alles zu vergeben.

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, Aufgaben, Git
    │   ├── tasks/              Aufgaben über alle Projekte, nach Fälligkeit
    │   ├── rueckblick/         Wochenrückblick
    │   ├── verlauf/            Fortlaufende Zeitleiste
    │   ├── connections/        Verwaltung der Git-Verbindungen
    │   ├── account/ admin/     Eigenes Konto · Adminbereich
    │   └── api/                REST-Endpunkte (siehe Abschnitt „API")
    ├── components/             UI-Komponenten (Board, Karten, Panels, Dialoge)
    ├── lib/
    │   ├── auth/               Sitzungen, Passwörter, TOTP, WebAuthn, Zugriffsschutz
    │   ├── crypto.ts           AES-256-GCM für Access Tokens
    │   ├── db.ts               Prisma-Client
    │   ├── git/                Provider-Adapter: github.ts · gitlab.ts · gitea.ts
    │   ├── sync/               Repo-Snapshot holen, speichern, Zeitgeber
    │   ├── tasks/              Aufgabenlogik, Brücke zu Git-Issues
    │   ├── webhooks/           Signaturprüfung eingehender Webhooks
    │   ├── review.ts           Wochengrenzen und Kennzahlen
    │   ├── timeline.ts         Cursor-Blättern für den Verlauf
    │   ├── security/           SSRF-Schutz, Ratenbegrenzung, Body-Prüfung
    │   └── 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 test Unit-Tests (Vitest, ohne Datenbank)
npm run test:e2e Ende-zu-Ende-Tests (Playwright, baut vorher)
npm run test:all Beides nacheinander
npm run docker:db Nur die Datenbank starten, mit Host-Port für npm run dev
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
POST /api/auth/mfa Zweiter Faktor: Einmalkennwort oder Wiederherstellungscode
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 POST PUT DELETE /api/account/totp Zwei-Faktor: Zustand, einrichten, aktivieren, abschalten
POST /api/account/totp/recovery Neue Wiederherstellungscodes
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 POST /api/templates Projektvorlagen auflisten / anlegen
PATCH DELETE /api/templates/:id Vorlage ändern / löschen
POST /api/templates/from-project Aus einem Projekt eine Vorlage machen
GET /api/admin/backup Vollständige Sicherung als JSON (Admin)
GET /api/search?q= Volltextsuche über eigene Notizen und Aufgaben
GET /api/activity Verlauf über alle Projekte, per Cursor (?vor=&id=&limit=)
GET POST /api/notifications Benachrichtigungsziele
PATCH DELETE /api/notifications/:id Ziel ändern / entfernen
POST /api/notifications/:id/test Testnachricht senden
GET PATCH DELETE /api/projects/:id Projekt lesen / ändern / löschen
POST /api/projects/:id/sync Alle Repositories des Projekts abgleichen
PATCH /api/projects/reorder Neue Reihenfolge im Brett
POST /api/projects/bulk Status, Tags oder Löschen für mehrere
GET /api/projects/:id/activity Verlauf des Projekts (?limit=)
GET POST /api/projects/:id/repositories Repositories des Projekts
PATCH DELETE /api/repositories/:id Repository ändern / vom Projekt lösen
POST /api/repositories/:id/sync Ein einzelnes Repository abgleichen
POST DELETE /api/repositories/:id/webhook Webhook einrichten / zurücknehmen
POST /api/webhooks/:repositoryId Eingehend von GitHub/GitLab/Gitea, per Signatur geprüft
GET POST /api/projects/:id/tasks Aufgaben des Projekts
PATCH /api/projects/:id/tasks/reorder Neue Reihenfolge nach dem Ziehen
POST /api/projects/:id/tasks/from-issues Offene Issues als Aufgaben übernehmen
PATCH DELETE /api/tasks/:id Aufgabe ändern / löschen
POST /api/tasks/:id/issue Aufgabe als Issue im verknüpften Repository anlegen
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_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 npm run docker:db? Ein blosses docker compose up -d db startet die DB ohne Host-Port — dann ist sie von npm run dev aus nicht erreichbar. 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

Am 25.07.2026 gegen echte Systeme verifiziert — beide Betriebsarten, alle drei Git-Adapter (GitHub, GitLab und Gitea jeweils gegen die öffentliche Instanz). Seitdem laufen die Ende-zu-Ende-Tests bei jeder Änderung mit; live nach draußen geht davon nur ein Test gegen die GitHub-API.

Werkzeugstand vom 18.08.2026:

Version
Node.js 24.19.0
npm 11.17.0
Next.js 15.5.21
Prisma 6.19.3
PostgreSQL 17.11 (Alpine)
Docker 29.7.2

Lizenz

MIT — siehe LICENSE.