Dentro de Nuestro Kanban CRM: Por Qué Reconstruimos Monday.com en Astro + Supabase
Estábamos pagando $48/mes por Monday.com. Tres asientos, plan Pro. Y cada semana, alguien del equipo decía algo como "Odio esto". No porque Monday.com sea malo — es genuinamente un software impresionante. Pero estaba haciendo unos 200 cosas que no necesitábamos y no podía hacer bien las 4 cosas que realmente queríamos. Así que hicimos lo que haría cualquier agencia llena de desarrolladores con demasiadas opiniones: construimos el nuestro.
Este no es un post de "somos más inteligentes que Monday.com". Es un post de "tenemos necesidades muy específicas, un stack que ya conocemos, y un fin de semana". Aquí está la historia completa de cómo construir un tablero kanban CRM personalizado usando Astro, Supabase, y una buena dosis de desprecio por el software SaaS hinchado.
Tabla de Contenidos
- Por qué Monday.com dejó de funcionar para nosotros
- Lo que realmente necesitábamos
- Elegir el stack: Astro + Supabase
- Diseño de base de datos: Mantenerlo estúpidamente simple
- Construir el tablero Kanban
- Actualizaciones en tiempo real con Supabase Realtime
- Autenticación y seguridad a nivel de fila
- Implementación y costos de hosting
- Lo que haríamos diferente
- Preguntas frecuentes

Por qué Monday.com dejó de funcionar para nosotros
Permíteme ser específico sobre nuestras frustraciones, porque las quejas vagas sobre herramientas SaaS son inútiles.
Problema 1: El modelo de datos nos estaba peleando. Monday.com piensa en "tableros" e "ítems" con "columnas". Nuestra agencia piensa en acuerdos, contactos y proyectos — tres entidades distintas con relaciones entre ellas. Necesitábamos que un acuerdo hiciera referencia a un contacto, y un proyecto a un acuerdo. Monday.com puede hacer esto más o menos con columnas vinculadas, pero es torpe. Cada vez que alguien creaba un nuevo acuerdo, tenía que vincularlo manualmente al contacto correcto. La gente se olvidaba. Los datos se volvían desordenados.
Problema 2: La vista kanban no podía hacer lo que queríamos. Necesitábamos ver acuerdos agrupados por etapa Y codificados por color según la fuente (referencia, orgánico, saliente). La vista kanban de Monday.com te permite agrupar por una columna de estado. Eso es todo. No puedes superponer una segunda dimensión visual sin hackearlo con convenciones de nomenclatura.
Problema 3: Velocidad. Este es subjetivo, pero Monday.com se sentía lento para lo que estábamos haciendo. Haz clic en un acuerdo, espera a que se cargue el panel lateral, desplázate más allá de campos que no usamos, encuentra la nota que necesitamos. Cada interacción tenía suficiente latencia como para sentir fricción.
Problema 4: Trayectoria de costos. A $48/mes para tres personas, no es caro. Pero estábamos considerando un cuarto miembro del equipo, y los saltos de precios de Monday.com a $60/mes para el plan Pro con 5 asientos (no puedes comprar 4). Eso es $720/año por una herramienta de la que estábamos activamente quejándonos.
El punto de quiebre
El desencadenante real fue vergonzosamente mundano. Un cliente potencial nos envió un correo, y dos miembros del equipo respondieron porque ninguno podía decir a partir de Monday.com quién había "reclamado" el cliente. El sistema de notificaciones no lo mostraba claramente, y nuestro torpe arreglo de agregarse a sí mismo en una columna "Personas" no era confiable. Ese es el momento en que abrí VS Code en lugar de Monday.com.
Lo que realmente necesitábamos
Antes de escribir cualquier código, pasamos alrededor de una hora listando exactamente lo que nuestro CRM necesitaba hacer. No lo que sería agradable. Lo que era realmente necesario.
Aquí está la lista:
- Tablero kanban con columnas para etapas de acuerdo: Lead → Contactado → Propuesta → Negociación → Ganado → Perdido
- Tarjetas de acuerdo mostrando: nombre del contacto, valor del acuerdo, etiqueta de fuente (codificada por color), miembro del equipo asignado, días en la etapa actual
- Arrastrar y soltar entre columnas con persistencia instantánea
- Vista de detalle de acuerdo con notas (markdown), información de contacto, y un registro de actividad simple
- Sincronización en tiempo real para que dos personas mirando el tablero vean el mismo estado
- Base de datos de contactos con información básica (nombre, correo electrónico, empresa, notas)
- Autenticación simple — solo nuestro equipo, sin acceso público
Eso es todo. Sin gráficos de Gantt. Sin seguimiento de tiempo. Sin motor de automatizaciones. Sin 47 tipos de columnas diferentes. Solo un tablero kanban respaldado por una base de datos real con relaciones reales.
Elegir el stack: Astro + Supabase
Somos una tienda de desarrollo Astro, así que Astro era la opción obvia. Pero vale la pena explicar por qué realmente tiene sentido aquí, porque la reputación de Astro como "generador de sitios estáticos" lo subestima significativamente.
Desde Astro 4.x (y ahora 5.x en 2025), la renderización del lado del servidor con rutas bajo demanda es una característica de primera clase. Puedes construir aplicaciones completamente dinámicas. Usamos el modo de renderización híbrida de Astro: la mayoría de las páginas se renderizan en el servidor bajo demanda, pero aún podemos pre-renderizar cosas como la página de inicio de sesión.
Para el tablero kanban interactivo en sí, usamos una isla React. Esta es la característica ganadora de Astro para aplicaciones como esta — el shell de la aplicación (nav, layout, verificaciones de autenticación) se renderiza en el servidor con cero JS, y el tablero kanban se monta como una única isla interactiva con client:load.
Supabase fue la opción de base de datos por varias razones:
| Característica | Por qué importaba |
|---|---|
| Postgres bajo el capó | Base de datos relacional real, claves foráneas reales, consultas reales |
| Suscripciones en tiempo real | Soporte WebSocket integrado para actualizaciones en vivo |
| Seguridad a nivel de fila (RLS) | Reglas de autenticación a nivel de base de datos, no solo a nivel de aplicación |
| Librería de cliente JS | API limpia, buen soporte de TypeScript |
| Plan gratuito | Nuestro uso cabe cómodamente en el plan gratuito de Supabase |
| Opción de auto-hosting | Si alguna vez superamos el plan gratuito, podemos ejecutarlo nosotros mismos |
Brevemente consideramos otras opciones:
| Opción | Por qué pasamos |
|---|---|
| Firebase / Firestore | NoSQL hace los datos relacionales incómodos. Quemados antes. |
| PlanetScale | Excelente, pero sin tiempo real incorporado. Necesitaríamos solución WebSocket separada. |
| Neon + Prisma | Combo sólido, pero Supabase nos da autenticación + tiempo real + BD en uno. |
| Construir en Next.js | Conocemos bien Next.js (lo construimos regularmente), pero para una herramienta interna, la arquitectura de islas de Astro significaba menos JS del lado del cliente para las partes no interactivas. |

Diseño de base de datos: Mantenerlo estúpidamente simple
El esquema tiene cuatro tablas. Eso es todo.
-- Contactos: las personas y empresas con las que hablamos
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()
);
-- Acuerdos: los ítems del pipeline en nuestro tablero 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, -- almacenado en centavos
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, -- para ordenar dentro de una columna
stage_entered_at timestamptz default now(),
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Registro de actividad: registro de solo adición simple
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()
);
-- Notas de acuerdo: notas markdown adjuntas a acuerdos
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()
);
El campo stage_entered_at en acuerdos es una de mis decisiones pequeñas favoritas. Cada vez que un acuerdo se mueve a una nueva etapa, actualizamos esta marca de tiempo. Eso nos permite calcular "días en etapa actual" sin consultar el registro de actividad. Simple, rápido, útil.
El campo position maneja el orden dentro de columnas kanban. Cuando arrastras una tarjeta entre dos otras, calculamos un nuevo valor de posición. Usamos espaciado de enteros (las posiciones se incrementan en 1000) para que raramente necesitemos rebalancear.
Construir el tablero Kanban
El tablero kanban es un componente React montado como una isla Astro. Usamos @dnd-kit/core para arrastrar y soltar porque es la librería de DnD más accesible y bien mantenida en el ecosistema React a partir de 2025.
Aquí está la estructura simplificada:
// 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);
// Suscribirse a cambios en tiempo real
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 función moveDeal hace una actualización optimista — actualiza inmediatamente el estado local, luego envía la actualización a Supabase. Si la actualización de la base de datos falla, se revierte. Esto hace que el tablero se sienta instantáneo.
const moveDeal = async (dealId: string, newStage: string, newPosition: number) => {
// Actualización optimista
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) {
// Revertir — refrescar desde el servidor
await refreshDeals();
toast.error('Failed to move deal');
}
// Registrar la actividad
await supabase.from('activities').insert({
deal_id: dealId,
user_id: currentUser.id,
action: 'stage_change',
details: { from: previousStage, to: newStage },
});
};
La página Astro que aloja esto es mínima:
---
// 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>
Esa directiva client:load está haciendo el trabajo pesado. El layout, nav, y shell de página son todos HTML renderizados en el servidor. El tablero kanban en sí se hidrata en el cliente. Esto significa que la carga inicial de la página es rápida — el navegador obtiene HTML inmediatamente, y el tablero interactivo se inicia justo después.
Actualizaciones en tiempo real con Supabase Realtime
Esta fue la característica que hizo que Supabase fuera el ganador claro para este proyecto. Cuando un miembro del equipo mueve un acuerdo, los otros miembros del equipo lo ven moverse en tiempo real. Sin necesidad de actualizar.
// 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 gotcha: cuando TÚ mueves un acuerdo, recibes tu propio cambio de vuelta a través de la suscripción en tiempo real. Si no tienes cuidado, esto causa un glitch visual donde la tarjeta salta. Manejamos esto etiquetando actualizaciones optimistas con una marca de tiempo e ignorando eventos en tiempo real que coinciden con cambios locales recientes. Son solo algunas líneas extra de código pero hace que la UX se sienta sólida.
Autenticación y seguridad a nivel de fila
Dado que esta es una herramienta interna, la autenticación es simple. Usamos Supabase Auth con correo electrónico/contraseña. Tres cuentas. Sin flujo de registro — creamos las cuentas manualmente en el panel de Supabase.
La seguridad a nivel de fila es donde se pone interesante. Aunque esta es una herramienta interna, RLS significa que nuestra base de datos no filtrará accidentalmente datos incluso si cometemos un error en el código de la aplicación.
-- Solo los usuarios autenticados pueden ver acuerdos
alter table deals enable row level security;
create policy "Los usuarios autenticados pueden leer todos los acuerdos"
on deals for select
to authenticated
using (true);
create policy "Los usuarios autenticados pueden insertar acuerdos"
on deals for insert
to authenticated
with check (true);
create policy "Los usuarios autenticados pueden actualizar acuerdos"
on deals for update
to authenticated
using (true);
create policy "Los usuarios autenticados pueden eliminar acuerdos"
on deals for delete
to authenticated
using (true);
Sí, estas políticas son permisivas — cualquier usuario autenticado puede hacer cualquier cosa. Para un equipo de tres personas, está bien. Si alguna vez crecemos a un tamaño donde necesitemos permisos basados en roles, la infraestructura RLS ya está ahí. Solo apretaríamos las políticas.
Implementación y costos de hosting
Aquí está la parte divertida. Hablemos de dinero.
| Servicio | Plan | Costo mensual |
|---|---|---|
| Supabase | Plan gratuito | $0 |
| Vercel (hosting Astro SSR) | Plan Pro (ya lo teníamos) | $0 incremental |
| Dominio | subdominio del dominio existente | $0 |
| Total | $0/mes |
Ya estábamos en el plan Pro de Vercel para proyectos de clientes, así que implementar una aplicación SSR más no nos cuesta nada extra. El plan gratuito de Supabase nos da 500MB de almacenamiento de base de datos, 50,000 usuarios activos mensuales (tenemos 3), y conexiones en tiempo real. Estamos usando quizás el 1% de la capacidad del plan gratuito.
Compáralo con Monday.com:
| Monday.com | Nuestro CRM personalizado | |
|---|---|---|
| Costo mensual | $48 (3 asientos, Pro) | $0 |
| Costo anual | $576 | $0 |
| Tiempo de construcción | 0 horas | ~20 horas |
| Mantenimiento | 0 horas/mes | ~1 hora/mes |
En nuestra tarifa horaria interna, 20 horas de tiempo de desarrollo vale mucho más de $576/año. Pero esa matemática pierde el punto. Construimos esto en parte porque queríamos, en parte porque es una herramienta mejor para nuestro flujo de trabajo específico, y en parte porque esas 20 horas nos enseñaron cosas que hemos usado desde entonces en proyectos de clientes. Hemos aplicado arquitecturas Astro + Supabase similares para aplicaciones respaldadas por CMS headless que construimos para clientes.
Lo que haríamos diferente
Ha pasado aproximadamente cuatro meses desde que lanzamos v1. Aquí está lo que cambiaría:
Usar Zustand desde el primer día
Comenzamos con el useState integrado de React y useContext para la gestión de estado. Para el momento en que agregamos sincronización en tiempo real, actualizaciones optimistas y lógica de reversión, el código de gestión de estado estaba enredado. Migramos a Zustand después de dos semanas. Debería haber comenzado allí.
Agregar búsqueda antes
No construimos búsqueda hasta la semana tres, y esas tres semanas de escanear manualmente columnas para encontrar un acuerdo específico fueron molestas. Una consulta simple ilike en Supabase habría tomado 30 minutos para implementar.
Atajos de teclado
Todavía no hemos agregado estos, pero los queremos. Presiona N para crear un nuevo acuerdo, / para buscar, 1-6 para filtrar por etapa. Pequeñas cosas que se acumulan cuando estás en la herramienta varias veces al día.
Mejor vista móvil
El tablero kanban funciona en móvil, técnicamente. Pero seis columnas no caben en la pantalla de un teléfono. Necesitamos una vista de lista para móvil. No lo hemos priorizado porque raramente revisamos el CRM en nuestros teléfonos, pero sería agradable.
Preguntas frecuentes
¿Cuánto tiempo tomó construir el tablero kanban CRM? La primera versión usable tomó alrededor de 20 horas repartidas en un fin de semana y algunas tardes. Eso nos consiguió el tablero kanban, detalles de acuerdo, arrastrar y soltar, y autenticación básica. Probablemente hemos gastado otras 10 horas desde entonces en mejoras como búsqueda, estilos móviles mejores, y correcciones de bugs.
¿Por qué Astro en lugar de Next.js para una aplicación dinámica? La arquitectura de islas de Astro significa que las partes no interactivas de nuestra aplicación (layout, nav, páginas estáticas) envían cero JavaScript. El tablero kanban en sí es una isla React que se hidrata al cargar. Para una herramienta interna donde la superficie interactiva se enfoca en un componente, este es un gran ajuste. Usamos Next.js para proyectos de clientes donde la interactividad está más distribuida entre páginas.
¿El plan gratuito de Supabase es realmente suficiente para un CRM? Para un equipo pequeño, absolutamente. Tenemos quizás 200 acuerdos, 150 contactos, y unos pocos miles de entradas de registro de actividad. Eso son kilobytes de datos. El plan gratuito de Supabase te da 500MB de almacenamiento, que no alcanzaremos en años. El límite de conexiones en tiempo real también es generoso — obtienes hasta 200 conexiones concurrentes en el plan gratuito.
¿Qué hay sobre copias de seguridad?
Supabase incluye copias de seguridad diarias en el plan Pro ($25/mes), pero estamos en el plan gratuito. Ejecutamos un pg_dump semanal a través de un trabajo cron en un VPS de $5/mes que ya teníamos. No es glamoroso, pero funciona. También tenemos un clon de proyecto de Supabase al que podemos restaurar si algo sale mal.
¿Puede este enfoque funcionar para un equipo más grande que 3 personas? Creo que hasta quizás 10-15 personas, esto funcionaría bien con políticas RLS más estrictas y alguna lógica de UI basada en roles. Más allá de eso, comenzarías a querer características como automatizaciones, flujos de trabajo personalizados, e informes que requerirían un esfuerzo de ingeniería serio. En ese punto, una herramienta CRM dedicada tiene más sentido — solo quizás no Monday.com.
¿Cómo funciona el rendimiento de la sincronización en tiempo real? Supabase Realtime usa WebSockets bajo el capó, y para nuestro caso de uso (3 usuarios concurrentes, actualizaciones de baja frecuencia), es esencialmente instantáneo. Medimos la latencia de extremo a extremo desde que un usuario arrastra una tarjeta hasta que otro usuario ve la actualización: típicamente 80-150ms. Eso es más rápido de lo que podemos percibir.
¿Consideraste alternativas de CRM de código abierto como Twenty o Folk? Miramos Twenty (el CRM de código abierto que se lanzó en 2024) y es impresionante. Pero es un CRM completo con muchas más características de las que necesitábamos, y el auto-hosting requiere más infraestructura. Nuestro objetivo fue construir exactamente lo que necesitábamos y nada más. Si Twenty hubiera existido cuando comenzamos y tuviera un modo más simple enfocado en kanban, podríamos haber ido por ese camino.
¿Construirías herramientas internas personalizadas para clientes también? Lo hemos hecho, en realidad. Varios clientes han venido a nosotros después de superar herramientas como Monday.com, Notion, o Airtable para flujos de trabajo específicos. Típicamente construimos estos con Astro o Next.js en el frontend y Supabase o un CMS headless en el backend. Si eso suena como algo que necesitas, deberíamos hablar.