---
title: "Crea una app en tiempo real con Astro y Convex (Tutorial 2026)"
description: "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."
date: 2025-07-31
categories: ["web-development"]
tags: ["astro","convex","real-time"]
---

import Button from "@components/widgets/Button.astro";
import Notice from "@components/widgets/Notice.astro";
import ListCheck from "@components/widgets/ListCheck.astro";
import Accordion from "@components/widgets/Accordion.astro";
import YouTubeEmbed from "@components/widgets/YouTubeEmbed.astro";

En este tutorial construirás una app en tiempo real con [Convex](https://go.bitdoze.com/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](/es/astro-vs-nextjs-vs-tanstack-start-cual-gana/), 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](/es/crear-blog-astro-gratis/) 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](/es/astro-7-compilaciones-mas-rapidas/) para más detalles.

<ListCheck>
- **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
</ListCheck>

### Convex: el tiempo real como primitiva de primera clase

**[Convex](https://go.bitdoze.com/convex)** se encarga del desarrollo backend con el tiempo real como primitiva de primera clase:

<ListCheck>

- **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

</ListCheck>

Además, Convex ahora es open source. Puedes [autoalojar Convex con Docker Compose](/es/autoalojar-convex/) 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](/astro-convex-vercel-deployment/)
- **UI responsive** con Tailwind CSS

<YouTubeEmbed
  url="https://www.youtube.com/embed/ZZWOj6kwWxc"
  label="Demo de chat en tiempo real con Astro + Convex"
/>

---

## 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.

<Notice type="info" title="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 `{" "}`.

</Notice>

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:

<ListCheck>

- **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

</ListCheck>

<Notice type="info" title="¿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.

</Notice>

<Notice type="info" title="¿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](https://convex.dev/templates/astro) para más detalles.

</Notice>

---

## 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
```

<Notice type="success" title="Consejo profesional">

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

</Notice>

**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

<Notice type="warning" title="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á.

</Notice>

**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:

<ListCheck>

- **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

</ListCheck>

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:

<ListCheck>

- 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

</ListCheck>

### 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>
    );
  };
}
```

<Notice type="warning" title="¿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](https://convex.dev/templates/astro) y el error más común al combinar Astro con Convex.

</Notice>

---

## 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>
```

<Notice type="info" title="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.

</Notice>

---

## 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
```

<Notice type="warning" title="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.).

</Notice>

**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.

<Notice type="success" title="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.

</Notice>

### 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

<Accordion label="La UI se queda en el spinner para siempre" group="troubleshooting">

`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.

</Accordion>

<Accordion label="Error: ConvexProvider no encontrado" group="troubleshooting">

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.

</Accordion>

<Accordion label="El build falla con un error de variables de entorno" group="troubleshooting">

`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.).

</Accordion>

<Accordion label="Tipos de _generated desincronizados" group="troubleshooting">

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.

</Accordion>

<Accordion label="El build de Astro falla por errores de HTML" group="troubleshooting">

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.

</Accordion>

---

## 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

<ListCheck>

- **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

</ListCheck>

### Buenas prácticas de seguridad

<ListCheck>

- **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

</ListCheck>

---

## 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.

<Notice type="info" title="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](https://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.

</Notice>

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](/es/autoalojar-convex/) en tu propia infraestructura. Si quieres comparar el rendimiento, consulta los [benchmarks de Convex autoalojado vs. el plan gratuito en la nube](/es/benchmark-convex-autoalojado/).

---

## 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.

<Notice type="success" title="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.

</Notice>

<Button text="Desplegar en Vercel →" link="/astro-convex-vercel-deployment/" variant="solid" color="blue" size="md" />

---

## Lo que has construido

<ListCheck>
- **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
</ListCheck>

---

## 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](https://convex.dev/auth) para la configuración.

<Notice type="info" title="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.

</Notice>

### 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](https://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](https://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](https://docs.convex.dev/database/reading-data).

### Desarrollo orientado a agentes

Si usas agentes de programación con IA: Convex tiene un [servidor MCP](https://docs.convex.dev/ai/convex-mcp-server) 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](/tanstack-start-get-start/).

---

## Conclusión

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

<ListCheck>
- **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
</ListCheck>

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.

<Button text="Siguiente: Desplegar en Vercel →" link="/astro-convex-vercel-deployment/" variant="solid" color="blue" size="md" />

¿Quieres profundizar en Astro? Echa un vistazo a los [mejores cursos y tutoriales de Astro.js](/best-astrojs-online-courses/) para llevar tus habilidades al siguiente nivel.