Je factuur komt binnen: $48/maand voor Monday.com. Drie gebruikers, Pro plan. En elke sprint review mompelt iemand dezelfde zin — "Ik haat dit ding." Niet omdat de software kapot is. Monday.com is echt indrukwekkend. Het doet alleen 200 dingen die je pijplijn niet nodig heeft en struikelt over de 4 workflows die je eigenlijk gebruikt. Kolommen sorteren niet op de manier waarop je verkoopproces werkt. Automatiseringen worden twee keer uitgevoerd. Het mobiele weergave maakt je accountmanager haar telefoon willen gooien. Dus hebben we gedaan wat elke dev shop met Supabase-tegoed en te veel meningen doet: we hebben het opnieuw gebouwd. Elf dagen, 1.200 regels Astro, nul spijt. Hier is het schema, de drag-controller, en de rekening waardoor onze CFO glimlachte.

Dit is geen "we zijn slimmer dan Monday.com" bericht. Het is een "we hadden zeer specifieke behoeften, een stack die we al kenden, en een weekend" bericht. Hier is het volledige verhaal van het bouwen van een aangepast CRM kanban board met Astro, Supabase, en een gezonde dosis verachting voor SaaS bloat.

Inhoudsopgave

Inside Our CRM Kanban: Why We Rebuilt Monday.com in Astro + Supabase

Waarom Monday.com Voor Ons Niet Meer Werkte

Laat me specifiek zijn over onze frustraties, omdat vage klachten over SaaS tools nutteloos zijn.

Probleem 1: Het gegevensmodel vecht tegen ons. Monday.com denkt in "boards" en "items" met "kolommen." Onze bureauwerk denkt in deals, contacten en projecten — drie afzonderlijke entiteiten met relaties ertussen. We hadden een deal nodig die verwijst naar een contact, en een project dat verwijst naar een deal. Monday.com kan dit soort doen met gekoppelde kolommen, maar het is onhandig. Telkens wanneer iemand een nieuwe deal maakte, moest hij deze handmatig aan het juiste contact koppelen. Mensen vergaten het. Gegevens werden rommel.

Probleem 2: De kanban-weergave kon niet doen wat we wilden. We hadden deals nodig gegroepeerd op fase EN kleurgecodeerd op bron (verwijzing, organisch, outbound). Monday.com's kanban-weergave laat je groeperen op één statuskolom. Dat is het. Je kunt niet een tweede visuele dimensie toevoegen zonder het te hacken met naamgeving.

Probleem 3: Snelheid. Dit is subjectief, maar Monday.com voelde traag voor wat we deden. Klik op een deal, wacht tot het zijpaneel laadt, scroll voorbij velden die we niet gebruiken, vind de ene opmerking die we nodig hebben. Elke interactie had net genoeg latentie om wrijving te voelen.

Probleem 4: Kosttrajectorie. Op $48/maand voor drie personen is het niet duur. Maar we wilden een vierde teamlid, en Monday.com's prijzen springen naar $60/maand voor het Pro plan met 5 gebruikers (je kunt niet 4 kopen). Dat is $720/jaar voor een tool waarvan we actief klaagden.

Het Kantelpunt

De werkelijke trigger was beschamend alledaags. Een potentiële klant mailde ons, en twee teamleden antwoordden beiden omdat geen van beiden kon zien van Monday.com wie de lead had "opgeëist". Het notificatiesysteem maakte het niet duidelijk genoeg op het oppervlak, en onze hacky oplossing om jezelf toe te voegen aan een "People" kolom was niet betrouwbaar. Dat is toen ik VS Code opende in plaats van Monday.com.

Wat We Eigenlijk Nodig Hadden

Voordat we code schreven, besteedden we ongeveer een uur aan het oplijsten van precies wat onze CRM moest doen. Niet wat leuk zou zijn. Wat eigenlijk nodig was.

Hier is de lijst:

  1. Kanban board met kolommen voor deal fasen: Lead → Gecontacteerd → Voorstel → Onderhandelingen → Gewonnen → Verloren
  2. Deal kaarten met: contactnaam, dealwaarde, brontag (kleurgecodeerd), toegewezen teamlid, dagen in huidige fase
  3. Slepen en neerzetten tussen kolommen met direct behoud
  4. Deal detailweergave met notities (markdown), contactgegevens, en een eenvoudig activiteitenlogboek
  5. Real-time synchronisatie zodat twee personen die het bord bekijken dezelfde staat zien
  6. Contactdatabase met basisinfo (naam, e-mail, bedrijf, notities)
  7. Eenvoudige auth — alleen ons team, geen openbare toegang

Dat is alles. Geen Gantt-diagrammen. Geen tijdregistratie. Geen automatiseringsmotor. Geen 47 verschillende kolomtypen. Alleen een kanban board ondersteund door een echte database met echte relaties.

De Stack Kiezen: Astro + Supabase

We zijn een Astro-ontwikkelingsbedrijf, dus Astro was de voor de hand liggende startpunt. Maar het is de moeite waard uit te leggen waarom het hier eigenlijk zinvol is, omdat Astro's reputatie als "static site generator" het aanzienlijk te kort doet.

Sinds Astro 4.x (en nu 5.x in 2026) is server-side rendering met on-demand routes een first-class feature. Je kunt volledige dynamische applicaties bouwen. We gebruiken Astro's hybrid rendering mode: de meeste pagina's worden server-rendered on request, maar we kunnen dingen als de login pagina nog steeds pre-renderen.

Voor het interactieve kanban board zelf gebruiken we een React island. Dit is Astro's killer feature voor apps zoals deze — de shell van de applicatie (nav, layout, auth checks) is server-rendered met nul JS, en het kanban board monteert als een enkele interactieve island met client:load.

Supabase was de databasekeuze om verschillende redenen:

Feature Waarom Het Ertoe Deed
Postgres onder de motorkap Echte relationele database, echte foreign keys, echte queries
Realtime subscriptions Ingebouwde WebSocket ondersteuning voor live updates
Row-Level Security (RLS) Auth regels op databaseniveau, niet alleen op app-niveau
JS client library Schone API, goede TypeScript ondersteuning
Gratis tier Ons gebruik past comfortabel in Supabase's gratis plan
Self-host optie Als we ooit het gratis tier uitgroeien, kunnen we het zelf hosten

We hebben kort andere opties overwogen:

Optie Waarom We Het Skippten
Firebase / Firestore NoSQL maakt relationele gegevens awkward. Eerder verbrand.
PlanetScale Geweldig, maar geen ingebouwde realtime. Zou aparte WebSocket oplossing nodig hebben.
Neon + Prisma Solide combo, maar Supabase geeft ons auth + realtime + DB in één.
Bouwen op Next.js We kennen Next.js goed, maar voor een intern tool betekent Astro's island architectuur minder client-side JS voor de niet-interactieve delen.

Inside Our CRM Kanban: Why We Rebuilt Monday.com in Astro + Supabase - architecture

Databaseontwerp: Het Domweg Eenvoudig Houden

Het schema heeft vier tabellen. Dat is alles.

-- 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()
);

Het stage_entered_at veld op deals is één van mijn favoriete kleine beslissingen. Telkens wanneer een deal naar een nieuw stadium gaat, werken we deze timestamp bij. Dat laat ons "dagen in huidige stadium" berekenen zonder het activiteitenlogboek te queryren. Eenvoudig, snel, nuttig.

Het position veld handelt ordening binnen kanban kolommen af. Wanneer je een kaart tussen twee anderen sleept, berekenen we een nieuwe positiewaarde. We gebruiken integer spacing (posities verhogen met 1000) dus we hoeven zelden opnieuw in evenwicht te brengen.

Het Kanban Board Bouwen

Het kanban board is een React component gemonteerd als een Astro island. We gebruikten @dnd-kit/core voor slepen en neerzetten omdat het de meest toegankelijke en goed onderhouden DnD bibliotheek in het React ecosysteem is vanaf 2026.

Hier is de vereenvoudigde structuur:

// 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>
  );
}

De moveDeal functie doet een optimistische update — het werkt onmiddellijk lokale state bij, en stuurt de update vervolgens naar Supabase. Als de databaseupdate mislukt, wordt deze teruggedraaid. Dit maakt het board voelen als onmiddellijk.

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 },
  });
};

De Astro pagina die dit host is minimaal:

---
// 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 richtlijn doet het zware werk. De layout, nav, en paginashell zijn allemaal server-rendered HTML. Het kanban board zelf hydreert op de client. Dit betekent dat de initiële pagina laad snel is — de browser krijgt onmiddellijk HTML en het interactieve board start meteen daarna op.

Real-Time Updates met Supabase Realtime

Dit was de functie die Supabase de duidelijke winnaar maakte voor dit project. Wanneer één teamlid een deal verplaatst, zien de andere teamleden het onmiddellijk verplaatsen. Geen vernieuwen nodig.

// 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);
    };
  }, []);
}

Eén gotcha: wanneer JIJ een deal verplaatst, krijg je je eigen wijziging terug via de realtime subscription. Als je niet voorzichtig bent, veroorzaakt dit een visuele glitch waar de kaart springt. We hanteren dit door optimistische updates te labelen met een timestamp en realtime events te negeren die recente lokale wijzigingen matchen. Het zijn een paar extra regels code maar het maakt de UX voelen echt.

Authenticatie en Row-Level Security

Omdat dit een intern tool is, is auth eenvoudig. We gebruiken Supabase Auth met e-mail/wachtwoord. Drie accounts. Geen sign-up flow — we maakten de accounts handmatig in het Supabase dashboard.

Row-Level Security is waar het interessant wordt. Hoewel dit een intern tool is, betekent RLS dat onze database niet onbedoeld gegevens zal lekken zelfs als we de applicatiecode verprullen.

-- 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, deze policies zijn permissief — elke geverifieerde gebruiker kan alles doen. Voor een team van drie personen is dat prima. Als we ooit groeien tot een grootte waar we rol-gebaseerde permissies nodig hebben, is de RLS-infrastructuur al aanwezig. We zouden gewoon de policies aanscherpen.

Implementatie en Hosting Kosten

Hier komt het grappige gedeelte. Laten we over geld praten.

Service Plan Maandelijkse Kosten
Supabase Gratis tier $0
Vercel (hosting Astro SSR) Pro plan (hadden we al) $0 incrementeel
Domain subdomein van bestaand domein $0
Totaal $0/maand

We zaten al op Vercel's Pro plan voor client projecten, dus het implementeren van één meer SSR app kost ons niets extra. Supabase's gratis tier geeft ons 500MB databaseopslag, 50.000 maandelijks actieve gebruikers (we hebben 3), en realtime verbindingen. We gebruiken misschien 1% van de gratis tier's capaciteit.

Vergelijken met Monday.com:

Monday.com Onze Aangepaste CRM
Maandelijkse kosten $48 (3 gebruikers, Pro) $0
Jaarlijkse kosten $576 $0
Bouwstijd 0 uur ~20 uur
Onderhoud 0 uur/maand ~1 uur/maand

Bij onze interne uurtarief is 20 uur dev-tijd veel meer waard dan $576/jaar. Maar die wiskunde mist het punt. We bouwden dit deels omdat we wilden, deels omdat het een beter gereedschap voor onze specifieke workflow is, en deels omdat die 20 uur ons dingen leerden die we sindsdien hebben gebruikt op client projecten. We hebben sindsdien vergelijkbare Astro + Supabase architecturen toegepast voor headless CMS-ondersteunde applicaties die we voor klanten bouwen.

Wat We Anders Zouden Doen

Het is ongeveer vier maanden geleden dat we v1 launchten. Hier is wat ik zou veranderen:

Zustand Van Dag Één Gebruiken

We begonnen met React's ingebouwde useState en useContext voor state management. Tegen de tijd dat we realtime sync, optimistische updates, en rollback logica toevoegden, was de state management code verward. We migreerden naar Zustand na twee weken. Had daar moeten beginnen.

Zoeken Eerder Toevoegen

We bouwden zoeken niet tot week drie, en die drie weken van handmatig scannen van kolommen voor een specifieke deal waren vervelend. Een eenvoudige ilike query op Supabase zou 30 minuten hebben genomen om in te implementeren.

Keyboard Shortcuts

Echt nog niet toegevoegd, maar we willen ze. Druk op N om een nieuwe deal te maken, / om te zoeken, 1-6 om op fase te filteren. Kleine dingen die optellen wanneer je meerdere keren per dag in het gereedschap bent.

Beter Mobiel Beeld

Het kanban board werkt op mobiel, technisch gezien. Maar zes kolommen passen niet op een telefoonscherm. We hebben een lijstweergave voor mobiel nodig. Hebben het niet geprioriteerd omdat we zelden de CRM op onze telefoons controleren, maar het zou leuk zijn.

Veelgestelde Vragen

Hoe lang duurde het om het CRM kanban board te bouwen? De eerste bruikbare versie duurde ongeveer 20 uur verspreid over een weekend en een paar avonden. Dat gaf ons het kanban board, deal details, slepen en neerzetten, en basis auth. We hebben waarschijnlijk nog 10 uur sinds dien besteed aan verbeteringen zoals zoeken, betere mobiele stijlen, en bugfixes.

Waarom Astro in plaats van Next.js voor een dynamische app? Astro's island architectuur betekent dat de niet-interactieve delen van onze app (layout, nav, statische pagina's) nul JavaScript verzenden. Het kanban board zelf is een React island die op laad hydreert. Voor een intern gereedschap waar het interactieve oppervlak is gericht op één component, is dit een geweldige fit.

Is Supabase's gratis tier echt genoeg voor een CRM? Voor een klein team, absoluut. We hebben misschien 200 deals, 150 contacten, en een paar duizend activiteitenlogboekitems. Dat zijn kilobytes gegevens. Supabase's gratis tier geeft je 500MB opslag, wat we jaren niet zullen bereiken. De realtime verbindingslimiet is ook royaal — je krijgt tot 200 gelijktijdige verbindingen op het gratis plan.

Wat met backups? Supabase bevat dagelijkse backups op het Pro plan ($25/maand), maar we zitten op de gratis tier. We draaien een wekelijkse pg_dump via een cron job op een $5/maand VPS die we al hadden. Het is niet glamoureus, maar het werkt. We hebben ook een Supabase project clone die we kunnen herstellen als iets fout gaat.

Kan deze benadering voor een team groter dan 3 personen werken? Tot misschien 10-15 personen, denk ik dat dit goed zou werken met sterkere RLS policies en wat op rol gebaseerde UI logica. Voorbij dat zou je features willen zoals automatiseringen, aangepaste workflows, en rapportage die serieuze engineering-inspanning zou vergen. Op dat moment maakt een specifiek CRM gereedschap meer zin — alleen misschien niet Monday.com.

Hoe presteert de real-time sync? Supabase Realtime gebruikt WebSockets onder de motorkap, en voor ons geval (3 gelijktijdige gebruikers, lage-frequentie updates) is het vrijwel onmiddellijk. We maten de end-to-end latentie van één gebruiker die een kaart sleept naar een ander gebruiker die de update ziet: typisch 80-150ms. Dat is sneller dan we kunnen waarnemen.

Heb je open-source CRM alternatieven zoals Twenty of Folk overwogen? We keken naar Twenty (de open-source CRM die in 2024 lanceerde) en het is indrukwekkend. Maar het is een volledige CRM met veel meer functies dan we nodig hadden, en het zelf hosten vereist meer infrastructuur. Ons doel was precies te bouwen wat we nodig hadden en niets meer. Als Twenty had bestaan toen we begonnen en had een eenvoudiger kanban-gericht mode gehad, hebben we dat misschien geprobeerd.

Zou je aangepaste intern gereedschap voor klanten bouwen? We hebben het gedaan, eigenlijk. Verschillende klanten zijn naar ons gekomen na het uitgroeien van gereedschappen zoals Monday.com, Notion, of Airtable voor specifieke workflows. We bouwen deze typisch met Astro of Next.js op de frontend en Supabase of een headless CMS op de backend. Als dat klinkt als iets dat je nodig hebt, zouden we moeten praten.