Logotipo de Bitdoze

Crea una app en tiempo real con Astro y Convex (Tutorial 2026)

Crea una app en tiempo real con Astro y Convex en este tutorial 2026. Construye un chat en vivo usando islas de Astro, consultas en tiempo real y despliegue en Vercel.

Dragos

Actualizado Publicado 49 min de lectura

Ilustración de una interfaz de chat en tiempo real construida con Astro y Convex

En este tutorial construirás una app en tiempo real con Convex y Astro: una aplicación de chat que carga al instante y se actualiza en vivo para todos los usuarios conectados en el momento en que se envía un mensaje. Usarás las islas de Astro para la estructura estática y las consultas en tiempo real de Convex para las partes interactivas. Al final tendrás una app de chat funcional lista para desplegar en Vercel.

Esta es la edición 2026, actualizada para Astro 7.2, Convex 1.45, React 19 y Tailwind v4. Si te preguntas cómo se compara Astro con Next.js y TanStack Start, la respuesta corta es: Astro no envía JavaScript por defecto y te permite hidratar solo los componentes que necesitan interactividad. ¿Nuevo en Astro? Puedes crear un blog gratis con Astro primero para familiarizarte con lo básico.


Por qué Astro + Convex para apps en tiempo real

Astro no envía JS por defecto

Astro envía HTML con JavaScript solo donde lo necesitas, a diferencia de los frameworks que mandan megabytes de JavaScript. Astro 7 incorpora un compilador en Rust que hace las compilaciones entre un 15 y un 61 % más rápidas. Consulta los benchmarks de rendimiento de compilación de Astro 7 para más detalles.

  • Cero JavaScript por defecto - Las páginas cargan con HTML/CSS puro
  • Arquitectura de islas - Los componentes interactivos se hidratan de forma independiente
  • Agnóstico de frameworks - Usa componentes de React, Vue, Svelte o JavaScript puro
  • Optimizaciones integradas - Optimización de imágenes, empaquetado de CSS y más, listo para usar

Convex: el tiempo real como primitiva de primera clase

Convex se encarga del desarrollo backend con el tiempo real como primitiva de primera clase:

  • Tiempo real por defecto - Las consultas se convierten en suscripciones en vivo
  • Consistencia fuerte - Se acabaron las condiciones de carrera y los datos inconsistentes
  • TypeScript en todas partes - Seguridad de tipos de extremo a extremo, de la base de datos al frontend
  • Serverless y escalable - Cero gestión de infraestructura
  • Transacciones ACID - Tus datos se mantienen consistentes incluso bajo carga pesada

Además, Convex ahora es open source. Puedes autoalojar Convex con Docker Compose si quieres control total sobre tus datos.


Qué vamos a construir hoy

Vamos a construir una aplicación de chat en tiempo real:

  • Carga inicial rápida con Astro
  • Actualizaciones de mensajes en tiempo real para todos los usuarios conectados
  • API con seguridad de tipos de la base de datos al frontend
  • Desplegable en Vercel, cubierto en la parte 2: despliega tu app Astro + Convex en Vercel
  • UI responsive con Tailwind CSS

Qué ha cambiado en 2026

Si vienes de la versión original de 2025 de este tutorial, esto es lo nuevo:

  • Astro 7.2.x: compilador en Rust para archivos .astro, Vite 8 con Rolldown por debajo, validación de HTML más estricta (las etiquetas sin cerrar ahora dan error en lugar de autocorregirse) y compressHTML: 'jsx' como nuevo valor por defecto para los espacios en blanco.
  • Convex 1.45.x: gestión de despliegues desde la CLI (npx convex deployment create / select), hosting en la UE (región de Irlanda, lanzada en febrero de 2026) y Convex ahora es open source.
  • React 19 + Tailwind v4: ambos son los valores por defecto al crear proyectos hoy.
  • Node.js 22.12.0+: requerido tanto por Astro 6+ como por Convex.

Cambio importante en Astro 7

El compilador Rust de Astro 7 es más estricto que el antiguo compilador JS. Si tu build falla de repente con errores de HTML, revisa si hay etiquetas sin cerrar o HTML inválido en tus plantillas .astro. Además, Astro 7 cambió el manejo por defecto de los espacios en blanco a compressHTML: 'jsx'. Si notas que faltan espacios entre elementos en línea, añade separadores explícitos {" "}.

Nada de lo que usa este tutorial emplea APIs eliminadas. Los patrones de código no han cambiado, solo las herramientas son más nuevas.


Requisitos previos y configuración

Antes de empezar, asegúrate de tener:

  • Node.js 22.12.0+ (LTS) instalado — compruébalo con node --version
  • Git instalado
  • Una cuenta de GitHub (para la autenticación de Convex)
  • Unos 45 minutos de tiempo de programación concentrado

¿Nuevo en TypeScript?

El TypeScript de este tutorial es sencillo, y la seguridad de tipos de Convex detecta los errores en tiempo de compilación en lugar de en ejecución.

¿Prefieres una plantilla?

Puedes generar todo este tutorial al instante con npx create-convex@latest -t astro. Configura el proyecto Astro + Convex con los mismos patrones que construiremos manualmente aquí. Consulta la plantilla de Convex para Astro para más detalles.


Paso 1: Crea tu proyecto Astro

Crea un nuevo proyecto Astro:

bash
# Create a new Astro project
npm create astro@latest astro-convex-chat

# When prompted, choose:
# - "Empty" / minimal template
# - Yes to TypeScript
# - Yes to install dependencies
# - Yes to initialize git repository

# Navigate to your project
cd astro-convex-chat

Ahora añadamos la integración de React para nuestros componentes interactivos:

bash
# Add React support to Astro
npx astro add react

# Add Tailwind CSS for styling
npx astro add tailwind

# Install additional utilities we'll need
npm install npm-run-all clsx

Consejo profesional

Los comandos npx astro add configuran los tipos de TypeScript y los ajustes de compilación automáticamente.

Verifica: Ejecuta ls — deberías ver src/, public/, astro.config.mjs y package.json.


Paso 2: Instala y configura Convex

Añade Convex al proyecto:

bash
# Install Convex
npm install convex

# Initialize Convex (this will prompt you to sign in with GitHub)
npx convex dev

Durante el proceso de configuración de Convex:

  1. Inicia sesión con GitHub — Convex usa GitHub para la autenticación
  2. Crea un nuevo proyecto — Ponle un nombre como “astro-chat-app”
  3. Acepta la configuración por defecto — Convex creará una carpeta convex/

Esto crea varios archivos importantes:

  • convex/ carpeta — Donde viven tus funciones de backend
  • .env.local — Contiene la URL de tu despliegue de Convex
  • convex/_generated/ — Tipos de TypeScript autogenerados

Mantén convex dev en ejecución

Asegúrate de mantener el comando npx convex dev en ejecución durante todo el desarrollo. Vigila tus funciones de backend y mantiene todo sincronizado. Si lo detienes, los tipos de convex/_generated/ quedarán desactualizados y tu build fallará.

Verifica: ls convex/ debería mostrar una carpeta _generated/. .env.local debería contener un valor CONVEX_URL.


Paso 3: Diseña el esquema de tu base de datos

Convex usa un enfoque de esquema primero para la seguridad de tipos. Define la estructura de datos de la app de chat en convex/schema.ts:

typescript
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  // Users table to store user information
  users: defineTable({
    name: v.string(),
    email: v.optional(v.string()),
    avatar: v.optional(v.string()),
  }).index("by_email", ["email"]),

  // Messages table for chat messages
  messages: defineTable({
    author: v.string(),
    body: v.string(),
    timestamp: v.number(),
  }).index("by_timestamp", ["timestamp"]),

  // Rooms table for different chat rooms (future enhancement)
  rooms: defineTable({
    name: v.string(),
    description: v.optional(v.string()),
    isPrivate: v.boolean(),
  }),
});

Qué hace este esquema:

  • defineSchema crea nuestro esquema de base de datos con seguridad de tipos
  • defineTable define tablas individuales con sus campos
  • v.string(), v.number() son los validadores de tipos de Convex
  • v.optional() hace que los campos sean opcionales
  • .index() crea índices de base de datos para consultas eficientes

Los índices ayudan al rendimiento. by_timestamp te permite consultar mensajes en orden cronológico.


Paso 4: Crea las funciones de backend

Crea las funciones de backend para la app de chat. En Convex, las funciones se ejecutan en el servidor y se exponen como APIs.

Funciones de mensajes

Crea convex/messages.ts:

typescript
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";

// Query to get all messages (with real-time updates!)
export const getMessages = query({
  args: {},
  handler: async (ctx) => {
    // Get the last 50 messages, ordered by timestamp
    const messages = await ctx.db
      .query("messages")
      .withIndex("by_timestamp")
      .order("desc")
      .take(50);

    // Return them in chronological order (oldest first)
    return messages.reverse();
  },
});

// Mutation to send a new message
export const sendMessage = mutation({
  args: {
    author: v.string(),
    body: v.string()
  },
  handler: async (ctx, args) => {
    // Validate input
    if (!args.author.trim()) {
      throw new Error("Author name is required");
    }

    if (!args.body.trim()) {
      throw new Error("Message cannot be empty");
    }

    // Insert the message with current timestamp
    await ctx.db.insert("messages", {
      author: args.author.trim(),
      body: args.body.trim(),
      timestamp: Date.now(),
    });
  },
});

// Query to get message count (for stats)
export const getMessageCount = query({
  args: {},
  handler: async (ctx) => {
    const messages = await ctx.db.query("messages").collect();
    return messages.length;
  },
});

Esto es lo que ocurre en este código:

  • Las funciones query solo pueden leer datos y proporcionan actualizaciones en tiempo real automáticamente
  • Las funciones mutation pueden modificar datos y se ejecutan como transacciones atómicas
  • ctx.db te da acceso a tu base de datos con seguridad de tipos completa
  • withIndex() usa nuestros índices predefinidos para consultas eficientes
  • El manejo de errores está integrado — los errores lanzados se envían automáticamente al cliente

Funciones de usuario

Crea convex/users.ts para la gestión de usuarios:

typescript
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";

// Get or create a user
export const getOrCreateUser = mutation({
  args: {
    name: v.string(),
    email: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    // Check if user already exists
    let user = null;
    if (args.email) {
      user = await ctx.db
        .query("users")
        .withIndex("by_email", (q) => q.eq("email", args.email))
        .first();
    }

    // Create new user if not found
    if (!user) {
      const userId = await ctx.db.insert("users", {
        name: args.name,
        email: args.email,
      });
      user = await ctx.db.get(userId);
    }

    return user;
  },
});

// Get online users count
export const getActiveUsersCount = query({
  args: {},
  handler: async (ctx) => {
    const users = await ctx.db.query("users").collect();
    return users.length;
  },
});

Paso 5: Crea el proveedor de Convex para Astro

Las islas de componentes de Astro necesitan una forma de conectarse a Convex. Vamos a crear un wrapper de proveedor en src/lib/convex.tsx:

typescript
import { CONVEX_URL } from "astro:env/client";
import { ConvexProvider, ConvexReactClient } from "convex/react";
import { type FunctionComponent, type JSX } from "react";

const client = new ConvexReactClient(CONVEX_URL);

// Astro context providers don't work when used in .astro files.
// See this and other related issues: https://github.com/withastro/astro/issues/2016#issuecomment-981833594
//
// This exists to conveniently wrap any component that uses Convex.
export function withConvexProvider<Props extends JSX.IntrinsicAttributes>(
  Component: FunctionComponent<Props>,
) {
  return function WithConvexProvider(props: Props) {
    return (
      <ConvexProvider client={client}>
        <Component {...props} />
      </ConvexProvider>
    );
  };
}

¿Por qué no usar ConvexProvider directamente?

Las islas de componentes de Astro se ejecutan fuera del árbol de contexto de React. Si colocas un <ConvexProvider> dentro de un archivo .astro, los componentes hijos no tendrán acceso al contexto de Convex. El HOC withConvexProvider envuelve cada componente isla en su propio proveedor, manteniendo intacto el contexto de React. Este es el patrón oficial de la plantilla de Convex para Astro y el error más común al combinar Astro con Convex.


Paso 6: Construye los componentes de la interfaz de chat

Ahora vamos a crear nuestros componentes de React para la interfaz de chat. Se usarán como islas de Astro.

Componente de lista de mensajes

Crea src/components/MessageList.tsx:

typescript
import { useQuery } from "convex/react";
import { api } from "../../convex/_generated/api";
import { withConvexProvider } from "../lib/convex";
import { clsx } from "clsx";

function MessageListComponent() {
  const messages = useQuery(api.messages.getMessages);
  const messageCount = useQuery(api.messages.getMessageCount);

  if (messages === undefined) {
    return (
      <div className="flex-1 flex items-center justify-center">
        <div className="text-center">
          <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
          <p className="text-gray-500">Loading messages...</p>
        </div>
      </div>
    );
  }

  return (
    <div className="flex-1 overflow-y-auto p-4 space-y-4">
      {/* Chat header with stats */}
      <div className="text-center text-sm text-gray-500 mb-6">
        {messageCount} messages in this chat
      </div>

      {messages.length === 0 ? (
        <div className="text-center py-12">
          <div className="text-6xl mb-4">💬</div>
          <h3 className="text-lg font-medium text-gray-900 mb-2">
            No messages yet
          </h3>
          <p className="text-gray-500">
            Be the first to start the conversation!
          </p>
        </div>
      ) : (
        <div className="space-y-3">
          {messages.map((message) => (
            <div
              key={message._id}
              className={clsx(
                "max-w-xs lg:max-w-md px-4 py-2 rounded-2xl",
                "bg-blue-500 text-white ml-auto"
              )}
            >
              <div className="flex items-center justify-between mb-1">
                <span className="text-xs font-medium opacity-90">
                  {message.author}
                </span>
                <span className="text-xs opacity-75">
                  {new Date(message.timestamp).toLocaleTimeString([], {
                    hour: '2-digit',
                    minute: '2-digit'
                  })}
                </span>
              </div>
              <p className="text-sm">{message.body}</p>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// Export the wrapped component as default
const MessageList = withConvexProvider(MessageListComponent);
export default MessageList;

Componente de entrada de mensajes

Crea src/components/MessageInput.tsx:

typescript
import { useMutation } from "convex/react";
import { useState, useRef, useEffect } from "react";
import { api } from "../../convex/_generated/api";
import { withConvexProvider } from "../lib/convex";
import { clsx } from "clsx";

function MessageInputComponent() {
  const sendMessage = useMutation(api.messages.sendMessage);
  const [author, setAuthor] = useState("");
  const [body, setBody] = useState("");
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const messageInputRef = useRef<HTMLInputElement>(null);

  // Load author name from localStorage
  useEffect(() => {
    const savedAuthor = localStorage.getItem("chat-author-name");
    if (savedAuthor) {
      setAuthor(savedAuthor);
    }
  }, []);

  // Save author name to localStorage when it changes
  useEffect(() => {
    if (author) {
      localStorage.setItem("chat-author-name", author);
    }
  }, [author]);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError(null);

    if (!author.trim() || !body.trim()) {
      setError("Please enter both your name and a message");
      return;
    }

    setIsLoading(true);
    try {
      await sendMessage({
        author: author.trim(),
        body: body.trim()
      });

      // Clear message input and focus it
      setBody("");
      messageInputRef.current?.focus();

    } catch (err) {
      console.error("Failed to send message:", err);
      setError(err instanceof Error ? err.message : "Failed to send message");
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="border-t bg-white p-4">
      {error && (
        <div className="mb-3 p-2 bg-red-50 border border-red-200 rounded-md text-red-700 text-sm">
          {error}
        </div>
      )}

      <form onSubmit={handleSubmit} className="space-y-3">
        {/* Author name input */}
        <div>
          <input
            type="text"
            placeholder="Your name"
            value={author}
            onChange={(e) => setAuthor(e.target.value)}
            className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            disabled={isLoading}
          />
        </div>

        {/* Message input */}
        <div className="flex gap-2">
          <input
            ref={messageInputRef}
            type="text"
            placeholder="Type your message..."
            value={body}
            onChange={(e) => setBody(e.target.value)}
            className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            disabled={isLoading}
          />
          <button
            type="submit"
            disabled={isLoading || !author.trim() || !body.trim()}
            className={clsx(
              "px-6 py-2 rounded-lg font-medium transition-colors",
              "focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2",
              isLoading || !author.trim() || !body.trim()
                ? "bg-gray-300 text-gray-500 cursor-not-allowed"
                : "bg-blue-500 text-white hover:bg-blue-600"
            )}
          >
            {isLoading ? (
              <div className="flex items-center gap-2">
                <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
                Sending...
              </div>
            ) : (
              "Send"
            )}
          </button>
        </div>
      </form>
    </div>
  );
}

// Export the wrapped component as default
const MessageInput = withConvexProvider(MessageInputComponent);
export default MessageInput;

Paso 7: Crea el layout principal

Crea src/layouts/ChatLayout.astro:

astro
---
import '../styles/global.css'
export interface Props {
  title: string;
}

const { title } = Astro.props;
---

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="description" content="Real-time chat built with Astro and Convex" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <title>{title}</title>
  </head>
  <body class="bg-gray-50 min-h-screen">
    <slot />
  </body>
</html>

Paso 8: Construye la página principal del chat

Actualiza src/pages/index.astro:

astro
---
import ChatLayout from '../layouts/ChatLayout.astro';
import MessageList from '../components/MessageList';
import MessageInput from '../components/MessageInput';
---

<ChatLayout title="Astro + Convex Chat">
  <div class="min-h-screen flex flex-col">
    <!-- Header -->
    <header class="bg-white shadow-sm border-b">
      <div class="max-w-4xl mx-auto px-4 py-4">
        <div class="flex items-center justify-between">
          <div>
            <h1 class="text-2xl font-bold text-gray-900">
              ⚡ Astro + Convex Chat
            </h1>
            <p class="text-sm text-gray-600">
              Real-time messaging with Astro and Convex
            </p>
          </div>
          <div class="flex items-center gap-2 text-sm text-gray-500">
            <div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
            <span>Live</span>
          </div>
        </div>
      </div>
    </header>

    <!-- Chat Container -->
    <main class="flex-1 max-w-4xl mx-auto w-full bg-white shadow-lg flex flex-col">
      <MessageList client:load />
      <MessageInput client:load />
    </main>

    <!-- Footer -->
    <footer class="bg-gray-100 border-t">
      <div class="max-w-4xl mx-auto px-4 py-3">
        <p class="text-center text-sm text-gray-600">
          Built with
          <a href="https://astro.build" class="text-blue-600 hover:underline">Astro</a>
          and
          <a href="https://convex.dev" class="text-blue-600 hover:underline">Convex</a>
        </p>
      </div>
    </footer>
  </div>
</ChatLayout>

La directiva client:load

La directiva client:load le dice a Astro que hidrate estos componentes de React en el lado del cliente. Esto nos da la interactividad que necesitamos manteniendo una carga inicial rápida. El resto de la página (cabecera, pie, layout) se envía como HTML estático con cero JavaScript.


Paso 9: Configura las variables de entorno

Actualiza tu astro.config.mjs para manejar las variables de entorno correctamente:

javascript
// @ts-check
import react from "@astrojs/react";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig, envField } from "astro/config";

// https://astro.build/config
export default defineConfig({
  integrations: [react()],
  env: {
    schema: {
      CONVEX_URL: envField.string({
        access: "public",
        context: "client",
      }),
    },
  },
  vite: {
    plugins: [tailwindcss()],
  },
});

Crea o actualiza .env.local con la URL de tu despliegue de Convex:

ini
# Your Convex deployment URL (auto-generated by convex dev)
CONVEX_URL=https://your-deployment.convex.cloud

El build fallará sin CONVEX_URL

Con astro:env, CONVEX_URL es una variable requerida en tiempo de compilación. Si falta, astro build fallará con un error. Asegúrate de que .env.local esté configurado localmente y de que CONVEX_URL esté definida como variable de entorno de compilación en tu host de despliegue (CI, Vercel, etc.).

Verifica: Tras ejecutar npm run dev, revisa la consola del navegador — si no hay errores de conexión con Convex, la URL es correcta.


Paso 10: Ejecuta tu aplicación

Ahora ejecutemos la app. Actualiza los scripts de tu package.json:

json
{
  "scripts": {
    "dev": "run-p dev:*",
    "dev:astro": "astro dev",
    "dev:convex": "convex dev",
    "build": "astro build",
    "preview": "astro preview",
    "convex": "convex"
  }
}

Arranca tu entorno de desarrollo:

bash
# This runs both Astro and Convex in parallel
npm run dev

Abre tu navegador en http://localhost:4321 y deberías ver tu app de chat.

Prueba las actualizaciones en tiempo real

Abre varias pestañas o ventanas del navegador para probar la sincronización en tiempo real. Los mensajes enviados desde una pestaña aparecerán al instante en todas las demás.

Verifica que funciona

Repasa estas comprobaciones para confirmar que todo está conectado:

  1. Abre http://localhost:4321 — la UI del chat debería renderizarse (no una página en blanco ni un spinner).
  2. Abre una segunda pestaña del navegador — ambas pestañas deberían mostrar el mismo estado vacío.
  3. Envía un mensaje en la pestaña 1 — debería aparecer al instante en la pestaña 2. Esa es la prueba del tiempo real.
  4. Ejecuta npx convex dashboard — abre el panel de Convex, ve a Data → tabla messages y confirma que existe la fila del mensaje.
  5. Ejecuta npx convex functions — lista las funciones desplegadas; deberían aparecer messages:getMessages y messages:sendMessage.

Solución de problemas

La UI se queda en el spinner para siempre

convex dev no está en ejecución. Tanto Astro como Convex deben ejecutarse en paralelo. Reinicia con npm run dev (que usa run-p para arrancar ambos). Revisa el terminal en busca de errores del proceso de Convex.

Error: ConvexProvider no encontrado

Se colocó un <ConvexProvider> dentro de un archivo .astro. Las islas de componentes de Astro se ejecutan fuera del árbol de contexto de React — debes usar el patrón del HOC withConvexProvider del Paso 5. Cada componente que usa Convex necesita su propio wrapper de proveedor.

El build falla con un error de variables de entorno

CONVEX_URL no está definida en el entorno de compilación. Con astro:env, una variable pública requerida que falta provoca un fallo del build. Defínela en .env.local para desarrollo local y como variable de entorno de compilación en tu host de despliegue (CI, Vercel, etc.).

Tipos de _generated desincronizados

El esquema o las funciones cambiaron pero convex dev no estaba en ejecución. Reinicia convex dev para regenerar convex/_generated/. Los tipos se autogeneran a partir de tu esquema y las firmas de tus funciones — deben mantenerse sincronizados.

El build de Astro falla por errores de HTML

El compilador Rust de Astro 7 es más estricto que el antiguo compilador JS. Las etiquetas sin cerrar y el HTML inválido que antes se autocorregían ahora causan errores de compilación. Revisa que tus plantillas .astro tengan una estructura HTML correcta.


Patrones que vale la pena conocer

Manejo de errores

Maneja los errores en tus componentes:

typescript
// In your components
const [error, setError] = useState<string | null>(null);

try {
  await sendMessage({ author, body });
} catch (err) {
  if (err instanceof ConvexError) {
    setError(err.data);
  } else {
    setError("Something went wrong. Please try again.");
  }
}

Optimización del rendimiento

  • Usa la hidratación parcial de Astro — Hidrata solo los componentes interactivos
  • Implementa paginación para listas de mensajes grandes usando .paginate() de Convex y usePaginatedQuery
  • Añade debouncing para funciones en tiempo real como los indicadores de escritura
  • Usa la caché integrada de Convex — Las consultas se cachean e invalidan automáticamente

Buenas prácticas de seguridad

  • Validación de entradas — Valida siempre los datos en tus funciones de Convex (ya implementado en el Paso 4)
  • Limitación de tasa — Usa @convex-dev/rate-limiter (componente oficial, con seguridad de tipos, transaccional, cola justa)
  • Autenticación — Usa @convex-dev/auth (beta) para OAuth con GitHub/Google, magic links y OTP vía Auth.js
  • Moderación de contenido — Añade filtros para contenido inapropiado antes de producción

Costes y límites para proyectos secundarios

Antes de publicar, conviene saber qué te da el plan gratuito y dónde están los límites.

Plan gratuito de Convex

El plan gratuito incluye ejecuciones de funciones, ancho de banda, almacenamiento de base de datos y almacenamiento de archivos con límites estrictos. Cuando alcanzas un límite, las nuevas mutaciones pueden fallar. Consulta las cifras actuales en docs.convex.dev/production/state/limits. El hosting en la UE (región de Irlanda) está disponible pero cuesta 1,3× la tarifa de EE. UU.

Algunas cosas que conviene saber:

  • Concurrencia: Los planes Free y Starter incluyen S16 = 16 consultas/mutaciones concurrentes. Suficiente para un tutorial o un proyecto secundario pequeño. Conviene saberlo si planeas un lanzamiento con picos de tráfico.
  • Hosting en la UE: Lanzado en febrero de 2026 en EU West (Irlanda). Buena noticia si tu audiencia está en Europa — menor latencia, aunque a 1,3× el precio de EE. UU.
  • Válvula de escape open source: Convex ahora es open source (get-convex/convex-backend, más de 11,9k estrellas). Puedes autoalojar Convex con Docker Compose en tu propia infraestructura. Si quieres comparar el rendimiento, consulta los benchmarks de Convex autoalojado vs. el plan gratuito en la nube.

Despliegue en producción

El tutorial te lleva hasta un entorno de desarrollo funcional. Este es el camino más corto a producción:

  1. Crea un despliegue de producción: npx convex deployment create prod
  2. Genera una deploy key en el panel de Convex (Settings → Deploy Keys)
  3. Define CONVEX_URL apuntando al despliegue de producción en el entorno de compilación de tu plataforma de hosting

Para despliegues de producción en un solo paso, ejecuta npx convex deploy con tu deploy key configurada.

Guía completa de despliegue en Vercel

La guía paso a paso del despliegue en Vercel está en la parte 2 de esta serie.

Desplegar en Vercel →

Lo que has construido

  • La arquitectura de islas de Astro: carga rápida con interactividad selectiva
  • La base de datos en tiempo real de Convex: sincronización automática entre clientes
  • Desarrollo con seguridad de tipos: TypeScript de extremo a extremo con tipos autogenerados
  • Patrones modernos de React: hooks, manejo de errores y optimización del rendimiento
  • Flujo de despliegue a producción: de desarrollo a producción con la CLI de Convex

Próximos pasos y mejoras

La app de chat funciona, pero hay mucho que puedes añadir. Todos estos son componentes oficiales de Convex — no hace falta construirlos desde cero.

Autenticación con Convex Auth

@convex-dev/auth (beta) te da OAuth con GitHub/Google, magic links, OTP y email+contraseña vía Auth.js. Reemplaza la tabla users hecha a mano y la mutación getOrCreateUser del Paso 4 con flujos de autenticación completos. Consulta convex.dev/auth para la configuración.

Convex Auth está en beta

La API puede cambiar. Es funcional y se mantiene activamente, pero fija tu versión y prueba las actualizaciones antes de integrarlas.

Rate limiting

@convex-dev/rate-limiter es un limitador de tasa con seguridad de tipos, transaccional y con cola justa. Intégralo para prevenir spam sin escribir lógica personalizada. Consulta convex.dev/components/rate-limiter.

Indicadores de escritura y presencia

@convex-dev/presence proporciona un hook usePresence para indicadores de escritura y estado “en línea ahora”. Directamente relevante para esta app de chat — es la solución lista para usar de la función de “indicadores de escritura” que de otro modo construirías manualmente. Consulta convex.dev/components/presence.

Paginación para listas grandes de mensajes

El límite take(50) del Paso 4 funciona para una demo, pero las apps en producción necesitan paginación. Convex ofrece .paginate(opts) en el servidor, que devuelve un cursor, junto con el hook usePaginatedQuery en el cliente. Consulta la documentación de paginación de Convex.

Desarrollo orientado a agentes

Si usas agentes de programación con IA: Convex tiene un servidor MCP para Claude Code y herramientas similares, además de npx convex ai-files para generar archivos de contexto para agentes. Astro 7 añade astro dev --background con logs en JSON, que los agentes pueden parsear.

Otros frameworks

Convex funciona con más que Astro. Si quieres probar un framework de frontend diferente con Convex, consulta Primeros pasos con TanStack Start y Convex.


Conclusión

Has construido una aplicación de chat en tiempo real que:

  • Carga al instante gracias a la generación estática de Astro
  • Se actualiza en tiempo real para todos los usuarios conectados
  • Escala automáticamente con la arquitectura serverless de Convex
  • Mantiene la consistencia de datos incluso bajo carga pesada
  • Conserva la seguridad de tipos de la base de datos al frontend

Los patrones de aquí (islas de Astro, consultas en tiempo real de Convex, el HOC withConvexProvider) se aplican a cualquier app en tiempo real: herramientas colaborativas, paneles en vivo, juegos multijugador, sistemas de inventario.

¿Listo para salir en vivo? El siguiente paso es desplegar en producción.

Siguiente: Desplegar en Vercel →

¿Quieres profundizar en Astro? Echa un vistazo a los mejores cursos y tutoriales de Astro.js para llevar tus habilidades al siguiente nivel.