~/jonasfink.dev
← cd ..

~/projects/amplify · 2026 · Full-Stack Developer

Amplify – Gemeinsame Projektboards

Next.jsReactTypeScriptTailwind v4PostgresAuth.jsMCPCloudflare R2Docker

Ein gemeinsames Projektboard für ein kleines Team: Aufgaben, Archiv, Dateien und Einladungen an einem Ort, den Menschen und Agenten (Cursor, Claude, ChatGPT) gleichermaßen lesen und schreiben. Die Mitgliedschaft bleibt die Grenze — ein Agent handelt als der angemeldete Nutzer und sieht nur dessen Projekte.

Amplify – Gemeinsame Projektboards — screenshotAmplify – Gemeinsame Projektboards — screenshotAmplify – Gemeinsame Projektboards — screenshotAmplify – Gemeinsame Projektboards — screenshot

## Situation

In einem kleinen Team verteilt sich die Arbeit an einem Projekt auf das Board, den Chat und die Sitzung eines Assistenten. Status, Zuständigkeit, Checklisten und Dateien leben an der einen Stelle, die nächste Anweisung an Cursor, Claude oder ChatGPT an einer anderen, und nichts davon weiß, wer überhaupt Mitglied des Projekts ist. Ein Agent, der eine Aufgabe anlegen soll, braucht dafür entweder eine zweite Quelle der Wahrheit oder einen Zugang, der weiter reicht als das Konto der Person, die ihn beauftragt. Es gab keine einzige Oberfläche, auf der ein Mensch und ein Agent dieselben Projekte lesen und schreiben, wobei der Agent auf die Projekte des angemeldeten Nutzers beschränkt bleibt.

## Aufgabe

Ziel war ein Board für ein kleines Team: Projekte mit den Status Offen, In Arbeit und Erledigt, ein Archiv, Priorität, Zuständige, Subtasks, Einladungslinks und Dateien. Die Nicht-Ziele bestimmten die Aufgabe fast so sehr wie die Ziele selbst: kein zweites Jira mit Sprints, Schätzungen und eigenen Workflows, und keine allgemeine Agenten-Plattform, die beliebige Werkzeuge anbindet. Technisch bestand die Aufgabe darin, eine Schreibstrecke zu entwerfen, die die Oberfläche und die Agent-Tools gemeinsam benutzen, die Mitgliedschaft in genau dieser Strecke prüft statt in jedem Aufrufer einzeln, und einen Agent-Login bereitzustellen, den Connectoren zu Ende bringen können. Connectoren erreichen weder localhost noch einen anderen Port als 443, sodass die Erreichbarkeit Teil der Aufgabe war und nicht ein nachgelagerter Deploy-Schritt.

## Umsetzung

Die Anwendung ist eine Next.js-16-App (App Router, React 19, TypeScript, Tailwind v4) mit Postgres über den Treiber postgres auf Supabase, ohne ORM, mit Zod an den Schreibgrenzen und Auth.js-Credentials, deren Passwörter mit bcrypt gehasht werden. Schreibzugriffe auf Aufgaben laufen über ein einziges Modul, task-writes: eine Transaktion prüft zuerst project_members und legt erst dann an oder ändert. Server Actions der Oberfläche und die sechs MCP-Tools (applify_list_projects, applify_list_tasks, applify_create_task, applify_create_subtask, applify_update_task, applify_update_subtask) rufen dasselbe Modul, sodass ein vom Agenten angelegter Task dieselben Regeln durchläuft wie einer aus dem Formular. Die Agent-Authentifizierung hat bewusst einen einzigen Prüfer. Ein Bearer-Key mit dem Präfix apl_ wird als SHA-256-Hash gespeichert, dem Nutzer nur einmal angezeigt und auf zehn Schlüssel pro Konto begrenzt. Claude und ChatGPT kommen über OAuth mit Authorization Code und PKCE S256; als Client sind nur claude.ai und chatgpt.com zugelassen, der Authorization-Code wird beim Tausch gelöscht, und der Access-Token ist selbst eine Zeile in api_keys. Dadurch lernt der MCP-Handler keinen zweiten Auth-Pfad. Einladungslinks speichern nur den Hash des Tokens, mit Ablauf und Widerruf. Dateien liegen in einem privaten R2-Bucket; der Upload läuft über eine zeitlich begrenzte presigned PUT-URL, die Zeile bleibt pending, bis Größe und Typ des Objekts bestätigt sind, bei 50 MB und einer Allowlist der Dateitypen. Das Archiv ist ein Status und keine zweite Tabelle: das Board zeigt nur Offen, In Arbeit und Erledigt, die Summary-Route zählt archivierte Aufgaben weiter, damit erledigte Geschichte beim Verlassen des Boards nicht verschwindet. Eine optionale Gemini-Nacherzählung formuliert ausschließlich diese deterministische Summary um und läuft nur, wenn ein Schlüssel gesetzt ist. Ausgeliefert wird die App als Docker-Compose-Stack über GitHub Actions auf einem Self-Hosted-Runner bei jedem Push auf main. Der App-Container bindet nur 127.0.0.1:3000; ein Tailscale-Sidecar stellt sie per Funnel auf Port 443 bereit, weil 443 auf dem Host bereits von einer anderen Anwendung belegt ist und Connectoren einen anderen Port nicht erreichen.

## Ergebnis

Die Anwendung ist für diesen Umfang funktionsfähig und im Betrieb: angemeldete Mitglieder sehen ihre Boards, durchsuchen Titel und Beschreibung der eigenen Projekte, laden Leute per Link ein, entfernen Mitglieder, wobei deren Zuweisungen gelöst werden und der letzte Owner stehen bleibt, legen Aufgaben mit Priorität, Fälligkeit und Checkliste an, schieben Erledigtes ins Archiv und hängen Dateien an ein Projekt oder eine Aufgabe. Dieselbe Person kann einen Agenten unter ihrem Konto verbinden; der Agent listet und schreibt nur dort, wo sie Mitglied ist. Offen und bewusst nicht verborgen sind: es gibt keine automatisierte Testsuite; das Schema wird von Hand im Supabase-SQL-Editor angewandt und nicht vom Container; die App-Rolle umgeht Row Level Security, RLS ist dennoch an und ohne Policies für anon, damit ein geleakter Anon-Key die Tabellen einschließlich Passwort-Hashes und Einladungs-Tokens nicht liest; OAuth-Access-Tokens gelten sieben Tage; ein Gemini-Briefing schickt Aufgabentext an Google und läuft nur mit gesetztem GEMINI_API_KEY.

## Fazit & Lessons Learned

Ein gemeinsames Schreibmodul wog schwerer als eine zweite API. Oberfläche und Agent durchlaufen dieselbe Transaktion, dieselbe Mitgliedschaftsprüfung und dieselben Zod-Grenzen, sodass sich die beiden Zugänge nicht auseinanderentwickeln können. Den OAuth-Access-Token als Zeile in api_keys wiederzuverwenden hielt die MCP-Authentifizierung bei einer Funktion: der Handler prüft einen Hash und erhält eine User-Id, unabhängig davon, ob der Schlüssel von Hand erzeugt oder von Claude eingetauscht wurde. Die größte Lektion lag im Deploy. Funnel auf Port 443 war eine Produktvorgabe, kein Nachgedanke, weil ein Connector, der localhost oder einen belegten Host-Port nicht erreicht, das Feature faktisch nicht hat. Für den Schritt über den persönlichen Tailnet hinaus wären die nächsten Schritte eine Testsuite an genau den Stellen, an denen eine stille Regression teuer wäre — Mitgliedschaft und Token-Tausch —, eine Env-Prüfung beim Start, die fehlende R2- oder Gemini-Zugangsdaten meldet statt beim ersten Upload, und kürzere Access-Tokens als die heutigen sieben Tage.

© 2026 Jonas Fink