Zum Inhalt springen
lehrr planrDoku

Dokumentation Entwickler

Für Entwickler

1. Tech-Stack#

WasWomitAnmerkung
FrameworkNext.js 16 (App Router)Achtung: Diese Version hat Breaking Changes gegenüber allem, was ein Modell aus dem Training kennt. Vor dem Schreiben von Code die Doku unter node_modules/next/dist/docs/ lesen - so steht es auch in AGENTS.md.
SpracheTypeScript, React 19
DatenbankPostgres 18.6 auf Neon (eu-central-1)Zugriff über postgres (postgres.js)
ORMDrizzle ORM 0.45 + drizzle-kit 0.31Schema in db/schema.ts, Migrationen in db/migrations/
Anmeldung@neondatabase/auth (Beta)Gekapselt hinter lib/auth.ts - bewegt sich das Paket, hängt der Wechsel an einer Datei
StylingTailwind CSS 4
ValidierungZod 4Vor allem für die MCP-Werkzeugschemata
Dateien@vercel/blob (privater Store)Store planr-material, Region fra1
MCP@modelcontextprotocol/sdk 1.30stdio und Streamable HTTP aus einer Registrierung
Teststsx gegen .test.ts-DateienKein Test-Framework, keine Datenbank, kein Mocking
HostingVercel (Funktionen in fra1)

Rund 45.700 Zeilen TypeScript/TSX in app, lib, components, mcp, db.


2. Lokales Setup#

npm install

Dann .env.local anlegen. Das absolute Minimum ist eine Datenbank:

DATABASE_URL=postgresql://user:pass@host/planr

Ohne NEON_AUTH_BASE_URL und NEON_AUTH_COOKIE_SECRET antwortet proxy.ts in Produktion mit 503 - eine vergessene Variable darf nicht bedeuten, dass alles offensteht. Lokal (NODE_ENV=development) läuft es ohne.

Migrationen einspielen:

npm run db:migrate

Schulkalender anlegen (Schuljahr, Ferien, Feiertage für MV):

npm run db:seed-kalender

Starten:

npm run dev

Umgebungsvariablen#

Betrieb

VariableZweck
DATABASE_URLPostgres. Wird auf Vercel von der Neon-Marketplace-Ressource verwaltet - von Hand überschreiben wird beim nächsten Anfassen der Integration zurückgesetzt.
NEON_AUTH_BASE_URL, NEON_AUTH_COOKIE_SECRETAnmeldung. Fehlen sie in Produktion: 503.
PLANR_REGISTRIERCODEZugangswort für /registrieren. Fehlt es in Produktion, ist die Registrierung zu, nicht offen. Lokal bleibt sie offen.
CRON_SECRETBearer für den täglichen Cron. Ohne Secret ist der Endpunkt lokal offen.
BLOB_READ_WRITE_TOKENDatei-Store.
TELEGRAM_TOKEN, TELEGRAM_CHAT_IDServerfehler als Nachricht, höchstens alle zehn Minuten je Route.
PLANR_API_TOKENÜbergangsweise gültiger globaler Schlüssel, gerechnet als Schlüssel des Kopplungsnutzers. Soll ersatzlos verschwinden.
PLANR_KOPPLUNGEN_NUTZERWem die Zugänge zu selbr, drillr, checkr und dem Experimente-Katalog gehören.

Kopplungen (alle optional, jede fällt einzeln still aus)

VariableWofür
SELBR_SUPABASE_URL, SELBR_SUPABASE_SERVICE_ROLE_KEYLernstand, Fokus, Hausaufgaben
DRILLR_API_KEYÜbungsblätter erzeugen
CHECKR_URL, CHECKR_REPORT_TOKENKorrekturaufträge und Rückkanal. Ohne CHECKR_URL gilt https://check-r.org. checkr.org ohne Bindestrich gehört jemand anderem und darf nie angesprochen werden, obwohl beide Domains mit 200 antworten.
PEK_SUPABASE_URL, PEK_SUPABASE_SERVICE_ROLE_KEYExperimente-Katalog
PRUEFR_URLArbeiten bauen
PLANR_MINI_URL, PLANR_MINI_TOKENModellbrücke für den Chat auf der Kursseite
MAIL_SMTP_URL, MAIL_ABSENDER, NEON_API_KEYEigener Mailversand mit deutschen Texten, siehe docs/mail.md

Fallstricke#

  • Skripte laden .env.local nicht von selbst. Sie brauchen --env-file-if-exists=.env.local, sonst landen sie auf einer lokalen Standard-Instanz statt auf der Datenbank, die die App benutzt.
  • drizzle-kit push gibt es hier nicht. Es wurde aus den Skripten entfernt: Es ändert die Datenbank sofort, ohne Datei und ohne Spur - und die Datenbank aus .env.local ist die Produktion.
  • Der Build läuft ohne DATABASE_URL. Alle Seiten sind force-dynamic, zur Bauzeit wird nichts abgefragt. Deshalb baut auch der Auth-Handler erst beim ersten Aufruf und nicht beim Laden des Moduls.

3. Projektstruktur#

app/
  (arbeit)/          Alles hinter der Anmeldung. Die Klammer erzeugt kein
                     Pfadsegment - sie ist die Grenze: /geteilt und /anmelden
                     liegen ausserhalb und bekommen die Kursliste damit auch
                     nicht versteckt im HTML mitgeliefert.
    klasse/[id]/     Übersicht, einheiten, stunden, stundenplan, stunde/[seq],
                     ka/[seq], stunde/[seq]/canva (HTML-Export)
    vorlage/[id]/    Vorlagen, vorlage/neu
    konto/           Einstellungen, Schlüssel, Export, Löschen
    freigabe/  vergleich/  zettel/  hilfe/  schuljahr/
  (vollbild)/        Präsentationsansicht für den Beamer
  (rechtliches)/     /regeln, /datenschutz, /impressum - ohne Anmeldung
  api/               21 Routen, siehe unten
  willkommen/  anmelden/  registrieren/  geteilt/[token]/
  actions.ts         41 Server-Actions

lib/                 Die Fachlichkeit. Reine Rechnung getrennt von Datenzugriff.
  engine/            Der Planungskern: buildPlan, weekPlan, schoolWeeks,
                     kalender, datum, termine, timelineLayout - alles pur
  besitz.ts          Die eine Stelle, die "gehört das mir?" beantwortet
  betreiber.ts       Welche Felder ein fremdes Konto gar nicht erst bekommt
  kopplungen.ts      Darf der gerade Arbeitende die fremden Zugänge benutzen?
  apiZugang.ts       Bearer-Schlüssel prüfen, Arbeit im Namen des Inhabers
  apiKontext.ts      AsyncLocalStorage statt eines Parameters durch 30 Funktionen
  *Api.ts            Die Prüfungen hinter den HTTP-Routen (rein, testbar)
  *.test.ts          51 Testdateien

components/          React-Komponenten, überwiegend Server Components
mcp/                 registrieren.ts (Server), werkzeuge.ts (27 Werkzeuge),
                     routen.ts (Pfad -> Handler), inprozess.ts, server.ts (stdio)
db/                  schema.ts (31 Tabellen), migrations/ (0000-0021)
scripts/             Seeds, Importe, Prüfskripte, Nachtlauf
docs/                Diese Dokumentation
paket/               npm-Paket `planr-mcp` (stdio-Hülle)
proxy.ts             Optimistische Cookie-Prüfung, nicht die Autorisierung
instrumentation.ts   onRequestError -> Protokoll + Telegram

Zwei Prinzipien, die überall wiederkehren#

Die Rechnung ist rein, der Datenzugriff steht daneben. lib/sequenz.ts, lib/abschnitte.ts, lib/zeitschiene.ts, lib/vplan.ts, lib/versuchsnote.ts und die ganze lib/engine/ kennen keine Datenbank. Deshalb sind sie ohne Aufwand testbar, und deshalb benutzen Oberfläche und Skript dieselbe Rechnung.

Die Absicherung sitzt in der Datenschicht, nicht in der Oberfläche. Ein Knopf, den man nicht sieht, ist keine Zugangsprüfung. lib/mehrbenutzer.test.ts liest den Quelltext von app/actions.ts und verlangt, dass jede exportierte Aktion entweder den Besitz auflöst oder mit Begründung auf einer Ausnahmeliste steht.


4. Datenmodell#

31 Tabellen in public, dazu eine Spalte aus dem Schema neon_auth (dem Anmeldedienst gehörend, von drizzle-kit über schemaFilter: ["public"] ausgeschlossen).

Nur drei Tabellen tragen einen Besitzer: classes, plan_templates und share_links. Alles andere hängt per Fremdschlüssel mit on delete cascade darunter und erbt ihn. Dazu kommen sperrzeiten, schul_einstellungen, vplan_uebernahmen und api_keys mit eigenem owner_id, weil sie an keiner dieser Wurzeln hängen.

erDiagram
    neon_auth_user ||--o{ classes : besitzt
    neon_auth_user ||--o{ plan_templates : besitzt
    neon_auth_user ||--o{ share_links : besitzt
    neon_auth_user ||--o{ api_keys : besitzt

    rahmenplaene ||--o{ rahmenplan_themen : "Themen mit Richtwert"
    rahmenplaene ||--o{ plan_templates : "wird übersetzt von"

    plan_templates ||--o{ template_items : "Stundenliste"
    plan_templates ||--o{ template_blocks : "Themenblöcke (Altweg)"
    plan_templates ||--o{ template_milestones : "Leistungen"
    plan_templates ||--o{ template_shares : "weitergegeben an"
    template_milestones ||--o{ template_requirements : "Mindestanforderungen"
    rahmenplan_themen ||--o{ template_items : "deckt ab"

    classes ||--o{ class_slots : "Stundenplan"
    classes ||--o{ termine : "Kalender-State"
    classes ||--o{ curriculum_items : "Stundenfolge"
    classes ||--o{ class_blocks : "Snapshot der Vorlage"
    classes ||--o{ class_milestones : "Snapshot der Vorlage"
    classes ||--o{ wiederholungs_signale : "was gewackelt hat"
    classes ||--o{ vplan_uebernahmen : "Protokoll"
    class_milestones ||--o{ class_requirements : "Snapshot der Vorlage"

    curriculum_items ||--o| lesson_plans : "Detailplanung (1:1)"
    lesson_plans ||--o{ lesson_phases : "Verlauf"
    lesson_plans ||--o{ lesson_materials : "Material"
    lesson_plans ||--o{ lesson_slides : "Folien"

    schuljahre ||--o{ schulferien : "Land"
    schuljahre ||--o{ sperrzeiten : "Schule"
    schuljahre ||--o{ schul_einstellungen : "A/B-Modus, vplan_url"

    feedback ||--o{ feedback_notizen : "Antwortspur"
    neon_auth_user ||--o{ planungs_freunde : "einlader / freund"

Was man an diesem Modell verstanden haben muss#

curriculum_items ist die Stundenfolge, termine der Kalender. Die Planung (lesson_plans) hängt am Stundeneintrag, nicht am Termin - fällt eine Stunde aus, wandert die Planung mit dem Stoff und klebt nicht am Datum.

class_blocks, class_milestones und class_requirements sind Kopien, keine Joins auf die Vorlage. Die Anforderungen waren bis zum 21.08.2026 ein Live-Join über classes.template_id (eine Spalte ohne Fremdschlüssel) - wer seine Vorlage löschte, verlor die Anforderungen aller daraus entstandenen Klassen.

template_items ist der neue Weg, template_blocks der alte. Bis zum 21.08.2026 speicherte eine Vorlage nur "Terme, 9 Doppelstunden" und planr rechnete "Terme (1/9)" bis "(9/9)". Jetzt trägt jede Zeile ihren eigenen Titel. Während der Umstellung gibt es beide nebeneinander: erzeugeSequenz liest die Stundenliste, wenn es sie gibt, und rechnet sonst aus den Blöcken. Die Blöcke tragen weiterhin die Wochenverteilung und die Meilenstein-Fenster.

Arrays statt Zwischentabellen bei share_links.classIds und planungs_freunde.classIds, weil daran nichts hängt: Die Liste wird immer vollständig gelesen und einmal geschrieben. Achtung: Leer bedeutet in beiden Fällen "alle Kurse des Besitzers" - beim Löschen eines Kurses darf man deshalb nicht einfach eine Id herausstreichen, sonst wird der Link weiter statt enger.

lesson_slides.tafel und lesson_slides.inhalt sind tot. Das Tafelbild wurde am 16.08.2026 abgebaut, inhalt von Migration 0003 nach bloecke überführt. Die Spalten bleiben stehen: Eine Migration für ein totes Feld ist Risiko ohne Gegenwert, und ein Rückbau des Deployments fände sonst leere Folien vor.

Migrationen#

npm run db:generate   # vergleicht db/schema.ts mit dem letzten Stand, schreibt eine Datei
npm run db:migrate    # spielt ein, was fehlt, und vermerkt es in drizzle.__drizzle_migrations

0000_baseline.sql beschreibt den Stand vom 11.08.2026 und ist durchgehend idempotent (CREATE TABLE IF NOT EXISTS, Fremdschlüssel in DO $$ ... EXCEPTION WHEN duplicate_object): Sie fasst eine bestehende Datenbank nicht an und legt auf einer leeren alles an. Zweimal ausführen ändert nichts.


5. API-Referenz#

21 Routen. Alle Datenrouten sind über einen persönlichen Bearer-Schlüssel geschützt (lib/apiZugang.ts -> mitSchluessel), anzulegen unter Einstellungen -> Schnittstellen-Schlüssel. Gespeichert wird nur der SHA-256-Hash; planr muss einen Schlüssel prüfen können, nie zurücklesen.

Ohne gültigen Schlüssel: 401 {"fehler":"nicht autorisiert"}. Die Arbeit läuft danach im Namen des Inhabers - alle Abfragen darunter filtern auf ihn, ohne ihn als Parameter zu kennen (AsyncLocalStorage in lib/apiKontext.ts).

Lerngruppen heißen wie im Stundenplan (9d). Gibt es dieselbe in zwei Fächern, gehört fach dazu.

Planung#

RouteParameterAntwort
GET /api/stunde?datum=YYYY-MM-DDWas für den Tag geplant war, was an Rückmeldung fehlt
GET /api/stunde?klasse=8d&seq=1statt seq geht datumEine Stunde vollständig: Verlauf, Material, Hausaufgabe
POST /api/stunde{klasse, datum|seq, stundenziel, phasen[], material[], ...}Planung eintragen und das Soll festschreiben
POST /api/rueckmeldung{klasse, datum, gehalten|ausgefallen, zielErreicht?, notiz?, wackelt?[]}Ist-Rückmeldung. wackelt legt Wiederholungssignale an, geprüft gegen die Themen der Übung
GET /api/grobplan?klasse=9dfach?Einheiten mit Nummern
POST /api/grobplan{klasse, werkzeug: "einheit_laenge"|"leistung_verschieben", ...}Geändert wird über Nummern, nie über Namen
GET /api/klassenprofil?klasse=9dfach?Klassenprofil - nur für den Betreiber
POST /api/klassenprofil{klasse, profil, anhaengen?}dito

Zweistufig schreiben (Entwurf + Prüfsumme)#

RouteModusAntwort
POST /api/stundenplan{modus:"entwurf", stundenplan}{pruefsumme, klartext, warnungen}
POST /api/stundenplan{modus:"uebernehmen", stundenplan, pruefsumme}{classId, termine, klartext}
GET /api/vorlageohne ParameterDie eigenen Vorlagen
GET /api/vorlage?fach=&jahrgang=land?Der Rahmenplan mit seinen Themen
POST /api/vorlage{modus:"anlegen", neu}{templateId, name, klartext}
POST /api/vorlage{modus:"entwurf", vorlage}{pruefsumme, klartext, zeilen}
POST /api/vorlage{modus:"uebernehmen", vorlage, pruefsumme}{templateId, zeilen, klartext}

Die Prüfsumme (lib/pruefsumme.ts) ist die Klammer: Geschrieben wird genau das, was im Entwurf stand. Dazwischen gehört ein Mensch, der den Klartext liest.

Tagesarbeit#

RouteParameter
GET /api/alltag?was=nachtraegeheute?
GET /api/alltag?was=woche&von=&bis=
GET /api/alltag?was=material&klasse=&datum=fach?
POST /api/alltag{was:"material_anlegen", klasse, datum, titel, typ?, link?}
POST /api/alltag{was:"material_abhaken", klasse, datum, titel, kopiert?, aufIserv?}
POST /api/alltag{was:"tu", klasse, datum} - erzeugt eine echte Übung in drillr
POST /api/alltag{was:"hausaufgabe", klasse, datum, aufgaben, hinweis?}

Material wird über den Titel angesprochen, nicht über eine Id: Wer sagt "das Arbeitsblatt ist kopiert", hat keine Nummer im Kopf. Ein Teilstück genügt, ein exakter Treffer gewinnt gegen jedes Teilstück, Mehrdeutigkeit ist ein Fehler mit Liste statt einer Vermutung (waehleMaterial in lib/material.ts).

Kopplungen nach draußen#

RouteFürWas hinausgeht
GET /api/tu-vorschlagdrillrNächste Stunde, Übungsthemen, Auffrischung, behandelt (was die Klasse schon hatte), optional die Vorstunde
GET /api/anforderungenprüfrMeilensteine mit Anforderungen. Nur lesend, keine Schülerdaten
GET /api/stoffverteilungmerkrJe Kurs die Stundenfolge mit Datum, Thema, Einheit, Stundenziel und Verlauf, dazu Ferien, Halbjahresgrenze und der gerechnete Tagesplan aus dem Vertretungsplan. Format kursbuch-plan-2
GET /api/hausaufgabenmerkrJe Kurs die laufenden Aufträge, je Zugangscode der Stand. Nur der Code geht hinaus, nie ein Name
GET /api/selbr-focusCronTäglich 5 Uhr: Fokus, Hausaufgaben und Freischaltung nach selbr. Bearer CRON_SECRET, läuft ausdrücklich alsNutzer(inhaber, ...)

Verwaltung#

RouteZweck
GET/POST /api/zettelFehler und Wünsche lesen (alle=1, art=fehler|wunsch), antworten und den Stand setzen
GET /api/versionWelcher Commit läuft (VERCEL_GIT_COMMIT_SHA). Für den Nachtlauf gebaut
POST /api/mcpMCP über Streamable HTTP. GET und DELETE antworten mit 405
POST /api/material/uploadToken für den Direkt-Upload aus dem Browser. Diese Route ist die Zugangsprüfung
GET /api/material/[id]Datei ausliefern, mit Besitzprüfung unmittelbar neben dem Abruf
/api/auth/[...path]Anmeldeweg, vom Paket bereitgestellt
POST /api/auth-mailMail-Webhook von Neon Auth. Signatur als abgetrenntes Ed25519-JWS geprüft - der rohe Rumpf muss roh bleiben. Nicht aktiviert

6. MCP-Server#

27 Werkzeuge, 15 lesend, 12 schreibend. Zwei Transporte, eine Registrierung (mcp/registrieren.ts) - sonst laufen die beiden Wege irgendwann auseinander.

# entfernt (der Normalfall)
claude mcp add --transport http planr https://deine-adresse/api/mcp --header "Authorization: Bearer planr_..."

# lokal über stdio
PLANR_URL=http://localhost:3000 PLANR_API_TOKEN=planr_... npm run mcp

Der Endpunkt ist zustandslos, und das ist keine Sparsamkeit: Eine Vercel-Funktion lebt für die Dauer einer Anfrage, eine Sitzung im Arbeitsspeicher fände die nächste nicht wieder. Also je Anfrage ein frischer Server ohne Sitzungskennung, und JSON statt Datenstrom (enableJsonResponse) - planr schickt nichts von sich aus.

Der Endpunkt ruft die Routen auf, nicht die Bibliotheken darunter (mcp/inprozess.ts, mcp/routen.ts). In den Routen stehen die Prüfung der Eingaben und die Form der Antwort; daran vorbeizugehen hieße, sie ein zweites Mal zu schreiben. Statt fetch bekommt der Aufrufer eine Zuordnung Pfad -> Handler untergeschoben, damit ein Werkzeugaufruf keine zweite Funktionsausführung kostet.

Der Chat auf der Kursseite benutzt dieselbe Zuordnung, führt die Werkzeuge aber in der angemeldeten Sitzung aus statt über einen Schlüssel.

Vollständige Werkzeugliste und der Weg vom Rahmenplan zur Stundenliste: docs/mcp.md. Die stdio-Hülle als npm-Paket liegt in paket/.


7. Deployment#

Vercel-Projekt planr, Funktionen in fra1 (vercel.json), Datenbank in eu-central-1. Beides gehört zusammen: planr stellt viele kleine Abfragen statt weniger großer und ist damit latenzgebunden. Der Prüfstein ist der Kopf x-vercel-id - dort muss fra1::fra1 stehen.

Die 18 Neon-Variablen verwaltet die Marketplace-Ressource, nicht die Hand. Datenbank wechseln heißt: Ressource trennen, neue verbinden.

CI (.github/workflows/pruefung.yml) läuft bei jedem Push und PR auf main:

npm test && npm run lint && npm run build

Fehlermeldung über instrumentation.ts (onRequestError): Jeder Serverfehler geht ins Protokoll und, wenn die Telegram-Variablen gesetzt sind, als Nachricht hinaus. Derselbe Fehler auf derselben Route meldet sich höchstens alle zehn Minuten. Kopfzeilen des Aufrufs gehen bewusst nicht mit - darin steht das Sitzungs-Cookie.

Migrationen laufen nie automatisch. db:migrate ist ein Kommando von Hand. Deployter Code vor einer Spalte, die es nicht gibt, ist der Fehler, den nachts niemand bemerkt - deshalb rührt auch der nächtliche Zettel-Lauf nichts an, was eine Migration braucht.


8. Mitarbeit#

Tests#

npm test

51 Testdateien, ausgeführt über tsx, ohne Framework und ohne Datenbank. Die Regel dahinter: Was gerechnet wird, wird rein gerechnet und dann geprüft.

Drei Testarten fallen aus dem Rahmen:

  • Wachen über den Quelltext. lib/freigabe.test.ts liest den Quelltext von lib/freigabe.ts und verbietet dort bestimmte Feldnamen - wer notizen, reflexion oder checkr in der geteilten Datenschicht ergänzt, fliegt auf. lib/mehrbenutzer.test.ts tut dasselbe für die Besitzprüfung in allen 41 Server-Actions, lib/planungsfreunde.test.ts für die Vergleichsansicht.
  • Charakterisierungstests. lib/engine/kalender.test.ts hält fest, was der Schulkalender vor dem Umbau gerechnet hat: 39 Schulwochen, letzte am 28.06.2027, 74 Termine bei Di+Do. Beim Umstieg darf sich nichts verschieben.
  • Doppelte Rechnung. lib/stundenplan-raster.test.ts misst alle sechs echten Lerngruppen zweimal - einmal getippt, einmal geklickt -, und die Termine müssen gleich sein. Zwei Rechnungen nebeneinander wären zwei Gelegenheiten, auseinanderzulaufen.

Prüfskripte gegen die echte Umgebung#

SkriptWas es prüft
scripts/vplan-pruefen.tsAdresse, Stand, abgedeckte Tage und jeden Befund des Vertretungsplans
scripts/blob-pruefen.tsLegt eine Wegwerf-Datei an und prüft, dass ihre Adresse ohne Anmeldung gesperrt ist
scripts/upload-pruefen.tsDie sechs Zugangsentscheidungen der Upload-Route
scripts/mail-pruefen.tsSPF, DKIM, DMARC, auf Wunsch eine echte Testmail
scripts/chat-probe.ts --messenOb das kleine Modell die richtigen Werkzeuge greift

Commit-Stil#

Die Commit-Nachricht sagt in einem deutschen Satz, was danach anders ist - nicht, was geändert wurde:

Oberstufenkurse gelten nicht mehr als ausgefallen
Der Zettel arbeitet sich nachts selbst ab
Ein Kurs lässt sich auch wieder loswerden

Ältere Commits tragen Präfixe (feat:, fix:, docs:), neuere nicht mehr.

Wo Entscheidungen dokumentiert werden#

Im Quelltext, direkt über der Stelle. planr hat ungewöhnlich lange Kommentare, und das ist Absicht: Sie erklären nicht, was der Code tut, sondern warum er nicht anders aussieht, und welche Alternative aus welchem Grund verworfen wurde. Wer etwas ändert, sollte den Kommentar zuerst lesen - meistens steht dort schon, warum die naheliegende Lösung nicht funktioniert hat.

Der größere Bogen steht in der CLAUDE.md im Wurzelverzeichnis; die Etappenpläne liegen in docs/superpowers/plans/.

Regeln, die nicht verhandelbar sind#

  1. Keine Schülerdaten. Kein Feld, keine Tabelle, kein Umweg.
  2. Der Besitz wird in der Datenschicht geprüft, nicht in der Oberfläche.
  3. Nicht vorhanden und nicht meins geben dieselbe Antwort. Ein "gibt es nicht" gegen ein "gehört dir nicht" verriete, welche Ids belegt sind.
  4. Kopplungen dürfen still ausfallen, aber nicht still leer aussehen. Nicht konfiguriert und nicht erreichbar sind zwei verschiedene Zustände.
  5. Schemaänderungen über Migrationen, nie über push.