CUSTOM APP

Dein Firmen-OS in 7 Schritten.

Der Bauplan zum Reel, Schritt für Schritt mit fertigen Prompts: Kunden, Arbeit, Geld, Content, Automationen und Cockpit in einem Dashboard. Gebaut mit Claude Code, Next.js, Supabase und Vercel.

  • Fortgeschritten

ERGEBNIS

Das ist dabei rausgekommen.

Der Überblick aus dem Reel. Darunter steht der Bauplan mit allen Prompts.

  • 867Commits (21.05. bis 03.10.2026)
  • 16Automationen, laufen nachts
  • 7Schritte im Bauplan

DAS BRAUCHST DU

Womit das gebaut ist.

  • Claude Code
  • Supabase
  • Next.js
  • Vercel

Der Überblick: sieben Module.

Ein Firmen-OS ist kein fertiges Tool, sondern ein Dashboard, das du dir selbst baust. Jedes Modul ist ein eigener Schritt, den du einzeln dazusteckst und im Alltag testest.

Gebaut wird mit Claude Code. Stack: Next.js, Supabase (Datenbank und Login), Tailwind und Vercel. Für jeden Schritt unten findest du, was das Modul können soll, das Datenmodell in Klartext, einen fertigen Prompt zum Einfügen in Claude Code und die typische Falle.

  1. Claude Code und eine Datenbank

    1. Fundament

    Next.js-Projekt, Supabase, Login, Navigation, CLAUDE.md. Alles an einem Ort.

  2. Kontakte, Deals, Pipeline

    2. Kunden

    Firmen, Kontakte und Deals. Du siehst, wer wo steht.

  3. Aufgaben, Termine, Zeiterfassung

    3. Arbeit

    Aufgaben, Kalender-Sync und Zeit, alles am selben Kunden.

  4. Rechnungen und Fixkosten

    4. Geld

    Rechnungen verwalten, Fixkosten im Blick.

  5. Von der Idee zum fertigen Video

    5. Content

    Eine Pipeline mit festen Stufen, KI nur auf Klick.

  6. Geplante Jobs, die nachts laufen

    6. Automationen

    Vercel Cron, geschützt, protokolliert, mit KI-Tagesplan.

  7. Startseite „Heute“

    7. Cockpit

    Fünf Blöcke, die zeigen, was heute wichtig ist.

Schritt 1: Fundament.

Claude Code baut, eine Datenbank hält alles zusammen. Hier entsteht das Gerüst, auf das jedes weitere Modul aufsetzt.

Projekt
Next.js mit App Router, TypeScript und Tailwind, lokal lauffähig.
Login
Alle Seiten liegen hinter einem Login (Supabase Auth).
Navigation
Eine Registry (src/lib/nav.ts) mit fünf Bereichen: Heute, Kunden, Arbeit, Content, Finanzen.
CLAUDE.md
Die Regeln für Claude Code stehen im Repo und gelten für jedes spätere Modul.
Deploy
Push auf main, Vercel baut.
Tabellen
Noch keine Fachtabellen, nur settings (Nutzer, Zeitzone, KI-Modell). Jede spätere Tabelle trägt user_id und Row Level Security.
Geklappt, wenn
Die leere Startseite läuft lokal, nach dem Login.
PROMPT-1-FUNDAMENT.md
KONTEXT
Ich baue mir ein Firmen-OS: ein Dashboard für meine Selbstständigkeit, in dem Kunden, Arbeit, Geld, Content und Automationen an einem Ort laufen. Ich bin der einzige Nutzer. Das Projekt wächst Modul für Modul, dieses ist das Fundament.

Stack: Next.js (App Router, TypeScript), Tailwind CSS, Supabase (Auth + Postgres), Deploy auf Vercel.

ZIEL
Ein lauffähiges Grundgerüst: Login, Datenbankanbindung, Navigation mit fünf Bereichen, leere Startseite "Heute" und eine CLAUDE.md mit den Regeln für alle späteren Module.

SCHRITTE
1. Lege das Projekt mit create-next-app an (TypeScript, Tailwind, App Router, src-Ordner, ESLint). Kein zusätzliches UI-Framework.
2. Installiere @supabase/supabase-js und @supabase/ssr. Lege drei Clients in src/lib/supabase/ an:
   - browser.ts (Anon-/Publishable-Key, nur für Client-Komponenten)
   - server.ts (Cookies, für Server Components und Server Actions)
   - admin.ts (Service-Role-Key, beginnt mit `import 'server-only'`, nur für Cron-Routen und Server-Code)
3. Auth: Login-Seite /login mit E-Mail und Passwort. Eine Middleware leitet jede Seite außer /login und /api/cron/* auf /login um, wenn keine Session da ist. Die Cron-Routen schützen sich später selbst per Secret.
4. Navigation: src/lib/nav.ts als einzige Registry. Fünf Bereiche in dieser Reihenfolge: Heute (/today), Kunden, Arbeit, Content, Finanzen. Rail oder Sidebar am Desktop, Bottom-Nav am Handy. Ein neuer Bereich entsteht nur in nav.ts.
5. Lege /today als leere Seite mit Überschrift "Heute" an und mache sie zur Startseite nach dem Login.
6. Datenbank: Lege supabase/migrations/001_settings.sql an:
   - Tabelle public.settings (user_id uuid primary key references auth.users(id) on delete cascade, display_name text, timezone text not null default 'Europe/Berlin', ai_model text not null default 'claude-haiku-4-5', created_at timestamptz not null default now())
   - Row Level Security aktiv, Policy "Users manage own settings" for all using (auth.uid() = user_id) with check (auth.uid() = user_id)
7. .env.example mit den Namen (ohne Werte): NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, CRON_SECRET, ANTHROPIC_API_KEY. .env.local kommt in .gitignore.
8. Lege CLAUDE.md im Projektroot an mit diesen Konventionen:
   - Stack und Ordnerstruktur (src/app, src/lib, supabase/migrations)
   - Jede Tabelle hat user_id uuid not null references auth.users(id) on delete cascade, created_at, updated_at, aktivierte RLS und die Policy "Users manage own <tabelle>".
   - Primärschlüssel: uuid default gen_random_uuid(). Geld immer numeric(10,2), nie float.
   - Datenbankänderungen nur als neue nummerierte Migrationsdatei, nie eine alte Datei ändern.
   - Datumslogik immer in Europe/Berlin, nie new Date().toISOString().split('T')[0].
   - Kein `any` in TypeScript. Kein Service-Role-Key im Client-Code.
   - Neue Seiten tragen sich in src/lib/nav.ts ein.
   - Commits auf Englisch im Conventional-Commits-Format.

AKZEPTANZKRITERIEN
- `npm run dev` startet ohne Fehler, `npm run build` und `npm run lint` laufen durch.
- Ohne Login lande ich auf /login, mit Login auf /today.
- In nav.ts stehen die fünf Bereiche, die Navigation zeigt sie an.
- Die Migration ist geschrieben, aber noch nicht eingespielt.
- Im Repo steht kein Key und kein Passwort.

LEITPLANKEN
- Frag mich nach, bevor du ein Supabase-Projekt anlegst oder mit echten Keys arbeitest. Ich lege das Projekt selbst an und trage die Werte in .env.local ein.
- Frag nach, bevor du ein Paket installierst, das ich oben nicht genannt habe.
- Frag nach, bevor du Dateien löschst oder überschreibst, die du nicht selbst angelegt hast.
- Baue nichts, was oben nicht steht: keine Beispieldaten, keine Dark-Mode-Umschaltung, keine Dashboards.
- Erkläre am Ende in fünf Zeilen, was du angelegt hast und was ich selbst tun muss (Supabase-Projekt, Migration einspielen, Vercel verbinden).

Typische Falle: Der Service-Role-Key von Supabase umgeht jede Zugriffsregel. Landet er in einer Variable mit NEXT_PUBLIC_ oder in Client-Code, kann ihn jeder im Browser lesen. Lege ihn nur in .env.local und in den Vercel-Einstellungen ab, benutze ihn nur in einer Datei, die mit import 'server-only' beginnt, und committe nie eine .env-Datei.

Schritt 2: Kunden.

Das erste Fachmodul: Wer sind meine Kunden und Interessenten, und wo steht jeder?

Firmen
Eine Liste mit Status Lead, Kunde oder ehemalig, mit Suche und Filter.
Kontakte
Personen, optional einer Firma zugeordnet.
Deals
Jeder Deal hat eine Stufe, einen nächsten Schritt und ein Datum dafür.
Pipeline
Spalten pro Stufe, überfällige nächste Schritte fallen rot auf.
Verlauf
Notiz, Anruf, Mail oder Termin pro Firma mit einem Klick erfassen.
Tabellen
companies (Name, Domain, Branche, Lifecycle), contacts (Firma, Name, E-Mail, Telefon, Rolle), pipeline_stages (Name, Reihenfolge, Wahrscheinlichkeit, gewonnen/verloren), deals (Firma, Stufe, Titel, nächster Schritt, Datum, Verlustgrund), activities (Firma, Art, Text, Zeitpunkt).
Geklappt, wenn
Du siehst auf einen Blick, bei wem als Nächstes etwas ansteht.
PROMPT-2-KUNDEN.md
KONTEXT
Fundament steht (Next.js, Supabase, Auth, nav.ts, CLAUDE.md). Ich baue jetzt das Kunden-Modul: Firmen, Kontakte, Deals in einer Pipeline und ein Verlauf pro Kunde. Halte dich an die Konventionen in CLAUDE.md (user_id, RLS, gen_random_uuid, Migrationen nummeriert).

ZIEL
Ich sehe auf einen Blick, wer in meiner Pipeline wo steht und bei wem als Nächstes etwas ansteht.

DATENBANK (supabase/migrations/002_crm.sql)
- companies: name (not null), domain, industry, notes, lifecycle text not null default 'lead' mit check (lifecycle in ('lead','customer','former')), created_at, updated_at, dazu user_id.
- contacts: company_id (nullable, on delete set null), first_name (not null), last_name, email, phone, role, notes, created_at, updated_at, user_id.
- pipeline_stages: name, sort_order integer, probability integer check (probability between 0 and 100), is_won boolean default false, is_lost boolean default false, user_id. Lege beim ersten Aufruf der Pipeline-Seite sechs Standardstufen an, falls keine existieren: Anfrage, Gespräch, Angebot, Verhandlung, Gewonnen, Verloren.
- deals: company_id (not null, on delete cascade), contact_id (nullable), stage_id, title, value numeric(10,2) (nullable), next_step text, next_step_on date, lost_reason text, closed_at timestamptz, created_at, updated_at, user_id.
- activities: company_id, deal_id (nullable), kind text mit check (kind in ('note','call','mail','meeting')), body text, occurred_at timestamptz default now(), user_id.
Index auf deals(stage_id), deals(next_step_on), activities(company_id, occurred_at desc). RLS und Policy "Users manage own <tabelle>" auf allen fünf Tabellen.

SEITEN UND ROUTEN
- /clients: Liste aller Firmen mit Lifecycle-Filter, Suche, Button "Neue Firma".
- /clients/[id]: Kopf mit Firma, darunter Tabs Kontakte, Deals, Verlauf. Im Verlauf kann ich Notiz, Anruf, Mail oder Termin mit einem Klick erfassen.
- /crm/contacts: alle Kontakte, nach Firma filterbar.
- /crm/pipeline: Spalten pro Stufe, Karten sind Deals (Titel, Firma, nächster Schritt mit Datum). Deal zwischen Spalten verschieben per Dropdown (Drag and Drop nur, wenn es ohne zusätzliches Paket geht). Überfällige nächste Schritte rot markiert.
- Alle Routen tragen sich in nav.ts unter "Kunden" ein.

AKZEPTANZKRITERIEN
- Ich kann eine Firma, einen Kontakt und einen Deal anlegen und den Deal durch alle Stufen schieben.
- Beim Verschieben in eine Stufe mit is_won oder is_lost wird closed_at gesetzt.
- Ein Deal in "Verloren" verlangt eine kurze Begründung (lost_reason).
- Es gibt genau eine Liste für Firmen und Kunden, getrennt nur über das Feld lifecycle.
- Ein zweiter Nutzer (Test-Account) sieht keine meiner Zeilen. Prüfe das mit einer SQL-Abfrage oder einem Test.
- `npm run build` und `npm run lint` laufen durch.

LEITPLANKEN
- Frag nach, bevor du die Pipeline-Stufen anders benennst oder anders gewichtest als oben.
- Frag nach, bevor du ein Drag-and-Drop-Paket installierst.
- Frag nach, bevor du bestehende Tabellen aus Migration 001 änderst.
- Keine Beispiel- oder Kundendaten in Migrationen oder Seed-Dateien.
- Baue noch keine Rechnungen, Aufgaben oder Zeiterfassung. Das sind eigene Module.

Typische Falle: Zwei getrennte Listen für Firmen und Kunden zeigen am Ende dieselben Zeilen. Bei mir war das anfangs so, ich habe sie zusammengelegt. Besser von Anfang an: eine Tabelle companies mit dem Feld lifecycle. Der Prompt oben macht genau das.

Schritt 3: Arbeit.

Aufgaben, Termine und Zeit gehören in ein System. Alles hängt am Kunden aus Schritt 2, dann weißt du, wofür die Stunden angefallen sind.

Aufgaben
Priorität, Deadline, optional ein Kunde. Überfälliges steht oben.
Termine
Dein Google-Kalender wird eingelesen (nur lesen), Termine werden über die E-Mail-Domain einer Firma zugeordnet.
Zeiterfassung
Start/Stopp-Timer mit Kunde, dazu manueller Nachtrag. Summe pro Kunde und Woche.
Eine Ansicht
Auf der Firmenseite stehen Aufgaben, Termine und Zeit als Tabs.
Tabellen
todos (Titel, Priorität, Deadline, Firma), time_entries (Firma, Start, Ende, Notiz), calendar_events (Google-ID, Start, Ende, Titel, Firma), email_domains (Domain, Firma, Status), google_auth (Token, nur serverseitig lesbar).
Geklappt, wenn
Aufgabe, Termin und Zeiteintrag hängen am selben Kunden.
PROMPT-3-ARBEIT.md
KONTEXT
Fundament und Kunden-Modul stehen. Ich baue jetzt das Arbeits-Modul: Aufgaben, Termine aus meinem Google-Kalender (nur lesen) und Zeiterfassung, alles am selben Kunden. Halte dich an CLAUDE.md.

ZIEL
Eine Aufgabe, ein Termin und ein Zeiteintrag können am selben Kunden hängen. Ich sehe, was heute fällig ist und wofür meine Stunden angefallen sind.

DATENBANK (supabase/migrations/003_work.sql)
- todos: title (not null), completed boolean default false, completed_at, priority text default 'medium' mit check (priority in ('low','medium','high')), deadline date, company_id (nullable, on delete set null), note, created_at, updated_at, user_id.
- time_entries: company_id (nullable), todo_id (nullable), started_at timestamptz not null, ended_at timestamptz (null = läuft gerade), note, created_at, user_id. Es darf pro Nutzer höchstens einen laufenden Eintrag geben (partieller Unique-Index auf user_id where ended_at is null).
- google_auth: user_id primary key, refresh_token text not null, access_token, expires_at, status text default 'ok' mit check (status in ('ok','invalid')), last_error, updated_at. RLS aktiv, aber KEINE Policy für den Client: die Tabelle ist nur über den Service-Role-Client lesbar.
- calendar_events: google_id text not null, company_id (nullable, on delete set null), starts_at timestamptz not null, ends_at timestamptz not null, all_day boolean default false, title, location, conference_url, updated_at, user_id. Unique auf (user_id, google_id).
- email_domains: domain text not null, company_id (nullable), status text mit check (status in ('pending','mapped','ignored')), user_id, Unique auf (user_id, domain). Damit ordne ich Termine über die E-Mail-Domain der Teilnehmer einer Firma zu.

KALENDER-SYNC
- OAuth mit Google, nur der Scope calendar.readonly. Seiten /settings/calendar (verbinden, Status, trennen) und der Callback unter /api/google/callback.
- Eine Funktion syncCalendar() in src/lib/calendar/sync.ts holt die Termine der nächsten 30 Tage und der letzten 7 Tage, schreibt sie per Upsert auf (user_id, google_id) und löscht Termine, die in Google nicht mehr existieren.
- Aufgerufen wird sie von einer geschützten Route /api/cron/calendar-sync: CRON_SECRET muss gesetzt sein, der Header Authorization muss genau "Bearer " plus CRON_SECRET sein, sonst 500 beziehungsweise 401. Das saubere Cron-Muster kommt im Automations-Modul, bis dahin baust du diese Prüfung direkt in die Route. Der Cron-Eintrag in vercel.json kommt ebenfalls erst dort, hier reicht ein Button "Jetzt synchronisieren".
- Ist das Token ungültig, setze google_auth.status auf 'invalid' und zeige in den Einstellungen einen Hinweis, statt die Seite zu brechen.

SEITEN UND ROUTEN
- /todos: offene Aufgaben nach Deadline sortiert, überfällige oben, Schnell-Eingabe, Filter nach Kunde.
- /work/calendar: Wochenansicht der Termine, Termin trägt die zugeordnete Firma.
- /time: Start/Stopp-Timer mit Kunden-Auswahl, Liste der Einträge, Summe pro Kunde und Woche. Manueller Nachtrag möglich.
- Alle Routen tragen sich in nav.ts unter "Arbeit" ein. An der Firmenseite (/clients/[id]) kommen Aufgaben, Termine und Zeit als weitere Tabs dazu.

AKZEPTANZKRITERIEN
- Timer starten, stoppen, Dauer stimmt. Ein zweiter Start bei laufendem Timer wird abgefangen.
- "Heute" und "überfällig" werden nach Europe/Berlin berechnet, nicht nach UTC. Schreibe dazu eine kleine Funktion berlinToday() in src/lib/date.ts samt Test (zwischen 00:00 und 02:00 Berliner Zeit muss der richtige Tag herauskommen).
- Ganztägige Termine landen auf dem richtigen Tag.
- Der Client (Browser) kann google_auth nicht lesen. Prüfe das mit einer Abfrage als eingeloggter Nutzer.
- `npm run build` und `npm run lint` laufen durch.

LEITPLANKEN
- Frag nach, bevor du in der Google Cloud Console oder sonstwo außerhalb des Repos etwas anlegen willst. Schreib mir stattdessen die Klickfolge (OAuth-Zustimmungsbildschirm, OAuth-Client, Redirect-URI), ich mache das selbst.
- Frag nach, bevor du einen anderen Scope als calendar.readonly verwendest.
- Frag nach, bevor du Termine in Google schreibst oder löschst. Der Sync liest nur.
- Keine Tokens, Client-Secrets oder Termindaten in Logs, Tests oder Beispieldateien.
- Kein Rechnungs- oder Geldthema in diesem Modul.

Typische Falle: Zeitzonen. Auf Vercel läuft Code in UTC. Wer „heute“ mit new Date().toISOString().split('T')[0] bestimmt, bekommt zwischen 0 und 2 Uhr Berliner Zeit den Vortag. Das ist mir in der Praxis begegnet. Speichere Zeitpunkte als timestamptz und berechne den Tag in Europe/Berlin in einer einzigen Funktion. Zweite Falle: Google-Tokens. Die Tabelle google_auth darf für den Browser nicht lesbar sein.

Schritt 4: Geld.

Rechnungen und Fixkosten, damit du nicht in einer Tabelle nachschauen musst, was offen ist und was regelmäßig abgeht.

Rechnungen
Erfassen, Positionen, Fälligkeit, Status von Entwurf bis bezahlt.
Überfällig
Wird aus Fälligkeit und Status berechnet, nicht gespeichert.
Fixkosten
Posten mit Rhythmus (monatlich, vierteljährlich, jährlich), umgerechnet auf die Monatssumme.
Ausgaben
Einzelne Ausgaben mit Kategorie.
Übersicht
Vier Zahlen auf einen Blick: offen, überfällig, Fixkosten pro Monat, Ausgaben im Monat.
Tabellen
invoices (Firma, Nummer, Datum, Fälligkeit, Status, Netto, Steuersatz), invoice_items (Position, Menge, Einzelpreis), fixed_costs (Name, Betrag, Rhythmus, Beginn, Ende), expenses (Beschreibung, Betrag, Datum).
Geklappt, wenn
Offene Rechnungen und Fixkosten stehen an einer Stelle.
PROMPT-4-GELD.md
KONTEXT
Fundament, Kunden und Arbeit stehen. Ich baue jetzt das Geld-Modul: Rechnungen verwalten und Fixkosten im Blick behalten. Halte dich an CLAUDE.md, besonders: Geld ist immer numeric(10,2).

ZIEL
Ich sehe an einer Stelle, welche Rechnungen offen oder überfällig sind und was monatlich an Fixkosten abgeht. Das Modul verwaltet Rechnungen (erfassen, Status pflegen), es ersetzt keine Buchhaltungssoftware und erzeugt keine rechtsgültigen Rechnungs-PDFs.

DATENBANK (supabase/migrations/004_finance.sql)
- invoices: company_id (not null, on delete restrict), deal_id (nullable), number text not null, issued_on date not null, due_on date not null, status text not null default 'draft' mit check (status in ('draft','sent','paid','void')), net_total numeric(10,2) not null check (net_total >= 0), tax_rate numeric(4,2) not null default 19.00, paid_on date, note, created_at, updated_at, user_id. Unique auf (user_id, number).
- invoice_items: invoice_id (on delete cascade), position integer, description text not null, quantity numeric(10,2) not null, unit_price numeric(10,2) not null, user_id.
- fixed_costs: name text not null, category text default 'other', amount numeric(10,2) not null check (amount >= 0), cadence text not null default 'monthly' mit check (cadence in ('monthly','quarterly','yearly')), starts_on date not null default current_date, ends_on date, note, created_at, updated_at, user_id.
- expenses: description text not null, category text default 'other', amount numeric(10,2) not null check (amount >= 0), spent_on date not null default current_date, note, created_at, user_id.
- Rechnungsnummern: eine Postgres-Funktion next_invoice_number(p_user uuid) vergibt sie atomar und lückenlos im Format JJJJ-NNN (Jahr plus laufende Nummer pro Jahr). Sie läuft serverseitig, nie im Frontend.
RLS und Policy "Users manage own <tabelle>" auf allen Tabellen.

SEITEN UND ROUTEN
- /finance: Übersicht mit vier Zahlen: offene Rechnungen (Anzahl), überfällige Rechnungen (Anzahl), Fixkosten pro Monat (Summe, Quartals- und Jahresposten auf den Monat umgerechnet), Ausgaben im laufenden Monat.
- /finance/invoices: Liste mit Statusfilter, Rechnung anlegen (Firma wählen, Positionen, Fälligkeit), Status per Button ändern (versendet, bezahlt). "Überfällig" ist kein gespeicherter Status, sondern wird aus due_on und status 'sent' berechnet.
- /finance/costs: Fixkosten und Ausgaben verwalten.
- An der Firmenseite (/clients/[id]) kommt der Tab "Rechnungen" dazu. Wenn eine Rechnung auf 'sent' springt, wird im Verlauf der Firma eine Aktivität "Rechnung versendet" angelegt (activities.kind um 'invoice_sent' erweitern, per neuer Migration 005_activity_invoice.sql, die den check-Constraint ersetzt).
- Routen tragen sich in nav.ts unter "Finanzen" ein.

AKZEPTANZKRITERIEN
- Rechnungssumme wird aus den Positionen berechnet und auf zwei Stellen gerundet gespeichert (net_total), die Brutto-Summe wird nur angezeigt.
- Zwei Rechnungen parallel anlegen erzeugt nie dieselbe Nummer.
- Eine Rechnung mit Status 'sent' oder 'paid' lässt sich nicht mehr löschen, nur auf 'void' setzen (stornieren).
- Eine überfällige Rechnung erscheint in der Übersicht und rot in der Liste.
- Die Monatssumme der Fixkosten ist korrekt für monthly, quarterly und yearly und ignoriert Posten mit abgelaufenem ends_on.
- `npm run build` und `npm run lint` laufen durch.

LEITPLANKEN
- Frag nach, bevor du PDF-Erzeugung, Steuerlogik über den einfachen Steuersatz hinaus oder DATEV-Export einbaust. Das ist bewusst nicht Teil dieses Moduls.
- Frag nach, bevor du bestehende Migrationen änderst. Der Constraint der activities-Tabelle wird über eine neue Migration ersetzt.
- Keine echten Beträge, Kunden oder Rechnungsnummern in Tests oder Seed-Daten. Nimm offensichtliche Platzhalter.
- Kein float, kein toFixed als Speicherformat. Rechne in der Datenbank oder mit ganzen Cent-Werten und runde einmal am Ende.

Typische Falle: Geld als Fließkommazahl: 0,1 plus 0,2 ist dort nicht 0,3. Speichere Beträge als numeric(10,2) und runde einmal am Ende. Zweite Falle: Rechnungsnummern. Vergibst du sie im Frontend, entstehen Dubletten und Lücken. Der Prompt legt eine Datenbankfunktion an. Dritte Falle: Dieses Modul verwaltet Rechnungen, es ersetzt keine Buchhaltung. Pflichtangaben, Aufbewahrung und Rechnungs-PDFs klärst du mit deinem Steuerberater oder einer geprüften Software.

Schritt 5: Content.

Idee, Hook, Skript, Stimme, fertiges Video: jede Stufe ist ein Status, den ein Eintrag durchläuft. Du entscheidest an den Übergängen, ob es weitergeht.

Board
Eine Spalte pro Stufe, jede Idee eine Karte.
Hooks
Die KI erzeugt Varianten mit Mechanismus, du wählst einen aus.
Skript
Neue Fassungen überschreiben nie, jede ist eine eigene Version.
Stimme und Video
Erst nur Upload und Status. Der Render läuft später in einem eigenen Worker.
Kostenkontrolle
Jeder KI-Aufruf wird protokolliert, ein Tageslimit greift.
Tabellen
content_items (Titel, Thema, Status), content_hooks (Idee, Mechanismus, Text, gewählt), content_scripts (Idee, Version, Text), content_voiceovers (Skript, Dateipfad, Status), content_renders (Idee, Status, Videopfad), ai_runs (Schritt, Modell, Token).
Geklappt, wenn
Eine Idee wandert als ein Eintrag bis zum Video.
PROMPT-5-CONTENT.md
KONTEXT
Die Module Fundament, Kunden, Arbeit und Geld stehen. Ich baue jetzt das Content-Modul: eine Produktions-Pipeline von der Idee bis zum fertigen Video. Halte dich an CLAUDE.md.

ZIEL
Jede Idee durchläuft feste Stufen (Idee, Hook, Skript, Stimme, Video, veröffentlicht). Ich sehe auf einem Board, wo jede Idee steht, und die KI-Schritte laufen nur auf meinen Klick.

DATENBANK (supabase/migrations/005_content.sql)
- content_items: title text not null, topic text, source_url text, status text not null default 'idea' mit check (status in ('idea','hook','script','voice','video','published')), published_url, created_at, updated_at, user_id.
- content_hooks: item_id (on delete cascade), generation integer, mechanism text (z. B. 'benefit','curiosity','contrarian','problem'), text text not null, selected boolean default false, created_at, user_id. Pro Idee darf höchstens ein Hook selected sein (partieller Unique-Index).
- content_scripts: item_id (on delete cascade), version integer not null, body text not null, created_at, user_id. Unique auf (item_id, version). Neue Fassungen überschreiben nie, sie legen eine neue Version an.
- content_voiceovers: script_id (on delete cascade), provider text, audio_path text (Pfad im privaten Supabase-Storage-Bucket "voiceovers"), duration_seconds numeric(6,1), status text default 'pending' mit check (status in ('pending','ready','failed')), error, created_at, user_id.
- content_renders: item_id (on delete cascade), status text default 'queued' mit check (status in ('queued','rendering','done','failed')), video_path, error, created_at, finished_at, user_id.
- ai_runs: item_id (nullable), step text mit check (step in ('hooks','script')), model text not null, input_tokens integer default 0, output_tokens integer default 0, created_at, user_id. Jeder KI-Aufruf schreibt eine Zeile.
RLS und Policy auf allen Tabellen. Storage-Bucket "voiceovers" privat, mit Policy, dass nur der eigene Nutzer lesen und schreiben darf.

SEITEN UND ROUTEN
- /content/pipeline: Board mit einer Spalte pro Status, Karten sind Ideen. Neue Idee per Schnell-Eingabe.
- /content/[id]: Detailseite einer Idee, aufgebaut als Schritte untereinander:
  1. Idee: Titel, Thema, Quelle.
  2. Hooks: Button "Hooks erzeugen" ruft die Anthropic-API (Modell aus settings.ai_model) und legt fünf Varianten an, jede mit Mechanismus. Ich wähle einen aus.
  3. Skript: Button "Skript erzeugen" nimmt den gewählten Hook und schreibt eine neue Version. Das Skript ist von Hand editierbar.
  4. Stimme: Platz für die Audiodatei. Erst nur Upload in den Bucket und Status. Die automatische Sprachausgabe über einen TTS-Anbieter ist ein späterer Schritt.
  5. Video: Button "Render anfordern" legt nur eine Zeile in content_renders mit status 'queued' an. Der Render selbst läuft außerhalb dieser App (eigener Worker), das ist hier nicht Teil der Aufgabe.
- Routen tragen sich in nav.ts unter "Content" ein.

KI-REGELN
- Die KI-Aufrufe laufen ausschließlich in Server Actions oder Route Handlern, mit ANTHROPIC_API_KEY aus der Umgebung.
- Standardmodell kommt aus settings.ai_model. Kein Aufruf beim Laden einer Seite, nur auf Klick.
- Pro Nutzer und Tag höchstens 30 KI-Aufrufe (Zähler über ai_runs). Bei Überschreitung klare Fehlermeldung statt Aufruf.
- Antworten der KI werden als JSON mit festem Schema angefordert und validiert, bevor sie gespeichert werden.

AKZEPTANZKRITERIEN
- Eine Idee lässt sich von 'idea' bis 'video' durchschieben, die Karte wandert auf dem Board mit.
- Jeder KI-Klick erzeugt genau eine Zeile in ai_runs mit Token-Zahlen.
- Ein zweiter Klick auf "Hooks erzeugen" überschreibt keine alten Hooks, sondern legt eine neue generation an.
- Das Tageslimit greift (Test mit gesenktem Limit).
- Der Storage-Bucket ist nicht öffentlich lesbar.
- `npm run build` und `npm run lint` laufen durch.

LEITPLANKEN
- Frag nach, bevor du ein Video-, Audio- oder Rendering-Paket installierst.
- Frag nach, bevor du einen kostenpflichtigen externen Dienst (TTS, Video) anbindest. Nenne mir vorher die erwartbaren Kosten pro Aufruf.
- Frag nach, bevor du das Tageslimit oder das Standardmodell änderst.
- Keine echten Skripte, Kundeninhalte oder Keys in Tests und Beispieldateien.

Typische Falle: KI-Kosten laufen unbemerkt auf, wenn jede Seitenansicht oder jeder Klick einen Aufruf auslöst. Der Prompt erzwingt: KI nur auf Klick, jeder Aufruf in ai_runs, ein Tageslimit, kleines Standardmodell. Zweite Falle: zu früh alles automatisieren. Baue erst die Stufen und Status, den Render und die Stimme hängst du später dran.

Schritt 6: Automationen.

Alles, was sich wiederholt, wird ein geplanter Job. Bei mir laufen 16 Crons, nachts und früh am Morgen. Hier legst du das Muster an und baust den ersten Job, den KI-Tagesplan.

Vercel Cron
Eine vercel.json ruft Routen unter /api/cron/... zu festen Zeiten auf (UTC).
Geschützt
Jede Route prüft ein Secret und lehnt alles andere ab.
Protokoll
Jeder Lauf steht mit Status und Fehlertext in der Datenbank.
Idempotent
Ein Job darf doppelt laufen, ohne doppelt zu wirken.
Tagesplan
Morgens schreibt die KI aus deinen Aufgaben, Terminen und offenen Punkten einen Plan und legt ihn ab.
Tabellen
daily_plan_cache (Datum, Text, Modell), automation_runs (Job, Start, Ende, Status, Fehler), automation_rules (Typ, an/aus, Schwelle, Priorität), automation_tasks (Regel, Anlass, Aufgabe; eindeutig pro Anlass).
Geklappt, wenn
Ein Job läuft ohne dich und hinterlässt morgens ein Ergebnis.
PROMPT-6-AUTOMATIONEN.md
KONTEXT
Die Module Kunden, Arbeit, Geld und Content stehen. Ich baue jetzt die Automationen: geplante Jobs auf Vercel Cron, die nachts laufen und mir morgens Arbeit abnehmen. Beispiel dafür ist ein KI-Tagesplan. Halte dich an CLAUDE.md.

ZIEL
Ein sauberes Muster für Cron-Routen, das jeder spätere Job übernimmt: geschützt, idempotent, protokolliert, in Berliner Zeit gedacht. Darauf zwei konkrete Jobs: die Kalender-Synchronisation (Route aus Modul Arbeit) und der Tagesplan.

DATENBANK (supabase/migrations/006_automations.sql)
- daily_plan_cache: plan_date date not null, content text not null, model text not null, created_at, user_id. Unique auf (user_id, plan_date).
- automation_runs: job text not null, started_at timestamptz default now(), finished_at, status text default 'ok' mit check (status in ('ok','error')), summary text, error text, user_id.
- automation_rules: type text not null, enabled boolean default true, threshold_days integer, priority text default 'medium', due_in_days integer, config jsonb not null default '{}', user_id.
- automation_tasks: rule_type text not null, subject_key text not null, todo_id (nullable, on delete set null), created_at, resolved_at, user_id. Unique auf (user_id, rule_type, subject_key). Das ist die Idempotenz: eine Regel legt für denselben Anlass nie zwei Aufgaben an.
RLS auf allen Tabellen mit Policy für den eigenen Nutzer.

CRON-MUSTER (src/lib/cron.ts)
Baue einen Wrapper withCron(jobName, handler) für Route Handler unter src/app/api/cron/<job>/route.ts:
1. Ist CRON_SECRET nicht gesetzt, antwortet die Route 500 mit klarer Meldung. Ohne diese Prüfung passt "Bearer undefined" auf einen fehlenden Wert.
2. Der Header Authorization muss genau "Bearer " plus CRON_SECRET sein, sonst 401. Vergleiche mit einem zeitkonstanten Vergleich.
3. Der Handler bekommt den Admin-Client (Service-Role, server-only) und die Berliner Tagesangabe berlinToday().
4. Start und Ende landen in automation_runs (Status, kurze Zusammenfassung, Fehlertext). Eine Exception führt zu Status 'error' und Antwort 500, nie zu stillem Erfolg.
5. Export const maxDuration passend zum Job (Standard 60 Sekunden).
Cron-Routen sind GET-Routen, weil Vercel Cron per GET aufruft.

VERCEL.JSON
Lege vercel.json mit diesen Crons an. Die Zeiten sind UTC, Berlin ist im Sommer UTC+2, im Winter UTC+1:
- /api/cron/calendar-sync, "30 4 * * *"
- /api/cron/daily-plan, "0 5 * * *"
Staffle künftige Jobs auf verschiedene Minuten, damit nicht alle zur selben Zeit starten.

JOB 1: calendar-sync
Ruft syncCalendar() aus Modul Arbeit auf, schreibt die Anzahl neuer, geänderter und gelöschter Termine in die Zusammenfassung.

JOB 2: daily-plan (KI-Tagesplan)
- Wenn für berlinToday() schon eine Zeile in daily_plan_cache existiert, antworte mit { status: 'already_generated' } und rufe die KI nicht erneut auf.
- Lade: offene Aufgaben (maximal 20, nach Deadline sortiert, nur Titel, Deadline und Priorität), Termine von heute, Anzahl überfälliger Rechnungen, Deals mit fälligem nächsten Schritt (nur Titel und Datum).
- Schicke das an die Anthropic-API (Modell aus settings.ai_model) mit diesem System-Prompt: "Du bist mein Planungsassistent. Schreibe einen Tagesplan auf Deutsch, höchstens acht Stichpunkte, nach Wichtigkeit sortiert, Termine mit Uhrzeit zuerst. Erfinde nichts, was nicht in den Daten steht."
- Speichere die Antwort in daily_plan_cache. Schlägt der KI-Aufruf fehl, speichere nichts, setze den Run auf 'error' und antworte 500, damit der nächste Aufruf es erneut versucht.

OBERFLÄCHE
- /automations: Liste der Jobs mit letztem Lauf (Zeit, Status, Zusammenfassung) aus automation_runs, dazu Button "Jetzt ausführen" pro Job (ruft die Route serverseitig mit dem Secret auf, nie aus dem Browser mit dem Secret).
- Der Bereich gehört nicht in die Haupt-Navigation, sondern als Eintrag unter Einstellungen in nav.ts.

AKZEPTANZKRITERIEN
- Aufruf ohne Header: 401. Aufruf mit falschem Header: 401. CRON_SECRET nicht gesetzt: 500. Aufruf mit richtigem Header: 200 und eine Zeile in automation_runs.
- Zwei Aufrufe von daily-plan am selben Tag erzeugen genau eine Zeile in daily_plan_cache und genau einen KI-Aufruf.
- Ein Aufruf um 00:30 Berliner Zeit rechnet mit dem Berliner Datum, nicht mit dem UTC-Vortag (Test für berlinToday()).
- CRON_SECRET und ANTHROPIC_API_KEY tauchen in keinem Log und keiner Antwort auf.
- `npm run build` und `npm run lint` laufen durch.

LEITPLANKEN
- Frag nach, bevor du die Cron-Zeiten änderst oder weitere Jobs anlegst. Ich prüfe vorher, wie viele Crons und welche Intervalle mein Vercel-Tarif erlaubt.
- Frag nach, bevor du Daten löschst (Retention-Jobs sind ein eigener Auftrag).
- Frag nach, bevor du E-Mails, Nachrichten oder irgendetwas nach außen verschickst. Diese Jobs schreiben nur in die eigene Datenbank.
- Kein Job ruft die KI in einer Schleife über viele Datensätze, ohne dass ich vorher ein Limit festgelegt habe.

Typische Falle: Das Secret. Ist CRON_SECRET nicht gesetzt, passt der Vergleich mit „Bearer undefined“, und die Route steht offen im Netz. Prüfe zuerst, ob das Secret existiert. Zweite Falle: UTC. Der Cron um 0 5 * * * läuft um 05:00 UTC, also 07:00 Uhr im Sommer und 06:00 Uhr im Winter in Berlin. Dritte Falle: Wiederholungen. Ohne Eindeutigkeit pro Anlass legt ein zweiter Lauf alles doppelt an, deshalb die Unique-Constraints. Vierte Falle: Tarif. Wie viele Crons und welche Intervalle erlaubt sind, hängt von deinem Vercel-Tarif ab, schau vorher in die Vercel-Dokumentation.

Schritt 7: Cockpit.

Die Startseite „Heute“. Alles, was in den Modulen davor liegt, läuft hier zusammen. Ab hier öffnest du morgens nur noch dieses eine Fenster.

Tagesplan
Der KI-Plan aus Schritt 6 steht ganz oben.
Aufgaben und Termine
Heute fällig und überfällig, dazu die Termine des Tages.
Pipeline und Geld
Deals mit fälligem nächsten Schritt, offene und überfällige Rechnungen als Zahl.
Automationen
Eine kleine Ampel zeigt, ob die Jobs der Nacht gelaufen sind.
Schnell-Eingabe
Eine Aufgabe anlegen, ohne die Seite zu verlassen.
Tabellen
Keine neuen Tabellen. Das Cockpit liest nur, in einer Datei (src/lib/today.ts), die alles parallel lädt.
Geklappt, wenn
Morgens zeigt eine Seite, was heute zählt.
PROMPT-7-COCKPIT.md
KONTEXT
Alle Module stehen, die Automationen laufen. Ich baue jetzt das Cockpit: die Startseite "Heute", die jeden Morgen zeigt, was wichtig ist. Es gibt keine neuen Tabellen, das Cockpit liest nur. Halte dich an CLAUDE.md.

ZIEL
Ich öffne morgens ein Fenster und weiß in zehn Sekunden, was heute ansteht. Die Seite bleibt bewusst klein.

DATENZUGRIFF
Eine einzige Datei src/lib/today.ts lädt alles parallel (Promise.all) mit dem Server-Client des eingeloggten Nutzers:
- Tagesplan: Zeile aus daily_plan_cache für berlinToday(), falls vorhanden.
- Aufgaben: überfällige und heute fällige, offen, nach Priorität sortiert, höchstens fünf.
- Termine: heute, nach Startzeit, höchstens fünf.
- Geld: Anzahl offener und überfälliger Rechnungen (nur Zahlen, keine Beträge).
- Pipeline: Deals, deren next_step_on heute oder früher ist, höchstens fünf.
- Automationen: Status des letzten Laufs je Job (ok oder Fehler) als kleine Ampel.

SEITE /today
Fünf Blöcke in dieser Reihenfolge, jeder mit Überschrift, höchstens fünf Zeilen und einem Link "Alle ansehen" in das jeweilige Modul:
1. Tagesplan (oben, größte Karte).
2. Aufgaben.
3. Termine.
4. Pipeline und Geld nebeneinander (zwei kleine Karten).
5. Automationen (nur die Ampel, bei Fehler mit Link auf /automations).
Dazu oben ein Schnell-Eingabefeld "Neue Aufgabe" (Enter legt sie an, ohne die Seite zu verlassen).
Am Handy stehen die Blöcke untereinander in derselben Reihenfolge.

ZUSTÄNDE
- Gibt es noch keinen Tagesplan (Cron noch nicht gelaufen oder fehlgeschlagen), zeigt der Block "Noch kein Tagesplan für heute." und einen Button "Jetzt erzeugen". Der Button ruft dieselbe Logik wie der Cron auf und ist über das Tageslimit aus dem Content-Modul begrenzt.
- Ist ein Block leer, zeigt er einen kurzen leeren Zustand ("Nichts überfällig."), nicht einfach nichts.
- Schlägt eine einzelne Abfrage fehl, zeigt nur dieser Block eine Fehlermeldung. Die Seite bleibt benutzbar (Promise.allSettled).

AKZEPTANZKRITERIEN
- Die Seite lädt mit Testdaten in unter zwei Sekunden und braucht pro Aufruf höchstens sechs Datenbankabfragen.
- Heute und überfällig richten sich nach Europe/Berlin.
- Mit einer leeren Datenbank zeigt jeder Block einen sinnvollen leeren Zustand.
- Bricht ein Block, bleiben die anderen sichtbar.
- Am Handy (390 Pixel Breite) gibt es kein horizontales Scrollen.
- `npm run build` und `npm run lint` laufen durch. Starte `npm run dev` und prüfe die Seite im Browser, bevor du "fertig" sagst.

LEITPLANKEN
- Frag nach, bevor du mehr als fünf Blöcke einbaust oder einen Block auf mehr als fünf Zeilen vergrößerst. Ein überladenes Cockpit nutzt man nach zwei Wochen nicht mehr.
- Frag nach, bevor du Diagramme oder Chart-Pakete installierst.
- Keine Beträge, Kundennamen oder echten Daten in Tests und Screenshots.
- Baue nichts, was Daten ändert, außer dem Schnell-Eingabefeld für Aufgaben und dem Button für den Tagesplan.

Typische Falle: Ein überladenes Cockpit nutzt du nach zwei Wochen nicht mehr. Der Prompt begrenzt auf fünf Blöcke mit je höchstens fünf Zeilen und leeren Zuständen. Zweite Falle: Bricht eine Abfrage, darf nicht die ganze Seite weiß bleiben. Deshalb lädt jeder Block für sich (Promise.allSettled) und zeigt im Fehlerfall nur seinen eigenen Hinweis.

Reihenfolge und Zeitplan.

Bau die Module in dieser Reihenfolge, jedes Modul braucht das davor.

Pro Modul
Als grobe Einschätzung, nicht als Messwert: rechne mit einem Wochenende pro Modul, die Automationen eher mit zwei.
Eine Woche testen
Benutze jedes Modul eine Woche im Alltag, bevor du das nächste baust. Was nervt, korrigierst du, solange das Modul klein ist.
Cockpit früh
Die leere Startseite „Heute“ entsteht schon in Schritt 1. Du füllst sie mit jedem Modul ein Stück, das hält dich bei der Stange.
Cron vorziehen
Der Kalender-Sync aus Schritt 3 braucht das Cron-Muster aus Schritt 6. Wenn dir der Sync wichtig ist, ziehe den Cron-Prompt nach Schritt 3 vor.
Vor jedem Prompt
Lass dir das Datenmodell von Claude Code vorschlagen und lies es. Spiel Migrationen selbst ein und prüfe die Zugriffsregeln mit einem zweiten Test-Account.

Die Zahlen aus dem Reel sind belegt: 867 Commits zwischen dem 21.05. und dem 03.10.2026 und 16 Crons. Du brauchst nicht so viele, der Bauplan ist der Weg dorthin.

PROMPTS

Alles, was du brauchst.

Die Checkliste zum Abhaken und die Vorlage für die CLAUDE.md deines Projekts.

FIRMEN-OS-BAUPLAN.md
FIRMEN-OS: BAUPLAN IN 7 SCHRITTEN

Jedes Modul steckst du einzeln dazu. Hake erst ab, wenn du es eine Woche im Alltag benutzt hast.

[ ] 1. Fundament: Claude Code + Supabase, Login, Navigation, CLAUDE.md
[ ] 2. Kunden: Firmen, Kontakte, Deals, Pipeline (wer steht wo)
[ ] 3. Arbeit: Aufgaben, Termine (Kalender-Sync), Zeiterfassung in einem System
[ ] 4. Geld: Rechnungen verwalten, Fixkosten
[ ] 5. Content: Idee, Hook, Skript, Stimme, fertiges Video
[ ] 6. Automationen: Vercel Cron, Protokoll, Tagesplan morgens per KI
[ ] 7. Cockpit: Startseite "Heute"

Vor jedem Modul:
[ ] Datenmodell von Claude vorschlagen lassen und lesen
[ ] Migration prüfen, dann selbst einspielen
[ ] RLS-Test: sieht ein zweiter Account nichts?

Nach jedem Modul:
[ ] npm run build und npm run lint laufen durch
[ ] Im Browser einmal alles durchklicken
[ ] Auf Vercel deployen, Umgebungsvariablen prüfen
[ ] Eine Woche benutzen, notieren, was nervt, erst dann das nächste Modul

WORAUF ES ANKOMMT

Darauf musst du achten.

  • Kein fertiges Tool

    Ein Firmen-OS ist ein Dashboard, das du dir selbst baust. Mein System ist mit Claude Code gebaut: Next.js, Supabase und Vercel, 867 Commits zwischen dem 21.05. und dem 03.10.2026.

  • Modul für Modul

    Du musst nicht alles auf einmal bauen. Jeder der sieben Schritte funktioniert allein. Fang mit dem an, der dir heute am meisten Tool-Chaos abnimmt.

Dein Firmen-OS, Modul für Modul.

Mit Schritt 1 anfangen: Prompt kopieren, in Claude Code einfügen, Fundament bauen.

Du kommst vom Reel? Das hier ist der Bauplan, Stichwort OS. Mehr Tipps auf @till.schae

Weiterlesen

Alle Anleitungen zu Coding und Agenten an einem Ort, von Skills über Connectors bis zu den Repos.

Coding & Agenten