He construido tres sistemas de subasta en los últimos dos años. El primero fue un desastre: consultando la base de datos cada segundo, condiciones de carrera por todas partes, pujas desapareciendo en el vacío. El segundo era mejor, pero requería gestionar un servidor WebSocket separado junto a la API principal. ¿El tercero? Ese es el que voy a explicarte. Usa Supabase Realtime, y es la primera vez que construir un motor de pujas se siente correcto.

Supabase Realtime se asienta sobre el Write-Ahead Log (WAL) de PostgreSQL y utiliza un servidor basado en Elixir para enviar los cambios de la base de datos por WebSockets a los clientes conectados. Para un sistema de subastas, esto significa que cada puja que llega a tu base de datos se propaga instantáneamente a todos los postores que están viendo esa subasta. Sin polling. Sin infraestructura pub/sub separada. Tu base de datos es tu sistema de eventos.

Vamos a construir uno desde cero.

Tabla de contenidos

Visión general de la arquitectura

Antes de escribir ningún código, entendamos qué estamos construyendo y cómo encajan las piezas.

Supabase Realtime te ofrece tres primitivas que se adaptan perfectamente a los requisitos de una subasta:

  • Postgres Changes: Suscríbete a eventos INSERT, UPDATE y DELETE en tus tablas de pujas y subastas. Cuando alguien realiza una puja, cada suscriptor recibe los datos de la nueva fila en milisegundos.
  • Broadcast: Envía mensajes efímeros a los participantes del canal. Perfecto para notificaciones del tipo "te han superado en la puja" que no necesitan persistirse.
  • Presence: Rastrea quién está viendo una subasta en este momento. Esto te permite mostrar "14 postores mirando" en tu interfaz y detectar sesiones fantasma.

El flujo de datos es el siguiente:

  1. El postor envía una puja desde tu frontend
  2. Una llamada RPC o una inserción directa llega a tu tabla bids
  3. Un trigger de PostgreSQL valida el importe de la puja y actualiza auctions.current_high_bid
  4. Supabase Realtime recoge el cambio del WAL y lo envía a todos los suscriptores en el canal de esa subasta
  5. Un segundo trigger lanza un evento Broadcast para notificar al anterior postor más alto que ha sido superado
  6. Cada cliente conectado actualiza su interfaz en tiempo real

La latencia desde la colocación de la puja hasta la actualización de la interfaz en todos los clientes es típicamente inferior a 100 ms. He medido el p99 en torno a 80-90 ms en producción en el nivel Pro de Supabase.

¿Por qué no usar simplemente polling?

Sé que algunos estáis pensando "¿no puedo simplemente hacer polling cada 500 ms?". Podéis. Pero con 200 postores concurrentes en una sola subasta, eso supone 400 solicitudes por segundo llegando a tu base de datos para una única subasta. Multiplica eso por 50 subastas activas y estarás en 20.000 consultas por segundo, la mayoría de las cuales no devuelven nada nuevo. Los WebSockets invierten este modelo: cero consultas cuando nada cambia, actualizaciones instantáneas cuando algo lo hace.

Esquema de base de datos y configuración

Este es el esquema que utilizo. Es deliberadamente simple — puedes ampliarlo, pero la estructura básica gestiona la mayoría de los tipos de subasta.

-- Tabla de subastas
CREATE TABLE auctions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  item_name TEXT NOT NULL,
  description TEXT,
  starting_price DECIMAL(12,2) NOT NULL DEFAULT 0,
  current_high_bid DECIMAL(12,2) DEFAULT 0,
  highest_bidder_id UUID REFERENCES auth.users(id),
  min_increment DECIMAL(12,2) DEFAULT 1.00,
  status TEXT NOT NULL DEFAULT 'active'
    CHECK (status IN ('scheduled', 'active', 'ended', 'sold', 'cancelled')),
  starts_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  ends_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '30 minutes',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Tabla de pujas
CREATE TABLE bids (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  auction_id UUID NOT NULL REFERENCES auctions(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES auth.users(id),
  amount DECIMAL(12,2) NOT NULL,
  placed_at TIMESTAMPTZ DEFAULT NOW(),
  CONSTRAINT positive_amount CHECK (amount > 0)
);

-- Índice para búsquedas rápidas de pujas por subasta
CREATE INDEX idx_bids_auction_amount ON bids(auction_id, amount DESC);
CREATE INDEX idx_bids_auction_time ON bids(auction_id, placed_at DESC);

-- Crítico: habilitar replica identity para Realtime
ALTER TABLE auctions REPLICA IDENTITY FULL;
ALTER TABLE bids REPLICA IDENTITY FULL;

La configuración REPLICA IDENTITY FULL es esencial. Sin ella, Supabase Realtime solo obtiene la clave primaria en los eventos UPDATE y DELETE, no los datos completos de la fila. Para un sistema de subastas, necesitas el payload completo para que los clientes puedan actualizar los importes de las pujas sin realizar una consulta adicional.

Habilitando la replicación de Realtime

En el panel de control de Supabase, ve a Database → Replication y activa la replicación para las tablas auctions y bids. Alternativamente, puedes hacerlo con SQL:

BEGIN;
  -- Eliminar la publicación existente si existe
  DROP PUBLICATION IF EXISTS supabase_realtime;
  
  -- Crear publicación con ambas tablas
  CREATE PUBLICATION supabase_realtime FOR TABLE auctions, bids;
COMMIT;

Seguridad a nivel de fila

No te saltes esto. RLS es tu capa de validación del lado del servidor.

ALTER TABLE auctions ENABLE ROW LEVEL SECURITY;
ALTER TABLE bids ENABLE ROW LEVEL SECURITY;

-- Cualquiera puede ver las subastas activas
CREATE POLICY "Public auction viewing" ON auctions
  FOR SELECT USING (status IN ('active', 'ended', 'sold'));

-- Los usuarios autenticados pueden ver todas las pujas en subastas activas
CREATE POLICY "View bids on active auctions" ON bids
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM auctions
      WHERE auctions.id = bids.auction_id
      AND auctions.status = 'active'
    )
  );

-- Solo los usuarios autenticados pueden realizar pujas
CREATE POLICY "Place bids" ON bids
  FOR INSERT WITH CHECK (
    auth.uid() = user_id
    AND EXISTS (
      SELECT 1 FROM auctions
      WHERE auctions.id = auction_id
      AND auctions.status = 'active'
      AND auctions.ends_at > NOW()
    )
  );

Triggers de PostgreSQL para la lógica de pujas

Aquí es donde ocurre la verdadera magia. La base de datos aplica toda la lógica de pujas en el servidor: el cliente no puede hacer trampas.

Trigger de validación de pujas y actualización de subasta

CREATE OR REPLACE FUNCTION process_new_bid()
RETURNS TRIGGER AS $$
DECLARE
  v_auction auctions%ROWTYPE;
BEGIN
  -- Bloquear la fila de la subasta para prevenir condiciones de carrera
  SELECT * INTO v_auction
  FROM auctions
  WHERE id = NEW.auction_id
  FOR UPDATE;

  -- Validar que la subasta está activa
  IF v_auction.status != 'active' THEN
    RAISE EXCEPTION 'Auction is not active';
  END IF;

  -- Validar que la subasta no ha finalizado
  IF v_auction.ends_at < NOW() THEN
    RAISE EXCEPTION 'Auction has ended';
  END IF;

  -- Validar que el importe de la puja supera el máximo actual más el incremento mínimo
  IF NEW.amount < v_auction.current_high_bid + v_auction.min_increment THEN
    RAISE EXCEPTION 'Bid must be at least % higher than current high bid of %',
      v_auction.min_increment, v_auction.current_high_bid;
  END IF;

  -- Evitar que un usuario se supere a sí mismo
  IF v_auction.highest_bidder_id = NEW.user_id THEN
    RAISE EXCEPTION 'You are already the highest bidder';
  END IF;

  -- Actualizar la subasta con la nueva puja más alta
  UPDATE auctions
  SET
    current_high_bid = NEW.amount,
    highest_bidder_id = NEW.user_id,
    updated_at = NOW()
  WHERE id = NEW.auction_id;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER validate_and_process_bid
  BEFORE INSERT ON bids
  FOR EACH ROW
  EXECUTE FUNCTION process_new_bid();

El bloqueo FOR UPDATE en la fila de la subasta es crítico. Sin él, dos pujas que lleguen simultáneamente podrían leer el mismo current_high_bid, ambas pasar la validación e insertarse. El bloqueo serializa el acceso.

Notificaciones de puja superada por Broadcast

Este trigger se dispara después de una puja exitosa y envía una notificación efímera al canal de la subasta:

CREATE OR REPLACE FUNCTION notify_outbid()
RETURNS TRIGGER AS $$
DECLARE
  v_previous_bidder UUID;
BEGIN
  -- Encontrar quién acaba de ser superado
  SELECT user_id INTO v_previous_bidder
  FROM bids
  WHERE auction_id = NEW.auction_id
    AND id != NEW.id
  ORDER BY amount DESC
  LIMIT 1;

  -- Emitir notificación de puja superada si había un postor anterior
  IF v_previous_bidder IS NOT NULL THEN
    PERFORM realtime.send(
      jsonb_build_object(
        'auction_id', NEW.auction_id,
        'new_high', NEW.amount,
        'outbid_user', v_previous_bidder,
        'new_leader', NEW.user_id
      ),
      'outbid',
      'auction:' || NEW.auction_id::text,
      true
    );
  END IF;

  RETURN NULL;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER after_bid_notify
  AFTER INSERT ON bids
  FOR EACH ROW
  EXECUTE FUNCTION notify_outbid();

Suscripción del lado del cliente con JavaScript

Ahora conectemos el frontend. Lo mostraré con patrones de JavaScript/React puro — el mismo enfoque funciona si estás construyendo con Next.js o cualquier otro framework.

Inicializar el cliente

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
  {
    realtime: {
      params: {
        eventsPerSecond: 20 // Limitar para el tráfico de subastas
      }
    }
  }
);

El parámetro eventsPerSecond importa. En una subasta muy activa con docenas de pujas por segundo, no querrás rerenderizar 50 veces por segundo. Veinte actualizaciones por segundo es más que suficiente para una interfaz fluida.

Suscribirse a un canal de subasta

function subscribeToAuction(auctionId, callbacks) {
  const channel = supabase.channel(`auction:${auctionId}`);

  channel
    // Escuchar nuevas pujas mediante Postgres Changes
    .on('postgres_changes', {
      event: 'INSERT',
      schema: 'public',
      table: 'bids',
      filter: `auction_id=eq.${auctionId}`
    }, (payload) => {
      callbacks.onNewBid(payload.new);
    })

    // Escuchar cambios de estado de la subasta
    .on('postgres_changes', {
      event: 'UPDATE',
      schema: 'public',
      table: 'auctions',
      filter: `id=eq.${auctionId}`
    }, (payload) => {
      callbacks.onAuctionUpdate(payload.new);
    })

    // Escuchar notificaciones de puja superada por broadcast
    .on('broadcast', { event: 'outbid' }, ({ payload }) => {
      callbacks.onOutbid(payload);
    })

    // Rastrear postores activos mediante Presence
    .on('presence', { event: 'sync' }, () => {
      const state = channel.presenceState();
      const bidderCount = Object.keys(state).length;
      callbacks.onPresenceUpdate(bidderCount, state);
    })

    .subscribe(async (status) => {
      if (status === 'SUBSCRIBED') {
        // Registrar la presencia de este usuario
        await channel.track({
          user_id: supabase.auth.getUser()?.data?.user?.id,
          status: 'watching',
          joined_at: new Date().toISOString()
        });
      }
    });

  return channel;
}

Hook de React para suscripciones a subastas

import { useState, useEffect, useCallback } from 'react';

function useAuction(auctionId) {
  const [auction, setAuction] = useState(null);
  const [bids, setBids] = useState([]);
  const [bidderCount, setBidderCount] = useState(0);
  const [isOutbid, setIsOutbid] = useState(false);

  useEffect(() => {
    // Obtener el estado inicial
    async function loadAuction() {
      const { data: auctionData } = await supabase
        .from('auctions')
        .select('*')
        .eq('id', auctionId)
        .single();
      setAuction(auctionData);

      const { data: bidData } = await supabase
        .from('bids')
        .select('*')
        .eq('auction_id', auctionId)
        .order('amount', { ascending: false })
        .limit(20);
      setBids(bidData || []);
    }
    loadAuction();

    // Suscribirse a las actualizaciones en tiempo real
    const channel = subscribeToAuction(auctionId, {
      onNewBid: (bid) => {
        setBids(prev => [bid, ...prev].slice(0, 20));
        setIsOutbid(false);
      },
      onAuctionUpdate: (updated) => setAuction(updated),
      onOutbid: (payload) => {
        const currentUser = supabase.auth.getUser()?.data?.user;
        if (payload.outbid_user === currentUser?.id) {
          setIsOutbid(true);
        }
      },
      onPresenceUpdate: (count) => setBidderCount(count)
    });

    return () => {
      supabase.removeChannel(channel);
    };
  }, [auctionId]);

  const placeBid = useCallback(async (amount) => {
    const user = (await supabase.auth.getUser()).data.user;
    const { data, error } = await supabase
      .from('bids')
      .insert({
        auction_id: auctionId,
        amount: parseFloat(amount),
        user_id: user.id
      })
      .select()
      .single();

    if (error) throw new Error(error.message);
    return data;
  }, [auctionId]);

  return { auction, bids, bidderCount, isOutbid, placeBid };
}

Gestión de condiciones de carrera y validación de pujas

Las condiciones de carrera son la mayor fuente de errores en los sistemas de subastas. Así es como las gestiono.

En el servidor: PostgreSQL hace el trabajo pesado

El SELECT ... FOR UPDATE en nuestra función de trigger es la primera línea de defensa. Pero hay otro patrón que he empezado a usar: bloqueos de aviso para subastas con alta contención:

CREATE OR REPLACE FUNCTION place_bid_safe(
  p_auction_id UUID,
  p_user_id UUID,
  p_amount DECIMAL
)
RETURNS TABLE(bid_id UUID, new_high DECIMAL) AS $$
DECLARE
  v_lock_key BIGINT;
  v_bid_id UUID;
BEGIN
  -- Generar una clave de bloqueo determinista a partir del UUID de la subasta
  v_lock_key := ('x' || left(p_auction_id::text, 15))::bit(64)::bigint;
  
  -- Adquirir bloqueo de aviso (bloquea pujas concurrentes en la misma subasta)
  PERFORM pg_advisory_xact_lock(v_lock_key);

  -- Ahora es seguro insertar (el trigger gestiona la validación)
  INSERT INTO bids (auction_id, user_id, amount)
  VALUES (p_auction_id, p_user_id, p_amount)
  RETURNING id INTO v_bid_id;

  RETURN QUERY
  SELECT v_bid_id, p_amount;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Llama a esto desde el cliente usando el RPC de Supabase:

const { data, error } = await supabase.rpc('place_bid_safe', {
  p_auction_id: auctionId,
  p_user_id: user.id,
  p_amount: bidAmount
});

En el cliente: interfaz optimista con reversión

Muestra la puja inmediatamente en la interfaz, pero prepárate para revertirla si el servidor la rechaza:

async function handleBidSubmit(amount) {
  const optimisticBid = {
    id: crypto.randomUUID(),
    amount,
    user_id: user.id,
    placed_at: new Date().toISOString(),
    _optimistic: true
  };

  // Mostrar inmediatamente
  setBids(prev => [optimisticBid, ...prev]);

  try {
    await placeBid(amount);
    // La puja real llegará vía Realtime y reemplazará la optimista
  } catch (err) {
    // Eliminar la puja optimista si falla
    setBids(prev => prev.filter(b => b.id !== optimisticBid.id));
    showError(err.message);
  }
}

Seguimiento de presencia para postores activos

Mostrar cuántas personas están viendo una subasta genera urgencia. El seguimiento de presencia es muy sencillo con Supabase:

// Actualizar el estado del usuario cuando empieza a pujar
async function updatePresenceStatus(channel, status) {
  await channel.track({
    user_id: user.id,
    status, // 'watching', 'bidding', 'won'
    last_active: new Date().toISOString()
  });
}

En la parte de visualización, puedes desglosar el estado de presencia para mostrar cuántos están pujando activamente frente a los que solo están mirando:

function parseBidderStats(presenceState) {
  const users = Object.values(presenceState).flat();
  return {
    total: users.length,
    bidding: users.filter(u => u.status === 'bidding').length,
    watching: users.filter(u => u.status === 'watching').length
  };
}

Ajuste de rendimiento y consideraciones de producción

Limitación y antirrebote

Una guerra de pujas puede generar docenas de eventos por segundo. Esto es lo que configuro:

  • En el servidor: eventsPerSecond: 20 en la configuración del cliente de Supabase
  • En el cliente: Antirrebote del botón de puja a 300 ms para evitar dobles clics
  • Actualizaciones de interfaz: Usa requestAnimationFrame para las animaciones de la lista de pujas

Tiempo de finalización de la subasta

No confíes en el reloj del cliente. Usa un trabajo cron de PostgreSQL mediante pg_cron:

-- Ejecutar cada 10 segundos para cerrar subastas expiradas
SELECT cron.schedule(
  'close-expired-auctions',
  '*/10 * * * * *',
  $$
  UPDATE auctions
  SET status = CASE
    WHEN highest_bidder_id IS NOT NULL THEN 'sold'
    ELSE 'ended'
  END
  WHERE status = 'active'
  AND ends_at <= NOW();
  $$
);

Extensión anti-sniping

La mayoría de las plataformas de subasta extienden el plazo si llega una puja durante los últimos segundos:

-- Añadir al trigger process_new_bid
IF v_auction.ends_at - NOW() < INTERVAL '30 seconds' THEN
  UPDATE auctions
  SET ends_at = ends_at + INTERVAL '30 seconds'
  WHERE id = NEW.auction_id;
END IF;

Supabase Realtime vs alternativas

He usado la mayoría de estas en producción. Aquí tienes una comparación honesta:

Característica Supabase Realtime Pusher Ably Firebase RTDB Socket.io (autoalojado)
Sincronización nativa con BD ✅ PostgreSQL WAL ❌ Servicio separado ❌ Servicio separado ✅ Árbol JSON ❌ Manual
Latencia (p99) ~80-100 ms ~60 ms ~50 ms ~100 ms ~40 ms (depende de la infraestructura)
Máx. eventos/seg 200k+ 10k (Pro) 50k 100k Ilimitado (tú lo escalas)
Integración de autenticación Integrada (RLS + JWT) Personalizada Basada en token Firebase Auth Personalizada
Presence ✅ Integrado ✅ Integrado ✅ Integrado ✅ Integrado ✅ Integrado
Nivel gratuito 500K MAU, 200 concurrentes 100 conexiones 6M msgs/mes 1GB almacenado $0 (costes de alojamiento)
Precio Pro $25/mes $49/mes $29/mes Pago por uso ~$100-500/mes (AWS)
Ideal para Aplicaciones en tiempo real centradas en BD Pub/sub simple Alta fiabilidad Aplicaciones móviles Control total

Para un sistema de subastas específicamente, Supabase gana porque tus pujas ya están en PostgreSQL. No necesitas sincronizar entre una base de datos y un sistema pub/sub separado. La puja llega a la BD, la BD dispara el push por WebSocket. Una única fuente de verdad.

Si estás construyendo sobre una arquitectura headless CMS, Supabase encaja de forma natural junto a la entrega de contenido sin añadir otro servicio que gestionar.

Despliegue y escalado de tu sistema de subastas

Para la mayoría de los proyectos, el nivel Pro gestionado de Supabase a $25/mes gestiona cómodamente hasta 10.000 subastas diarias. Esto es lo que hay que vigilar:

  • Límites de conexión: El nivel Pro te da 500 conexiones Realtime concurrentes. Si necesitas más, tendrás que actualizar o implementar pooling de conexiones en el cliente.
  • Tamaño del WAL: Un alto volumen de pujas genera un tráfico WAL significativo. Monitoriza tu slot de replicación para evitar el crecimiento excesivo del disco.
  • Número de canales: Cada subasta tiene su propio canal. Con miles de subastas activas, comprueba que tu cliente cancela correctamente la suscripción de las subastas finalizadas.

Para un frontend construido con Astro o Next.js, el cliente JS de Supabase funciona de forma idéntica — solo asegúrate de inicializarlo en el lado del cliente para las suscripciones Realtime.

Si estás construyendo algo que necesita gestionar una escala seria — cientos de miles de postores concurrentes — contáctanos. Hemos diseñado estos sistemas a escala y podemos ayudarte a evitar los errores más comunes. También puedes consultar nuestra página de precios para compromisos basados en proyectos.

Preguntas frecuentes

¿Cuántos postores concurrentes puede gestionar Supabase Realtime? Supabase Realtime puede gestionar más de 200.000 eventos por segundo en servidores distribuidos en su plataforma gestionada. El nivel Pro a $25/mes admite hasta 500 conexiones concurrentes por proyecto. Para subastas más grandes, el nivel Enterprise ofrece límites personalizados, o puedes autoalojar el servidor Realtime (es de código abierto) en tu propia infraestructura.

¿Es Supabase Realtime lo suficientemente rápido para una subasta en vivo? Sí. En mis pruebas, la latencia de extremo a extremo desde la inserción de la puja hasta la notificación al cliente promedia alrededor de 50-80 ms, con p99 por debajo de 100 ms. Para contexto, el tiempo de reacción humano es de aproximadamente 200-300 ms, por lo que las pujas aparecen de forma prácticamente instantánea. El cuello de botella rara vez es Supabase — normalmente es la conexión de red del cliente.

¿Cómo evito las condiciones de carrera cuando dos personas pujan simultáneamente? Usa el bloqueo a nivel de fila SELECT ... FOR UPDATE de PostgreSQL dentro de una función de trigger, o usa bloqueos de aviso mediante pg_advisory_xact_lock(). Esto serializa el procesamiento de pujas por subasta para que solo se valide una puja a la vez. La puja "perdedora" también se valida — simplemente ve el high bid actualizado del ganador y o bien tiene éxito (si sigue siendo más alta) o falla con un error apropiado.

¿Puedo usar Supabase Realtime con Next.js o Astro? Por supuesto. El cliente @supabase/supabase-js funciona en cualquier entorno JavaScript. Para Next.js, inicializa el cliente de Supabase en un componente cliente (ya que Realtime necesita WebSockets del navegador) y úsalo dentro de hooks useEffect. Para Astro, úsalo en islas interactivas del lado del cliente. El código de suscripción es idéntico independientemente de tu elección de framework.

¿Qué ocurre si la conexión de un usuario se interrumpe en mitad de una subasta? Supabase Realtime intenta reconectarse automáticamente. Cuando el cliente se reconecta y se vuelve a suscribir, recibe el estado actual. Para subastas críticas, recomiendo también obtener el estado más reciente de la subasta mediante una consulta estándar al reconectarse para asegurar que no se perdió nada durante la ventana de desconexión. El sistema Presence eliminará automáticamente al usuario desconectado tras un tiempo de espera.

¿Cómo gestiono los tiempos de finalización de la subasta con precisión? Nunca confíes en los temporizadores del lado del cliente para los tiempos de finalización de la subasta — pueden manipularse. Usa la extensión pg_cron de PostgreSQL para verificar y cerrar las subastas expiradas cada 10 segundos en el servidor. Envía la marca de tiempo del servidor a los clientes para que puedan mostrar una cuenta atrás, pero la determinación real del fin siempre ocurre en la base de datos.

¿Es Supabase Realtime gratuito para proyectos pequeños? El nivel gratuito de Supabase incluye Realtime con hasta 200 conexiones concurrentes y 500.000 usuarios activos mensuales. Eso es suficiente para un sitio de subastas hobby o un MVP. Si gestionas una plataforma de subastas en producción con tráfico significativo, el nivel Pro a $25/mes con $0,09/GB de salida es donde querrás empezar. Es significativamente más barato que gestionar tu propia infraestructura WebSocket.

¿Cómo pruebo un sistema de pujas en tiempo real localmente? Usa la CLI de Supabase (supabase start) para ejecutar una instancia local de Supabase con Realtime habilitado. Abre múltiples pestañas del navegador para simular múltiples postores. Para pruebas de carga, uso un script simple de Node.js que crea más de 100 clientes de Supabase y los hace pujar entre sí con un temporizador. Esto detecta condiciones de carrera y te ayuda a ajustar el parámetro eventsPerSecond antes de pasar a producción.