Votre facture arrive : 48 $/mois pour Monday.com. Trois sièges, plan Pro. Et à chaque review de sprint, quelqu'un marmonne la même phrase — « Je déteste ce truc ». Pas parce que le logiciel est cassé. Monday.com est réellement impressionnant. C'est juste qu'il fait 200 choses que votre pipeline n'a pas besoin et échoue sur les 4 workflows que vous utilisez réellement. Les colonnes ne se trient pas selon votre processus de vente. Les automations se déclenchent deux fois. La vue mobile donne envie à votre responsable de compte de jeter son téléphone. Nous avons donc fait ce que ferait n'importe quelle agence de développement avec des crédits Supabase et trop d'opinions : nous l'avons reconstruit. Onze jours, 1 200 lignes d'Astro, zéro regrets. Voici le schéma, le contrôleur de glisser-déposer, et la facture qui a fait sourire notre CFO.

Ce n'est pas un post « nous sommes plus intelligents que Monday.com ». C'est un post « nous avions des besoins très spécifiques, une pile technologique que nous connaissions déjà, et un week-end ». Voici l'histoire complète de la construction d'un tableau kanban CRM personnalisé utilisant Astro, Supabase, et une bonne dose de mépris envers l'obésité logicielle SaaS.

Table des matières

À l'intérieur de notre CRM Kanban : Pourquoi nous avons reconstruit Monday.com en Astro + Supabase

Pourquoi Monday.com a cessé de fonctionner pour nous

Soyons précis sur nos frustrations, car les plaintes vagues sur les outils SaaS sont inutiles.

Problème 1 : Le modèle de données nous combattait. Monday.com pense en « tableaux » et « éléments » avec des « colonnes ». Notre agence pense en offres, contacts et projets — trois entités distinctes avec des relations entre elles. Nous avions besoin qu'une offre référence un contact, et un projet référence une offre. Monday.com peut presque faire cela avec des colonnes liées, mais c'est maladroit. Chaque fois que quelqu'un créait une nouvelle offre, il devait la lier manuellement au bon contact. Les gens oubliaient. Les données devenaient désordonnées.

Problème 2 : La vue kanban ne pouvait pas faire ce que nous voulions. Nous avions besoin de voir les offres groupées par étape ET codées par couleur selon la source (parrainage, organique, démarchage). La vue kanban de Monday.com vous permet de grouper par une colonne de statut. C'est tout. Vous ne pouvez pas ajouter une deuxième dimension visuelle sans la bidouiller avec des conventions de nommage.

Problème 3 : Vitesse. C'est subjectif, mais Monday.com semblait lent pour ce que nous faisions. Cliquez sur une offre, attendez que le panneau latéral se charge, faites défiler les champs que nous n'utilisons pas, trouvez la note que nous avons besoin. Chaque interaction avait juste assez de latence pour sembler être de la friction.

Problème 4 : Trajectoire des coûts. À 48 $/mois pour trois personnes, ce n'est pas cher. Mais nous regardions l'ajout d'un quatrième membre d'équipe, et la tarification de Monday.com saute à 60 $/mois pour le plan Pro avec 5 sièges (vous ne pouvez pas acheter 4). C'est 720 $/an pour un outil dont nous nous plaignions activement.

Le point de basculement

Le déclencheur réel était embarrassamment banal. Un client potentiel nous a envoyé un email, et deux membres d'équipe ont tous les deux répondu parce que personne ne pouvait dire depuis Monday.com qui avait « réclamé » le prospect. Le système de notifications n'en rendait pas compte clairement, et notre contournement hacky d'ajouter vous-même une colonne « Personnes » n'était pas fiable. C'est à ce moment que j'ai ouvert VS Code au lieu de Monday.com.

De quoi avions-nous réellement besoin

Avant d'écrire du code, nous avons passé environ une heure à lister exactement ce que notre CRM devait faire. Pas ce qui serait sympa. Ce qui était réellement nécessaire.

Voici la liste :

  1. Tableau kanban avec des colonnes pour les étapes de l'offre : Prospect → Contacté → Proposition → Négociation → Remporté → Perdu
  2. Cartes d'offre affichant : nom du contact, valeur de l'offre, balise source (codée par couleur), membre d'équipe assigné, jours à l'étape actuelle
  3. Glisser et déposer entre les colonnes avec persistance instantanée
  4. Vue détaillée de l'offre avec notes (markdown), informations de contact, et un simple journal d'activité
  5. Synchronisation en temps réel afin que deux personnes regardant le tableau voient le même état
  6. Base de données de contacts avec informations de base (nom, email, entreprise, notes)
  7. Auth simple — juste notre équipe, pas d'accès public

C'est tout. Pas de diagrammes de Gantt. Pas de suivi du temps. Pas de moteur d'automations. Pas de 47 types de colonnes différents. Juste un tableau kanban soutenu par une vraie base de données avec de vraies relations.

Choisir la pile : Astro + Supabase

Nous sommes une agence de développement Astro, donc Astro était le choix évident. Mais cela vaut la peine d'expliquer pourquoi cela a vraiment du sens ici, car la réputation d'Astro en tant que « générateur de site statique » le sous-estime considérablement.

Depuis Astro 4.x (et maintenant 5.x en 2026), le rendu côté serveur avec des routes à la demande est une fonctionnalité de première classe. Vous pouvez construire des applications entièrement dynamiques. Nous utilisons le mode de rendu hybride d'Astro : la plupart des pages sont rendues côté serveur à la demande, mais nous pouvons quand même pré-rendre des choses comme la page de connexion.

Pour le tableau kanban interactif lui-même, nous utilisons une île React. C'est la fonctionnalité vedette d'Astro pour des applications comme celle-ci — l'enveloppe de l'application (nav, disposition, vérifications d'authentification) est rendue côté serveur avec zéro JS, et le tableau kanban se monte en tant qu'une seule île interactive avec client:load.

Supabase était le choix de la base de données pour plusieurs raisons :

Fonctionnalité Pourquoi cela a compté
Postgres en dessous Vraie base de données relationnelle, vraies clés étrangères, vraies requêtes
Abonnements Realtime Support WebSocket intégré pour les mises à jour en direct
Sécurité au niveau des lignes (RLS) Règles d'authentification au niveau de la base de données, pas seulement au niveau de l'application
Bibliothèque client JS API claire, bon support TypeScript
Tier gratuit Notre utilisation s'adapte confortablement au plan gratuit de Supabase
Option d'auto-hébergement Si nous surpassons jamais le tier gratuit, nous pouvons l'exécuter nous-mêmes

Nous avons brièvement envisagé d'autres options :

Option Pourquoi nous avons passé
Firebase / Firestore NoSQL rend les données relationnelles maladroites. Avons été brûlés avant.
PlanetScale Excellent, mais pas de realtime intégré. Aurait besoin d'une solution WebSocket séparée.
Neon + Prisma Combo solide, mais Supabase nous donne l'authentification + realtime + DB en un.
Construire sur Next.js Nous connaissons bien Next.js (nous l'utilisons régulièrement), mais pour un outil interne, l'architecture d'îles d'Astro signifiait moins de JS côté client pour les parties non interactives.

À l'intérieur de notre CRM Kanban : Pourquoi nous avons reconstruit Monday.com en Astro + Supabase - architecture

Conception de la base de données : Garder ça bêtement simple

Le schéma a quatre tables. C'est tout.

-- Contacts : les personnes et entreprises avec lesquelles nous parlons
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 : les éléments du pipeline sur notre tableau kanban
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, -- stocké en centimes
  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, -- pour l'ordre dans une colonne
  stage_entered_at timestamptz default now(),
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- Journal d'activité : simple journal d'ajout uniquement
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()
);

-- Notes sur les offres : notes markdown attachées aux offres
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()
);

Le champ stage_entered_at sur les offres est l'une de mes décisions préférées. Chaque fois qu'une offre se déplace vers une nouvelle étape, nous mettons à jour cet horodatage. Cela nous permet de calculer « jours à l'étape actuelle » sans interroger le journal d'activité. Simple, rapide, utile.

Le champ position gère l'ordre dans les colonnes kanban. Quand vous traînez une carte entre deux autres, nous calculons une nouvelle valeur de position. Nous utilisons l'espacement des entiers (les positions s'incrémentent par 1000) afin que nous ayons rarement besoin de rééquilibrer.

Construire le tableau kanban

Le tableau kanban est un composant React monté en tant qu'île Astro. Nous avons utilisé @dnd-kit/core pour le glisser-déposer car c'est la bibliothèque DnD la plus accessible et la plus bien maintenue dans l'écosystème React en 2026.

Voici la structure simplifiée :

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

  // S'abonner aux modifications en temps réel
  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>
  );
}

La fonction moveDeal fait une mise à jour optimiste — elle met à jour immédiatement l'état local, puis envoie la mise à jour à Supabase. Si la mise à jour de la base de données échoue, elle revient en arrière. Cela rend le tableau instantané.

const moveDeal = async (dealId: string, newStage: string, newPosition: number) => {
  // Mise à jour optimiste
  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 depuis le serveur
    await refreshDeals();
    toast.error('Failed to move deal');
  }

  // Enregistrer l'activité
  await supabase.from('activities').insert({
    deal_id: dealId,
    user_id: currentUser.id,
    action: 'stage_change',
    details: { from: previousStage, to: newStage },
  });
};

La page Astro qui héberge ceci est minimale :

---
// 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>

Cette directive client:load fait le gros du travail. La disposition, la nav, et l'enveloppe de page sont tous du HTML rendu côté serveur. Le tableau kanban lui-même s'hydrate sur le client. Cela signifie que le chargement initial de la page est rapide — le navigateur obtient le HTML immédiatement, et le tableau interactif démarre juste après.

Mises à jour en temps réel avec Supabase Realtime

C'était la fonctionnalité qui a fait de Supabase le gagnant clair pour ce projet. Quand un membre d'équipe déplace une offre, les autres membres d'équipe la voient se déplacer en temps réel. Pas besoin de rafraîchir.

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

Une chose gênante : quand VOUS déplacez une offre, vous récupérez votre propre changement via l'abonnement realtime. Si vous n'êtes pas prudent, cela crée un problème visuel où la carte saute. Nous gérons cela en balisonnant les mises à jour optimistes avec un horodatage et en ignorant les événements realtime qui correspondent à des changements locaux récents. C'est quelques lignes de code supplémentaires mais cela rend l'UX solide.

Authentification et sécurité au niveau des lignes

Puisque c'est un outil interne, l'authentification est simple. Nous utilisons Supabase Auth avec email/mot de passe. Trois comptes. Pas de flux d'inscription — nous avons créé les comptes manuellement dans le tableau de bord Supabase.

La sécurité au niveau des lignes est où ça devient intéressant. Bien que ce soit un outil interne, RLS signifie que notre base de données n'aura pas accidentellement de fuite de données même si nous échouons le code d'application.

-- Seuls les utilisateurs authentifiés peuvent voir les offres
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);

Ouais, ces politiques sont permissives — tout utilisateur authentifié peut faire n'importe quoi. Pour une équipe de trois personnes, c'est bien. Si nous nous développons jamais à une taille où nous avons besoin de permissions basées sur les rôles, l'infrastructure RLS est déjà là. Nous resserrerions simplement les politiques.

Déploiement et coûts d'hébergement

Voici la partie amusante. Parlons d'argent.

Service Plan Coût mensuel
Supabase Tier gratuit $0
Vercel (hébergeant Astro SSR) Plan Pro (l'avait déjà) $0 supplémentaires
Domaine Sous-domaine du domaine existant $0
Total $0/mois

Nous étions déjà sur le plan Pro de Vercel pour les projets clients, donc déployer une autre application SSR ne nous coûte rien de plus. Le tier gratuit de Supabase vous donne 500MB de stockage de base de données, 50 000 utilisateurs actifs mensuels (nous en avons 3), et les connexions realtime. Nous utilisons peut-être 1 % de la capacité du tier gratuit.

Comparez cela à Monday.com :

Monday.com Notre CRM personnalisé
Coût mensuel $48 (3 sièges, Pro) $0
Coût annuel $576 $0
Temps de construction 0 heures ~20 heures
Maintenance 0 heures/mois ~1 heure/mois

Au taux horaire interne, 20 heures de temps de développement valent bien plus que 576 $/an. Mais ces mathématiques ratent le point. Nous avons construit cela en partie parce que nous le voulions, en partie parce que c'est un meilleur outil pour notre workflow spécifique, et en partie parce que ces 20 heures nous ont enseigné des choses que nous avons depuis utilisées sur les projets clients. Nous avons depuis appliqué des architectures Astro + Supabase similaires pour les applications soutenues par CMS headless que nous construisons pour les clients.

Ce que nous ferions différemment

Cela fait environ quatre mois depuis que nous avons lancé la v1. Voici ce que je changerais :

Utiliser Zustand dès le premier jour

Nous avons commencé avec le useState et useContext intégrés de React pour la gestion d'état. Au moment où nous avons ajouté la synchronisation en temps réel, les mises à jour optimistes, et la logique de rollback, le code de gestion d'état était emmêlé. Nous avons migré vers Zustand après deux semaines. Aurait dû commencer là.

Ajouter la recherche plus tôt

Nous n'avons pas construit la recherche jusqu'à la semaine trois, et ces trois semaines de balayage manuel des colonnes pour une offre spécifique étaient ennuyeuses. Une simple requête ilike sur Supabase aurait pris 30 minutes à implémenter.

Raccourcis clavier

Toujours pas ajouté ceux-ci, mais nous les voulons. Appuyez sur N pour créer une nouvelle offre, / pour chercher, 1-6 pour filtrer par étape. Des petites choses qui s'additionnent quand vous êtes dans l'outil plusieurs fois par jour.

Meilleure vue mobile

Le tableau kanban fonctionne sur mobile, techniquement. Mais six colonnes ne rentrent pas sur l'écran d'un téléphone. Nous avons besoin d'une vue liste pour mobile. Nous ne l'avons pas priorisée parce que nous vérifions rarement le CRM sur nos téléphones, mais ce serait sympa.

FAQ

Combien de temps a-t-il fallu pour construire le tableau kanban CRM ?

La première version utilisable a pris environ 20 heures réparties sur un week-end et quelques soirées. Cela nous a donné le tableau kanban, les détails de l'offre, le glisser-déposer, et l'authentification de base. Nous avons probablement passé 10 heures supplémentaires depuis sur des améliorations comme la recherche, de meilleurs styles mobiles, et les corrections de bugs.

Pourquoi Astro au lieu de Next.js pour une application dynamique ?

L'architecture d'îles d'Astro signifie que les parties non interactives de notre application (disposition, nav, pages statiques) expédient zéro JavaScript. Le tableau kanban lui-même est une île React qui s'hydrate au chargement. Pour un outil interne où la surface interactive est concentrée sur un composant, c'est un excellent ajustement. Nous utilisons Next.js pour les projets clients où l'interactivité est plus distribuée sur les pages.

Le tier gratuit de Supabase est-il vraiment suffisant pour un CRM ?

Pour une petite équipe, absolument. Nous avons peut-être 200 offres, 150 contacts, et quelques milliers d'entrées du journal d'activité. C'est des kilobytes de données. Le tier gratuit de Supabase vous donne 500MB de stockage, que nous n'atteindrons pas avant des années. Le plafond des connexions realtime est aussi généreux — vous obtenez jusqu'à 200 connexions simultanées sur le plan gratuit.

Et les sauvegardes ?

Supabase inclut les sauvegardes quotidiennes sur le plan Pro ($25/mois), mais nous sommes sur le tier gratuit. Nous exécutons un pg_dump hebdomadaire via un travail cron sur un VPS $5/mois que nous avions déjà. Ce n'est pas reluisant, mais ça marche. Nous avons également un clone de projet Supabase duquel nous pouvons restaurer si quelque chose s'est mal passé.

Cette approche peut-elle fonctionner pour une équipe plus grande que 3 personnes ?

Jusqu'à peut-être 10-15 personnes, je pense que cela fonctionnerait bien avec des politiques RLS plus strictes et une logique UI basée sur les rôles. Au-delà, vous voudriez des fonctionnalités comme les automations, les workflows personnalisés, et les rapports qui requerraient un engineering sérieux. À ce stade, un outil CRM dédié a plus de sens — juste peut-être pas Monday.com.

Comment la synchronisation en temps réel se comporte-t-elle ?

Supabase Realtime utilise WebSockets sous le capot, et pour notre cas d'utilisation (3 utilisateurs simultanés, mises à jour peu fréquentes), c'est essentiellement instantané. Nous avons mesuré la latence de bout en bout d'un utilisateur traînant une carte à un autre utilisateur voyant la mise à jour : généralement 80-150ms. C'est plus rapide que ce que nous pouvons percevoir.

Avez-vous envisagé des alternatives CRM open-source comme Twenty ou Folk ?

Nous avons examiné Twenty (le CRM open-source qui a lancé en 2024) et c'est impressionnant. Mais c'est un CRM complet avec beaucoup plus de fonctionnalités que nous avions besoin, et l'auto-hébergement nécessite plus d'infrastructure. Notre objectif était de construire exactement ce dont nous avions besoin et rien de plus. Si Twenty avait existé quand nous avons commencé et avait un mode fokalisé kanban plus simple, nous aurions peut-être suivi cette route.

Construiriez-vous des outils internes personnalisés pour les clients aussi ?

Nous avons fait, en fait. Plusieurs clients sont venus à nous après dépasser les outils comme Monday.com, Notion, ou Airtable pour des workflows spécifiques. Nous construisons généralement ceux-ci avec Astro ou Next.js sur le frontend et Supabase ou un CMS headless sur le backend. Si cela semble être quelque chose dont vous avez besoin, nous devrions parler.