Warum wir Monday.com abgelöst und unser CRM in Astro + Supabase gebaut haben
Deine Rechnung kommt rein: $48/Monat für Monday.com. Drei Plätze, Pro-Plan. Und bei jedem Sprint-Review murmelt jemand denselben Satz – "Ich hasse dieses Ding." Nicht weil die Software kaputt ist. Monday.com ist wirklich beeindruckend. Es macht nur 200 Dinge, die deine Pipeline nicht braucht, und scheitert bei den 4 Workflows, die du eigentlich brauchst. Spalten lassen sich nicht so sortieren, wie dein Verkaufsprozess abläuft. Automationen feuern zweimal. Die Mobile-Ansicht bringt deine Account-Managerin dazu, ihr Handy werfen zu wollen. Also taten wir, was jede Dev-Agentur mit Supabase-Credits und zu vielen Meinungen tut: Wir haben es neu gebaut. Elf Tage, 1.200 Zeilen Astro, null Bedauern. Hier ist das Schema, der Drag-Controller und die Rechnung, die unserem CFO ein Lächeln ins Gesicht zauberte.
Das ist kein "wir sind schlauer als Monday.com"-Post. Es ist ein "wir hatten sehr spezifische Anforderungen, einen Stack den wir bereits kannten, und ein Wochenende"-Post. Hier ist die vollständige Geschichte, wie wir ein benutzerdefiniertes CRM-Kanban-Board mit Astro, Supabase und einer gesunden Portion Verachtung für SaaS-Überfluss gebaut haben.
Inhaltsverzeichnis
- Warum Monday.com nicht mehr für uns funktionierte
- Was wir wirklich brauchten
- Stack-Auswahl: Astro + Supabase
- Datenbankdesign: Keep It Stupid Simple
- Kanban-Board bauen
- Echtzeit-Updates mit Supabase Realtime
- Authentifizierung und Row-Level Security
- Deployment und Hosting-Kosten
- Was wir anders machen würden
- FAQ

Warum Monday.com nicht mehr für uns funktionierte
Lass mich spezifisch über unsere Frustration sein, denn vage Beschwerden über SaaS-Tools sind nutzlos.
Problem 1: Das Datenmodell arbeitete gegen uns. Monday.com denkt in "Boards" und "Items" mit "Spalten." Unsere Agentur denkt in Deals, Kontakte und Projekte – drei unterschiedliche Entitäten mit Beziehungen untereinander. Wir brauchten einen Deal, der auf einen Kontakt verweist, und ein Projekt, das auf einen Deal verweist. Monday.com kann das mit verknüpften Spalten teilweise, aber es ist unbeholfen. Jedes Mal wenn jemand einen neuen Deal erstellte, musste er ihn manuell mit dem richtigen Kontakt verknüpfen. Leute vergaßen es. Daten wurden durcheinander.
Problem 2: Die Kanban-Ansicht konnte nicht das, was wir brauchten. Wir brauchten Deals gruppiert nach Stage UND farbcodiert nach Quelle (Referral, Organic, Outbound). Monday.coms Kanban-Ansicht lässt dich nach einer Status-Spalte gruppieren. Das wars. Du kannst keine zweite visuelle Dimension hinzufügen, ohne mit Benennungskonventionen zu hacken.
Problem 3: Geschwindigkeit. Das ist subjektiv, aber Monday.com fühlte sich langsam an für das, was wir taten. Klick auf einen Deal, warte bis der Seitenpanel lädt, scrolle an Feldern vorbei, die wir nicht nutzen, finde die eine Note, die wir brauchen. Jede Interaktion hatte genug Latenz, um sich wie Reibung anzufühlen.
Problem 4: Kostenentwicklung. Bei $48/Monat für drei Personen ist es nicht teuer. Aber wir dachten an ein viertes Teammitglied, und Monday.coms Preisgestaltung springt auf $60/Monat für den Pro-Plan mit 5 Plätzen (du kannst nicht 4 kaufen). Das sind $720/Jahr für ein Tool, über das wir aktiv klagten.
Der Wendepunkt
Der eigentliche Auslöser war peinlich trivial. Ein potenzieller Kunde schrieb uns an, und zwei Teammitglieder antworteten beide, weil keiner aus Monday.com sehen konnte, wer den Lead "gefordert" hatte. Das Benachrichtigungssystem machte es nicht deutlich genug, und unser Workaround, sich selbst zu einer "People"-Spalte hinzuzufügen, war nicht zuverlässig. Das war der Moment, in dem ich VS Code öffnete, statt Monday.com.
Was wir wirklich brauchten
Bevor wir Code schrieben, brauchten wir etwa eine Stunde, um genau aufzulisten, was unser CRM tun musste. Nicht was schön wäre. Was wirklich notwendig war.
Hier ist die Liste:
- Kanban-Board mit Spalten für Deal-Stages: Lead → Contacted → Proposal → Negotiation → Won → Lost
- Deal-Karten mit: Kontaktname, Deal-Wert, Source-Tag (farbcodiert), zugewiesenes Teammitglied, Tage in aktueller Stage
- Drag and Drop zwischen Spalten mit sofortiger Persistierung
- Deal-Detailansicht mit Notes (Markdown), Kontaktinfos und einfachelem Activity Log
- Echtzeit-Sync sodass zwei Personen, die das Board anschauen, denselben Zustand sehen
- Kontaktdatenbank mit Basisinfos (Name, Email, Unternehmen, Notes)
- Einfache Auth – nur unser Team, kein öffentlicher Zugang
Das wars. Keine Gantt-Charts. Kein Time Tracking. Keine Automations-Engine. Keine 47 verschiedenen Spaltentypen. Nur ein Kanban-Board mit echter Datenbank und echten Beziehungen.
Stack-Auswahl: Astro + Supabase
Wir sind eine Astro-Development-Agentur, also war Astro die offensichtliche Wahl. Aber es lohnt sich zu erklären, warum es wirklich Sinn macht, denn Astros Ruf als "Static Site Generator" unterschätzt es erheblich.
Seit Astro 4.x (und jetzt 5.x im Jahr 2026) ist Server-Side Rendering mit On-Demand-Routes ein First-Class-Feature. Du kannst vollständige dynamische Anwendungen bauen. Wir nutzen Astros Hybrid-Rendering-Modus: Die meisten Seiten werden on-demand auf dem Server gerendert, aber wir können Dinge wie die Login-Seite trotzdem pre-rendern.
Für das interaktive Kanban-Board selbst nutzen wir eine React-Island. Das ist Astros Killer-Feature für Apps wie diese – die Shell der Anwendung (Nav, Layout, Auth-Checks) wird mit null JS server-gerendert, und das Kanban-Board mountet als einzelne interaktive Island mit client:load.
Supabase war die Datenbank-Wahl aus mehreren Gründen:
| Feature | Warum es wichtig war |
|---|---|
| Postgres unter der Haube | Echte relationale Datenbank, echte Foreign Keys, echte Queries |
| Realtime-Subscriptions | Built-in WebSocket-Support für Live-Updates |
| Row-Level Security (RLS) | Auth-Regeln auf Datenbankebene, nicht nur auf App-Ebene |
| JS-Client-Bibliothek | Saubere API, gute TypeScript-Unterstützung |
| Kostenlos-Tier | Unsere Nutzung passt komfortabel in Supabase free plan |
| Self-Host-Option | Falls wir je über den Free-Tier hinauswachsen, können wir es selbst hosten |
Wir haben kurz andere Optionen erwogen:
| Option | Warum wir passten |
|---|---|
| Firebase / Firestore | NoSQL macht relationale Daten unbeholfen. Haben wir schon erlebt. |
| PlanetScale | Großartig, aber kein Built-in Realtime. Würde separate WebSocket-Lösung brauchen. |
| Neon + Prisma | Solide Kombo, aber Supabase gibt uns Auth + Realtime + DB in einem. |
| Building on Next.js | Wir kennen Next.js gut (wir bauen es regelmäßig), aber für ein internes Tool bedeutete Astros Island-Architektur weniger Client-side JS für die nicht-interaktiven Teile. |

Datenbankdesign: Keep It Stupid Simple
Das Schema hat vier Tabellen. Das wars.
-- Contacts: the people and companies we talk to
create table contacts (
id uuid default gen_random_uuid() primary key,
name text not null,
email text,
company text,
phone text,
notes text,
created_at timestamptz default now()
);
-- Deals: the pipeline items on our kanban board
create table deals (
id uuid default gen_random_uuid() primary key,
contact_id uuid references contacts(id) on delete set null,
title text not null,
value integer, -- stored in cents
stage text not null default 'lead'
check (stage in ('lead', 'contacted', 'proposal', 'negotiation', 'won', 'lost')),
source text
check (source in ('referral', 'organic', 'outbound', 'repeat', 'other')),
assigned_to uuid references auth.users(id),
position integer not null default 0, -- for ordering within a column
stage_entered_at timestamptz default now(),
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Activity log: simple append-only log
create table activities (
id uuid default gen_random_uuid() primary key,
deal_id uuid references deals(id) on delete cascade,
user_id uuid references auth.users(id),
action text not null, -- 'stage_change', 'note', 'created', etc.
details jsonb,
created_at timestamptz default now()
);
-- Deal notes: markdown notes attached to deals
create table deal_notes (
id uuid default gen_random_uuid() primary key,
deal_id uuid references deals(id) on delete cascade,
user_id uuid references auth.users(id),
content text not null,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
Das stage_entered_at-Feld auf Deals ist eine meiner Lieblingskleinen-Entscheidungen. Jedes Mal wenn ein Deal in eine neue Stage wechselt, aktualisieren wir diesen Timestamp. Das lässt uns "Tage in aktueller Stage" berechnen, ohne das Activity Log zu befragen. Einfach, schnell, nützlich.
Das position-Feld behandelt die Sortierung innerhalb von Kanban-Spalten. Wenn du eine Karte zwischen zwei anderen ziehst, berechnen wir einen neuen Position-Wert. Wir nutzen Integer-Abstände (Positionen inkrementieren um 1000), sodass wir selten neu balancieren müssen.
Kanban-Board bauen
Das Kanban-Board ist eine React-Komponente, die als Astro-Island gemountet wird. Wir nutzen @dnd-kit/core für Drag und Drop, weil es die zugänglichste und gut gepflegte DnD-Bibliothek im React-Ökosystem ist (Stand 2026).
Hier ist die vereinfachte Struktur:
// src/components/KanbanBoard.tsx
import { DndContext, DragOverlay } from '@dnd-kit/core';
import { useState } from 'react';
import { KanbanColumn } from './KanbanColumn';
import { DealCard } from './DealCard';
import { useDeals } from '../hooks/useDeals';
import { useRealtimeDeals } from '../hooks/useRealtimeDeals';
const STAGES = ['lead', 'contacted', 'proposal', 'negotiation', 'won', 'lost'];
const SOURCE_COLORS: Record<string, string> = {
referral: '#10b981',
organic: '#3b82f6',
outbound: '#f59e0b',
repeat: '#8b5cf6',
other: '#6b7280',
};
export function KanbanBoard() {
const { deals, moveDeal } = useDeals();
const [activeId, setActiveId] = useState<string | null>(null);
// Subscribe to realtime changes
useRealtimeDeals();
const handleDragEnd = async (event) => {
const { active, over } = event;
if (!over) return;
const dealId = active.id;
const newStage = over.data.current?.stage;
const newPosition = over.data.current?.position;
if (newStage) {
await moveDeal(dealId, newStage, newPosition);
}
setActiveId(null);
};
return (
<DndContext onDragStart={({ active }) => setActiveId(active.id)} onDragEnd={handleDragEnd}>
<div className="kanban-grid">
{STAGES.map((stage) => (
<KanbanColumn
key={stage}
stage={stage}
deals={deals.filter((d) => d.stage === stage)}
sourceColors={SOURCE_COLORS}
/>
))}
</div>
<DragOverlay>
{activeId ? <DealCard deal={deals.find((d) => d.id === activeId)} overlay /> : null}
</DragOverlay>
</DndContext>
);
}
Die moveDeal-Funktion macht ein optimistisches Update – sie aktualisiert sofort den lokalen Zustand, sendet dann das Update an Supabase. Falls das Datenbankupdate fehlschlägt, wird es zurückgerollt. Das macht das Board sich sofort anfühlen.
const moveDeal = async (dealId: string, newStage: string, newPosition: number) => {
// Optimistic update
setDeals((prev) =>
prev.map((d) =>
d.id === dealId
? { ...d, stage: newStage, position: newPosition, stage_entered_at: new Date().toISOString() }
: d
)
);
const { error } = await supabase
.from('deals')
.update({
stage: newStage,
position: newPosition,
stage_entered_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
})
.eq('id', dealId);
if (error) {
// Rollback — refetch from server
await refreshDeals();
toast.error('Failed to move deal');
}
// Log the activity
await supabase.from('activities').insert({
deal_id: dealId,
user_id: currentUser.id,
action: 'stage_change',
details: { from: previousStage, to: newStage },
});
};
Die Astro-Seite, die das hostet, ist minimal:
---
// src/pages/board.astro
import Layout from '../layouts/App.astro';
import { KanbanBoard } from '../components/KanbanBoard';
import { getSession } from '../lib/auth';
const session = await getSession(Astro.request);
if (!session) return Astro.redirect('/login');
---
<Layout title="Deal Board">
<KanbanBoard client:load />
</Layout>
Die client:load-Direktive leistet die schwere Arbeit. Das Layout, Nav und Page Shell sind alle server-gerendered HTML. Das Kanban-Board selbst hydratisiert auf dem Client. Das bedeutet der initiale Page Load ist schnell – der Browser bekommt HTML sofort, und das interaktive Board bootet danach auf.
Echtzeit-Updates mit Supabase Realtime
Das war das Feature, das Supabase für dieses Projekt die klare Wahl machte. Wenn ein Teammitglied einen Deal bewegt, sehen es andere Mitglieder in Echtzeit. Kein Refresh nötig.
// src/hooks/useRealtimeDeals.ts
import { useEffect } from 'react';
import { supabase } from '../lib/supabase';
import { useDealsStore } from '../stores/deals';
export function useRealtimeDeals() {
const { updateDealLocally, addDealLocally, removeDealLocally } = useDealsStore();
useEffect(() => {
const channel = supabase
.channel('deals-changes')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'deals' },
(payload) => {
switch (payload.eventType) {
case 'UPDATE':
updateDealLocally(payload.new);
break;
case 'INSERT':
addDealLocally(payload.new);
break;
case 'DELETE':
removeDealLocally(payload.old.id);
break;
}
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, []);
}
Ein Gotcha: Wenn DU einen Deal verschiebst, bekommst du deine eigene Änderung über die Realtime-Subscription zurück. Falls du nicht aufpasst, verursacht das einen visuellen Glitch, wo die Karte springt. Wir handhaben das, indem wir optimistische Updates mit einem Timestamp taggen und Realtime-Events ignorieren, die kürzliche lokale Änderungen matchen. Es sind ein paar zusätzliche Code-Zeilen, aber es macht die UX robust.
Authentifizierung und Row-Level Security
Da das ein internes Tool ist, ist Auth einfach. Wir nutzen Supabase Auth mit Email/Passwort. Drei Accounts. Kein Sign-up Flow – wir erstellten die Accounts manuell im Supabase Dashboard.
Row-Level Security ist wo es interessant wird. Auch wenn das ein internes Tool ist, bedeutet RLS, dass unsere Datenbank nicht versehentlich Daten leakt, falls wir die Anwendung vermasseln.
-- Only authenticated users can see deals
alter table deals enable row level security;
create policy "Authenticated users can read all deals"
on deals for select
to authenticated
using (true);
create policy "Authenticated users can insert deals"
on deals for insert
to authenticated
with check (true);
create policy "Authenticated users can update deals"
on deals for update
to authenticated
using (true);
create policy "Authenticated users can delete deals"
on deals for delete
to authenticated
using (true);
Ja, diese Policies sind freizügig – jeder authentifizierte Nutzer kann alles tun. Für ein Team von drei Personen ist das okay. Falls wir je zu einer Größe wachsen, wo wir rollenbasierte Permissions brauchen, ist die RLS-Infrastruktur bereits da. Wir würden die Policies einfach verschärfen.
Deployment und Hosting-Kosten
Hier kommt der unterhaltsame Teil. Lass uns über Geld sprechen.
| Service | Plan | Monatliche Kosten |
|---|---|---|
| Supabase | Free-Tier | $0 |
| Vercel (Hosting Astro SSR) | Pro-Plan (hatte wir bereits) | $0 inkrementell |
| Domain | Subdomain einer bestehenden Domain | $0 |
| Gesamt | $0/Monat |
Wir waren bereits auf Vercels Pro-Plan für Client-Projekte, also kostet die Bereitstellung einer weiteren SSR-App uns nichts Extra. Supabase free-Tier gibt uns 500MB Datenbank-Speicher, 50.000 monatliche aktive Nutzer (wir haben 3), und Realtime-Connections. Wir nutzen vielleicht 1% des Free-Tier-Kapazität.
Vergleich mit Monday.com:
| Monday.com | Unser Custom CRM | |
|---|---|---|
| Monatliche Kosten | $48 (3 Plätze, Pro) | $0 |
| Jährliche Kosten | $576 | $0 |
| Buildzeit | 0 Stunden | ~20 Stunden |
| Wartung | 0 Stunden/Monat | ~1 Stunde/Monat |
Bei unserer internen Stundensatz sind 20 Stunden Dev-Zeit viel mehr wert als $576/Jahr. Aber die Mathe verpasst den Punkt. Wir bauten das teilweise weil wir wollten, teilweise weil es ein besseres Tool für unseren spezifischen Workflow ist, und teilweise weil diese 20 Stunden uns Dinge beibrachten, die wir später bei Client-Projekten anwandten. Seitdem haben wir ähnliche Astro + Supabase-Architekturen für Headless-CMS-gestützte Anwendungen verwendet, die wir für Clients bauen.
Was wir anders machen würden
Es sind etwa vier Monate seit wir v1 geliefert haben. Hier ist was ich ändern würde:
Nutze Zustand von Tag eins
Wir starteten mit Reacts built-in useState und useContext für State Management. Bis wir Realtime Sync, Optimistische Updates und Rollback-Logik hinzufügten, war der State-Management-Code verwickelt. Wir migrieren zu Zustand nach zwei Wochen. Sollten wir von Anfang an gemacht haben.
Suche früher hinzufügen
Wir bauten Suche nicht bis Woche drei, und diese drei Wochen manuell in Spalten nach einem spezifischen Deal scannen waren unangenehm. Eine einfache ilike-Query auf Supabase hätte 30 Minuten zum Implementieren gebraucht.
Tastatur-Shortcuts
Haben das noch nicht hinzugefügt, aber wir wollen sie. Drücke N um einen neuen Deal zu erstellen, / zum Suchen, 1-6 zum Nach Stage filtern. Kleine Dinge, die sich addieren, wenn du mehrmals täglich im Tool bist.
Bessere Mobile-Ansicht
Das Kanban-Board funktioniert technisch auf dem Handy. Aber sechs Spalten passen nicht auf einen Handyscreen. Wir brauchen eine List View für Mobile. Haben es nicht priorisiert, weil wir CRM selten auf unseren Handys checken, aber es wäre schön.
FAQ
Wie lange brauchte es, das CRM Kanban-Board zu bauen?
Die erste brauchbare Version brauchte etwa 20 Stunden über ein Wochenende und ein paar Abende verteilt. Das gab uns das Kanban-Board, Deal-Details, Drag und Drop und Basic-Auth. Wir haben seither wahrscheinlich noch 10 Stunden auf Verbesserungen wie Suche, bessere Mobile-Styles und Bugfixes gebraucht.
Warum Astro statt Next.js für eine dynamische App?
Astros Island-Architektur bedeutet die nicht-interaktiven Teile unserer App (Layout, Nav, statische Seiten) versenden null JavaScript. Das Kanban-Board selbst ist eine React-Island, die bei Load hydratisiert. Für ein internes Tool, wo die interaktive Oberfläche auf eine Komponente fokussiert ist, ist das großartig. Wir nutzen Next.js für Client-Projekte, wo die Interaktivität sich mehr über Seiten verteilt.
Ist Supabase free-Tier wirklich genug für ein CRM?
Für ein kleines Team, absolut. Wir haben vielleicht 200 Deals, 150 Kontakte und ein paar Tausend Activity-Log-Einträge. Das sind Kilobytes an Daten. Supabase free-Tier gibt dir 500MB Speicher, den wir Jahre lang nicht treffen werden. Die Realtime-Connections-Cap ist auch großzügig – du bekommst bis zu 200 gleichzeitige Connections im Free-Plan.
Was ist mit Backups?
Supabase enthält tägliche Backups im Pro-Plan ($25/Monat), aber wir sind im Free-Tier. Wir führen ein wöchentliches pg_dump über einen Cron-Job auf einem $5/Monat VPS, den wir bereits hatten, aus. Es ist nicht glamourös, aber es funktioniert. Wir haben auch ein Supabase-Projekt-Klon, den wir bei Bedarf wiederherstellen können.
Kann dieser Ansatz für ein Team größer als 3 Personen funktionieren?
Bis vielleicht 10-15 Personen, denke ich das würde mit strafferen RLS-Policies und etwas rollenbasierter UI-Logik funktionieren. Darüber hinaus würdest du anfangen Features wie Automationen, Custom Workflows und Reporting zu wollen, die seriöse Enginering-Arbeit erfordern würden. An diesem Punkt macht ein dediziertes CRM-Tool mehr Sinn – vielleicht nur nicht Monday.com.
Wie ist die Performance des Realtime-Sync?
Supabase Realtime nutzt WebSockets unter der Haube, und für unsere Verwendung (3 gleichzeitige Nutzer, niedrige Updatefrequenz), ist es im Grunde sofort. Wir maßen die End-to-End-Latenz von einem Nutzer, der eine Karte zieht, zu einem anderen Nutzer, der das Update sieht: typisch 80-150ms. Das ist schneller als wir wahrnehmen können.
Hattest du Open-Source CRM Alternativen wie Twenty oder Folk in Betracht gezogen?
Wir sahen uns Twenty an (das Open-Source CRM, das 2024 gestartet wurde) und es ist beeindruckend. Aber es ist ein volles CRM mit weit mehr Features als wir brauchten, und Self-Hosting braucht mehr Infrastruktur. Unser Ziel war zu bauen exakt was wir brauchten und nichts mehr. Falls Twenty existiert hätte, als wir starteten und einen einfacheren Kanban-fokussiertem Modus hätte, hätten wir das vielleicht genommen.
Würdest du auch Custom interne Tools für Clients bauen?
Wir haben, wirklich. Mehrere Clients kamen zu uns, nachdem sie Tools wie Monday.com, Notion oder Airtable für spezifische Workflows outgrown hatten. Wir bauen diese typisch mit Astro oder Next.js auf dem Frontend und Supabase oder einem Headless CMS auf dem Backend. Falls das wie etwas klingt, das du brauchst, sollten wir reden.