Nous payions 48 $/mois pour Monday.com. Trois sièges, plan Pro. Et chaque semaine, quelqu'un dans l'équipe disait une version de « Je déteste ça ». Non pas parce que Monday.com est mauvais — c'est vraiment un logiciel impressionnant. Mais il faisait environ 200 choses dont nous n'avions pas besoin et ne pouvait pas vraiment faire les 4 choses que nous voulions réellement. Alors nous avons fait ce que ferait n'importe quelle agence remplie de développeurs ayant trop d'opinions : nous avons construit le nôtre.

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

Table des matières

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

Pourquoi Monday.com a cessé de fonctionner pour nous

Permettez-moi d'être spécifique au sujet de 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 « colonnes ». Notre agence pense en termes d'offres, de contacts et de projets — trois entités distinctes avec des relations entre elles. Nous avions besoin qu'une offre fasse référence à un contact, et qu'un projet fasse référence à une offre. Monday.com peut faire cela avec les colonnes liées, mais c'est lourd. 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 devions voir les offres groupées par étape ET codées par couleur selon la source (recommandation, organique, sortant). 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 : la vitesse. Celui-ci 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 dont nous avons besoin. Chaque interaction avait juste assez de latence pour se sentir comme une friction.

Problème 4 : la trajectoire des coûts. À 48 $/mois pour trois personnes, ce n'est pas cher. Mais nous observions l'ajout d'un quatrième membre de l'équipe, et la tarification de Monday.com passe à 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 étonnamment banal. Un client potentiel nous a envoyé un e-mail, et deux membres de l'équipe ont tous les deux répondu parce que ni l'un ni l'autre ne pouvait dire à partir de Monday.com qui avait « revendiqué » le prospect. Le système de notification ne l'a pas mis en évidence assez clairement, et notre solution de contournement bidouillée d'ajouter vous-même à une colonne « Personnes » n'était pas fiable. C'est alors que j'ai ouvert VS Code au lieu de Monday.com.

Ce que nous avions réellement besoin

Avant d'écrire le moindre code, nous avons passé environ une heure à lister exactement ce que notre CRM devait faire. Non pas ce qui serait bien. Ce qui était réellement nécessaire.

Voici la liste :

  1. Tableau kanban avec des colonnes pour les étapes d'offre : Lead → Contacté → Proposition → Négociation → Remporté → Perdu
  2. Cartes d'offres affichant : nom du contact, valeur de l'offre, balise source (codée par couleur), membre de l'équipe assigné, jours à l'étape actuelle
  3. Glisser-déposer entre les colonnes avec persistance instantanée
  4. Vue de détail de l'offre avec notes (markdown), informations de contact, et un simple journal d'activité
  5. Synchronisation en temps réel pour que deux personnes regardant le tableau voient le même état
  6. Base de données de contacts avec informations de base (nom, e-mail, entreprise, notes)
  7. Authentification 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 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 un atelier de développement Astro, donc Astro était le point de départ évident. Mais il vaut la peine d'expliquer pourquoi cela a réellement du sens ici, car la réputation d'Astro en tant que « générateur de site statique » ne lui rend pas justice.

Depuis Astro 4.x (et maintenant 5.x en 2025), 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 toujours pré-rendre des éléments comme la page de connexion.

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

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

Fonctionnalité Pourquoi cela importait
Postgres sous le capot Vraie base de données relationnelle, vraies clés étrangères, vraies requêtes
Abonnements Realtime Prise en charge WebSocket intégrée 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 propre, bon support TypeScript
Niveau gratuit Notre utilisation s'adapte confortablement au plan gratuit de Supabase
Option d'auto-hébergement Si nous surpassons le niveau gratuit, nous pouvons le faire fonctionner nous-mêmes

Nous avons brièvement considéré d'autres options :

Option Pourquoi nous avons refusé
Firebase / Firestore NoSQL rend les données relationnelles maladroites. Nous avons déjà eu des problèmes.
PlanetScale Super, mais pas de realtime intégré. Nous aurions 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 le construisons 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 kanban CRM : pourquoi nous avons reconstruit Monday.com en Astro + Supabase - architecture

Conception de la base de données : garder les choses 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 d'offre : 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 mineures préférées. Chaque fois qu'une offre passe à 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 au sein des colonnes kanban. Lorsque vous faites glisser une carte entre deux autres, nous calculons une nouvelle valeur de position. Nous utilisons l'espacement des entiers (les positions s'incrémentent de 1000) pour que nous ayons rarement besoin de rééquilibrer.

Construction du 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 bien maintenue dans l'écosystème React en 2025.

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 changements 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 effectue 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 l'annule. 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) {
    // Annulation — récupérer depuis le serveur
    await refreshDeals();
    toast.error('Échec du déplacement de l\'offre');
  }

  // 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 l'héberge 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 la 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 immédiatement du HTML, et le tableau interactif démarre juste après.

Mises à jour en temps réel avec Supabase Realtime

C'était la fonctionnalité qui rendait Supabase le gagnant clair pour ce projet. Lorsqu'un membre de l'équipe déplace une offre, les autres membres de l'équipe la voient se déplacer en temps réel. Aucune actualisation nécessaire.

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

Un hic : lorsque VOUS déplacez une offre, vous récupérez votre propre changement via l'abonnement realtime. Si vous n'êtes pas prudent, cela provoque un scintillement visuel où la carte saute. Nous gérons cela en marquant 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 se sentir solide.

Authentification et sécurité au niveau des lignes

Puisque c'est un outil interne, l'authentification est simple. Nous utilisons Supabase Auth avec e-mail/mot de passe. Trois comptes. Aucun flux d'inscription — nous avons créé les comptes manuellement dans le tableau de bord de 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 ne fuira pas accidentellement les données même si nous gâchons le code de l'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 grandissons un jour jusqu'à un point où nous avons besoin d'autorisations basées sur les rôles, l'infrastructure RLS est déjà là. Nous resserrerions juste les politiques.

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

C'est la partie amusante. Parlons argent.

Service Plan Coût mensuel
Supabase Niveau gratuit 0 $
Vercel (hébergement Astro SSR) Plan Pro (déjà eu) 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 le déploiement d'une application SSR de plus ne nous coûte rien en plus. Le niveau gratuit de Supabase nous donne 500 Mo de stockage de base de données, 50 000 utilisateurs actifs mensuels (nous en avons 3), et des connexions realtime. Nous utilisons peut-être 1 % de la capacité du niveau 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 heure ~20 heures
Maintenance 0 heure/mois ~1 heure/mois

À notre taux horaire interne, 20 heures de temps de développement valent beaucoup plus que 576 $/an. Mais ces mathématiques ratent le point. Nous avons construit ceci en partie parce que nous le voulions, en partie parce que c'est un meilleur outil pour notre flux de travail spécifique, et en partie parce que ces 20 heures nous ont appris 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 un CMS headless que nous construisons pour les clients.

Ce que nous ferions différemment

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

Utiliser Zustand dès le départ

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

Ajouter la recherche plus tôt

Nous n'avons pas construit la recherche avant la semaine trois, et ces trois semaines de scan 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

Pas encore ajoutés, mais nous les voulons. Appuyez sur N pour créer une nouvelle offre, / pour rechercher, 1-6 pour filtrer par étape. De 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 un écran de 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 bien.

FAQ

Combien de temps a 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é encore 10 heures depuis sur les améliorations comme la recherche, de meilleurs styles mobiles, et la correction des 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) n'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 niveau 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 kilooctets de données. Le niveau gratuit de Supabase vous donne 500 Mo de stockage, que nous n'atteindrons pas pendant des années. Le plafond des connexions realtime est généreux aussi — 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 niveau gratuit. Nous exécutons un pg_dump hebdomadaire via un travail cron sur un serveur privé virtuel de 5 $/mois que nous avions déjà. Ce n'est pas glamour, mais ça marche. Nous avons également un clone de projet Supabase sur lequel nous pouvons restaurer en cas de problème.

Cette approche peut-elle fonctionner pour une équipe de plus de 3 personnes ? Jusqu'à peut-être 10-15 personnes, je pense que cela fonctionnerait bien avec des politiques RLS plus serrées et une logique UI basée sur les rôles. Au-delà, vous voudriez commencer les automations, les flux de travail personnalisés, et les rapports qui nécessiteraient des efforts d'ingénierie sérieux. À ce moment-là, un outil CRM dédié a plus de sens — juste peut-être pas Monday.com.

Comment la synchronisation en temps réel fonctionne-t-elle ? Supabase Realtime utilise WebSockets sous le capot, et pour notre cas d'usage (3 utilisateurs simultanés, mises à jour peu fréquentes), c'est essentiellement instantané. Nous avons mesuré la latence de bout en bout depuis un utilisateur faisant glisser une carte jusqu'à un autre utilisateur voyant la mise à jour : généralement 80-150 ms. C'est plus rapide que nous pouvons percevoir.

Avez-vous considéré les alternatives CRM open-source comme Twenty ou Folk ? Nous avons regardé Twenty (le CRM open-source 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 kanban-focalisé plus simple, nous aurions peut-être choisi cette route.

Construiriez-vous des outils internes personnalisés pour les clients aussi ? Nous l'avons fait, en fait. Plusieurs clients nous ont approchés après avoir surpassé les outils comme Monday.com, Notion, ou Airtable pour des flux de travail spécifiques. Nous construisons généralement ceux-ci avec Astro ou Next.js sur le front-end et Supabase ou un CMS headless sur le back-end. Si cela semble être quelque chose dont vous avez besoin, nous devrions parler.