Tu primer motor de ofertas consultaba la base de datos cada segundo. Las ofertas llegaban fuera de orden. Algunas desaparecían completamente. Añadiste un servidor WebSocket separado para la segunda versión—mejor, pero ahora estás gestionando dos infraestructuras. El tercer intento usó Supabase Realtime, y las condiciones de carrera se detuvieron. Sin bucle de polling. Sin servidor de socket independiente. Los triggers de PostgreSQL transmiten actualizaciones de ofertas en el momento en que se confirman, y cada cliente conectado ve el mismo estado dentro de 50 milisegundos. He implementado tres sistemas de subastas en dos años. La versión de Supabase es la única que no tuve que reconstruir después del lanzamiento. Aquí está la arquitectura que la hizo funcionar—y la única función que casi la rompe.

Supabase Realtime se basa en el Write-Ahead Log (WAL) de PostgreSQL y utiliza un servidor basado en Elixir para empujar cambios de base de datos sobre WebSockets a clientes conectados. Para un sistema de subastas, esto significa que cada oferta que llega a tu base de datos se propaga instantáneamente a cada postor que observa esa subasta. Sin polling. Sin infraestructura de pub/sub separada. Tu base de datos es tu sistema de eventos.

Constructemos uno desde cero.

Tabla de contenidos

Descripción General de la Arquitectura

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

Supabase Realtime te proporciona tres primitivos que se mapean perfectamente a los requisitos de subastas:

  • Postgres Changes: Suscribirse a eventos INSERT, UPDATE y DELETE en tus tablas de ofertas y subastas. Cuando alguien hace una oferta, cada suscriptor obtiene los datos de la fila nueva en milisegundos.
  • Broadcast: Enviar mensajes efímeros a participantes del canal. Perfecto para notificaciones "has sido superado" que no necesitan ser persistidas.
  • Presence: Rastrear quién está viendo actualmente una subasta. Esto te permite mostrar "14 postores viendo" en tu interfaz y detectar sesiones fantasma.

El flujo de datos se ve así:

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

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

¿Por Qué No Solo Usar Polling?

Sé que algunos están pensando "¿no puedo solo hacer polling cada 500ms?" Puedes. Pero con 200 postores concurrentes en una sola subasta, son 400 solicitudes por segundo golpeando tu base de datos para una subasta. Multiplica eso por 50 subastas activas y estás en 20,000 consultas por segundo—la mayoría de las cuales 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

Aquí está el esquema que uso. Es deliberadamente simple—puedes extenderlo, pero la estructura central maneja la mayoría de los tipos de subastas.

-- 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 ofertas
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 ofertas 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 identidad de réplica 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 eventos UPDATE y DELETE—no los datos completos de la fila. Para un sistema de subastas, necesitas el payload completo para que los clientes actualicen montos de ofertas sin hacer una consulta separada.

Habilitación de Replicación Realtime

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

BEGIN;
  -- Eliminar 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 subastas activas
CREATE POLICY "Public auction viewing" ON auctions
  FOR SELECT USING (status IN ('active', 'ended', 'sold'));

-- Los usuarios autenticados pueden ver todas las ofertas 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 hacer ofertas
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 Lógica de Ofertas

Aquí es donde ocurre la verdadera magia. La base de datos refuerza toda la lógica de ofertas del lado del servidor—el cliente no puede hacer trampa.

Trigger de Validación de Oferta 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 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 haya terminado
  IF v_auction.ends_at < NOW() THEN
    RAISE EXCEPTION 'Auction has ended';
  END IF;

  -- Validar que el monto de la oferta exceda el actual + 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;

  -- Prevenir auto-superación
  IF v_auction.highest_bidder_id = NEW.user_id THEN
    RAISE EXCEPTION 'You are already the highest bidder';
  END IF;

  -- Actualizar subasta con nueva oferta 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();

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

Broadcast de Notificaciones de Superación

Este trigger se dispara después de una oferta exitosa y envía una notificación efímera al canal de 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;

  -- Transmitir notificación de superación si hubo 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 Cliente con JavaScript

Ahora conectemos el frontend. Mostraré esto con patrones vanilla JavaScript/React—el mismo enfoque funciona si estás construyendo con Next.js u otro framework.

Inicializa 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 // Acelerar para tráfico de subastas
      }
    }
  }
);

Ese parámetro eventsPerSecond importa. En una subasta activa con docenas de ofertas por segundo, no quieres re-renderizar 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 ofertas vía 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 subasta
    .on('postgres_changes', {
      event: 'UPDATE',
      schema: 'public',
      table: 'auctions',
      filter: `id=eq.${auctionId}`
    }, (payload) => {
      callbacks.onAuctionUpdate(payload.new);
    })

    // Escuchar notificaciones broadcast de superación
    .on('broadcast', { event: 'outbid' }, ({ payload }) => {
      callbacks.onOutbid(payload);
    })

    // Rastrear postores activos vía 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') {
        // Rastrear 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 de 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(() => {
    // Cargar 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 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 };
}

Manejo de Condiciones de Carrera y Validación de Ofertas

Las condiciones de carrera son la fuente más grande de bugs en sistemas de subastas. Así es cómo las manejo.

Lado del Servidor: PostgreSQL Hace el Trabajo Pesado

El SELECT ... FOR UPDATE en nuestra función trigger es la primera línea de defensa. Pero hay otro patrón que he comenzado a usar—bloqueos asesores para subastas de 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 desde UUID de subasta
  v_lock_key := ('x' || left(p_auction_id::text, 15))::bit(64)::bigint;
  
  -- Adquirir bloqueo asesor (bloquea ofertas concurrentes en la misma subasta)
  PERFORM pg_advisory_xact_lock(v_lock_key);

  -- Ahora seguro de insertar (trigger maneja 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 RPC de Supabase:

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

Lado del Cliente: UI Optimista con Reversión

Muestra la oferta inmediatamente en la interfaz, pero esté listo 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 oferta real llegará vía Realtime y reemplazará la optimista
  } catch (err) {
    // Eliminar oferta optimista en caso de fallo
    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 crea urgencia. El seguimiento de presencia es muy simple con Supabase:

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

En el lado de visualización, puedes desglosar el estado de presencia para mostrar cuántos están ofertando activamente vs. solo observando:

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

Aceleración y Desaceleración

Una guerra de pujas puede generar docenas de eventos por segundo. Aquí está lo que configuro:

  • Lado del servidor: eventsPerSecond: 20 en la configuración del cliente de Supabase
  • Lado del cliente: Desacelerar el botón de oferta en 300ms para prevenir doble-clics
  • Actualizaciones de interfaz: Usar requestAnimationFrame para animaciones de lista de ofertas

Tiempo de Fin de Subasta

No confíes en el reloj del cliente. Usa un trabajo cron de PostgreSQL vía 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-Snipe

La mayoría de las plataformas de subastas extienden la fecha límite si una oferta llega 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 estos en producción. Aquí hay una comparación honesta:

Feature Supabase Realtime Pusher Ably Firebase RTDB Socket.io (auto-alojado)
Sincronización nativa de BD ✅ PostgreSQL WAL ❌ Servicio separado ❌ Servicio separado ✅ Árbol JSON ❌ Manual
Latencia (p99) ~80-100ms ~60ms ~50ms ~100ms ~40ms (depende de infra)
Máx eventos/seg 200k+ 10k (Pro) 50k 100k Ilimitado (escalas tú)
Integración de autenticación Integrada (RLS + JWT) Personalizada Basada en token Firebase Auth Personalizada
Presence ✅ Integrada ✅ Integrada ✅ Integrada ✅ Integrada ✅ Integrada
Tier gratuito 500K MAU, 200 concurrentes 100 conexiones 6M msgs/mes 1GB almacenado $0 (costos de alojamiento)
Precio Pro $25/mes $49/mes $29/mes Pay-as-you-go ~$100-500/mes (AWS)
Mejor para Apps en tiempo real centradas en BD Pub/sub simple Alta confiabilidad Apps móviles Control total

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

Si estás construyendo en una arquitectura de CMS headless, Supabase encaja naturalmente junto a la entrega de contenido sin agregar otro servicio a gestionar.

Implementación y Escalado de tu Sistema de Subastas

Para la mayoría de proyectos, el tier Pro gestionado de Supabase a $25/mes maneja hasta 10,000 subastas diarias cómodamente. Aquí está lo que debes vigilar:

  • Límites de conexión: El tier Pro te da 500 conexiones concurrentes de Realtime. Si necesitas más, tendrás que actualizar o implementar agrupación de conexiones en el cliente.
  • Tamaño WAL: El bidding de alto volumen genera tráfico WAL significativo. Monitorea tu ranura de replicación para evitar hinchazón de disco.
  • Conteo de canales: Cada subasta obtiene su propio canal. Con miles de subastas activas, prueba que tu cliente se desuscriba correctamente de las subastas terminadas.

Para un frontend construido con Astro o Next.js, el cliente JS de Supabase funciona de forma idéntica—solo asegúrate de que lo estés inicializando del lado del cliente para suscripciones de Realtime.

Si estás construyendo algo que necesita manejar escala seria—cientos de miles de postores concurrentes—contáctanos. Hemos diseñado estos sistemas a escala y podemos ayudarte a evitar las trampas. También puedes revisar nuestra página de precios para compromisos basados en proyectos.

Preguntas Frecuentes

¿Cuántos postores concurrentes puede manejar Supabase Realtime?
Supabase Realtime puede manejar más de 200,000 eventos por segundo en servidores distribuidos en su plataforma gestionada. El tier Pro a $25/mes soporta hasta 500 conexiones concurrentes por proyecto. Para subastas más grandes, el tier Enterprise ofrece límites personalizados, o puedes auto-alojar el servidor de Realtime (es 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 end-to-end desde inserción de oferta hasta notificación de cliente promedia alrededor de 50-80ms, con p99 bajo 100ms. Para contexto, el tiempo de reacción humano es aproximadamente 200-300ms, así que las ofertas aparecen efectivamente instantáneas. El cuello de botella rara vez es Supabase—usualmente es la conexión de red del cliente.

¿Cómo prevengo condiciones de carrera cuando dos personas hacen ofertas simultáneamente?
Usa el bloqueo a nivel de fila SELECT ... FOR UPDATE de PostgreSQL dentro de una función trigger, o usa bloqueos asesores vía pg_advisory_xact_lock(). Esto serializa el procesamiento de oferta por subasta para que solo una oferta sea validada a la vez. La oferta "perdedora" aún se valida—solo que ve la oferta actualizada más alta del ganador y o bien tiene éxito (si aún es más alta) o falla con un error apropiado.

¿Puedo usar Supabase Realtime con Next.js o Astro?
Absolutamente. 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é sucede si la conexión de un usuario se cae a mitad de una subasta?
Supabase Realtime intenta automáticamente reconectarse. Cuando el cliente se reconecta y se resuscribe, recibe el estado actual. Para subastas críticas, recomiendo también buscar el estado de subasta más reciente vía una consulta estándar al reconectarse para asegurar que nada se perdió durante la ventana de desconexión. El sistema de Presence eliminará automáticamente al usuario desconectado después de un tiempo de espera.

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

¿Es Supabase Realtime gratuito para proyectos pequeños?
El tier gratuito de Supabase incluye Realtime con hasta 200 conexiones concurrentes y 500,000 usuarios activos mensuales. Eso es suficiente para un sitio de subasta aficionado o un MVP. Si estás ejecutando una plataforma de subastas en producción con tráfico significativo, el tier Pro a $25/mes con $0.09/GB de egreso es donde querrás comenzar. Es significativamente más barato que ejecutar tu propia infraestructura de WebSocket.

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