---
title: "Build a Real-Time App with Astro and Convex"
description: "Learn how to build real-time applications using Astro's static site generation with Convex's backend-as-a-service. Tutorial with working code examples and deployment guide."
date: 2025-07-31
categories: ["cms"]
tags: ["astro","convex"]
---

Let's build applications that are fast and real-time. We'll combine **Astro** for static sites and **Convex** for backend functionality. By the end of this tutorial, you'll have a chat application that loads quickly and updates in real-time across connected users.

You get both speed and interactivity.

## 🤔 What Makes This Stack So Special?

Let's understand why Astro + Convex works well:

### Why Astro is Perfect for Modern Apps

**Astro** ships HTML with JavaScript only where you need it, unlike frameworks that send megabytes of JavaScript.

<ListCheck>
- **Zero JavaScript by default** - Pages load with pure HTML/CSS
- **Islands Architecture** - Interactive components are hydrated independently
- **Framework agnostic** - Use React, Vue, Svelte, or plain JavaScript components
- **Built-in optimizations** - Image optimization, CSS bundling, and more out of the box
</ListCheck>

### Why Convex is a Backend Game-Changer

**Convex** handles backend development:

<ListCheck>

- **Real-time by default** - Queries become live subscriptions

- **Strong consistency** - No more race conditions or data inconsistencies
- **TypeScript everywhere** - End-to-end type safety from database to frontend
- **Serverless and scalable** - Zero infrastructure management required
- **ACID transactions** - Your data stays consistent even under heavy load

</ListCheck>

Convex provides real-time capabilities with consistency guarantees.

---

## 🎯 What We're Building Today

We're building a **real-time chat application**:

- **Fast initial page load** with Astro
- **Real-time message updates** across all connected users
- **Type-safe API** from database to frontend
- **Production-ready deployment** on modern hosting platforms
- **Beautiful, responsive UI** with Tailwind CSS

<YouTubeEmbed
  url="https://www.youtube.com/embed/ZZWOj6kwWxc"
  label="Astro + Convex Real-Time Chat Demo"
/>

---

## 🛠️ Prerequisites & Setup

Before starting, make sure you have:

<ListCheck>

- **Node.js 18+** installed on your machine
- **Basic TypeScript knowledge** (we'll explain the Convex-specific parts)
- **A GitHub account** (for Convex authentication)
- **About 45 minutes** of focused coding time

</ListCheck>

<Notice type="info" title="New to TypeScript?">

The TypeScript here is straightforward, and Convex's type safety helps development.

</Notice>

---

## 🚀 Step 1: Create Your Astro Project

Create a new Astro project:

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

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

# Navigate to your project
cd astro-convex-chat
```

Now let's add React integration for our interactive components:

```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="Pro Tip">

The `npx astro add` commands configure TypeScript types and build settings automatically.

</Notice>

---

## 🔧 Step 2: Install and Configure Convex

Add Convex to the project:

```bash
# Install Convex
npm install convex

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

During the Convex setup process:

1. **Sign in with GitHub** - Convex uses GitHub for authentication
2. **Create a new project** - Name it something like "astro-chat-app"
3. **Accept the default configuration** - Convex will create a `convex/` folder

This creates several important files:
- `convex/` folder - Where your backend functions live
- `.env.local` - Contains your Convex deployment URL
- `convex/_generated/` - Auto-generated TypeScript types

<Notice type="warning" title="Keep convex dev Running">

Make sure to keep the `npx convex dev` command running throughout development. It watches your backend functions and keeps everything in sync.

</Notice>

---

## 📊 Step 3: Design Your Database Schema

Convex uses a schema-first approach for type safety. Define the chat app's data structure in `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(),
  }),
});
```

**What's happening here?**

<ListCheck>

- **defineSchema** creates our database schema with type safety
- **defineTable** defines individual tables with their fields
- **v.string(), v.number()** are Convex's type validators
- **v.optional()** makes fields optional
- **.index()** creates database indexes for efficient queries

</ListCheck>

The indexes help performance. `by_timestamp` lets you query messages in chronological order.

---

## 🔨 Step 4: Create Backend Functions

Create backend functions for the chat app. In Convex, functions run on the server and are exposed as APIs.

### Message Functions

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

**Key Concepts Explained:**

<ListCheck>

- **query** functions can only read data and automatically provide real-time updates
- **mutation** functions can modify data and run as atomic transactions
- **ctx.db** gives you access to your database with full type safety
- **withIndex()** uses our predefined indexes for efficient queries
- **Error handling** is built-in - thrown errors are automatically sent to the client

</ListCheck>

### User Functions

Create `convex/users.ts` for user management:

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

---

## ⚛️ Step 5: Create the Convex Provider for Astro

Astro's component islands need a way to connect to Convex. Let's create a provider wrapper in `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>
    );
  };
}

```

---

## 🎨 Step 6: Build the Chat Interface Components

Now let's create our React components for the chat interface. These will be used as Astro islands.

### Message List Component

Create `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;
```

### Message Input Component

Create `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;
```

---

## 🏗️ Step 7: Create the Main Layout

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

---

## 📱 Step 8: Build the Main Chat Page

Update `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">
              Lightning-fast real-time messaging
            </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="The client:load Directive">

The `client:load` directive tells Astro to hydrate these React components on the client side. This gives us the interactivity we need while keeping the initial page load fast.

</Notice>

---

## ⚙️ Step 9: Configure Environment Variables

Update your `astro.config.mjs` to handle environment variables properly:

```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()],
  },
});

```

Create or update `.env.local` to ensure your Convex URL is accessible:

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

---

## 🚀 Step 10: Run Your Application

Now let's see your creation in action! Update your `package.json` scripts:

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

Start your development environment:

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

Open your browser to `http://localhost:4321` and you should see your chat app! 🎉

<Notice type="success" title="Testing Real-Time Updates">

Open multiple browser tabs or windows to see the real-time magic in action. Messages sent from one tab will instantly appear in all other tabs!

</Notice>

---



## 🔧 Advanced Patterns & Best Practices

### Error Handling

Implement robust error handling in your components:

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

### Performance Optimization

<ListCheck>

- **Use Astro's partial hydration** - Only hydrate interactive components
- **Implement pagination** for large message lists
- **Add debouncing** for real-time features like typing indicators
- **Use Convex's built-in caching** - Queries are automatically cached and invalidated

</ListCheck>

### Security Best Practices

<ListCheck>

- **Input validation** - Always validate data in your Convex functions
- **Rate limiting** - Implement rate limiting for message sending
- **Content moderation** - Add filters for inappropriate content
- **Authentication** - Implement proper user authentication for production apps

</ListCheck>

---

## 🎓 What You've Learned

Congratulations! You've just built a production-ready real-time application using cutting-edge technologies. Here's what you've mastered:

<ListCheck>

- **Astro's Islands Architecture** - Fast loading with selective interactivity
- **Convex's Real-Time Database** - Automatic synchronization across clients
- **Type-Safe Development** - End-to-end TypeScript with auto-generated types
- **Modern React Patterns** - Hooks, error handling, and performance optimization
- **Deployment Strategies** - Static site hosting with serverless backend

</ListCheck>

## 🚀 Next Steps & Enhancements

Ready to take your app to the next level? Here are some exciting features to add:

<Button text="Message Threading" size="sm" color="blue" variant="outline" />
<Button text="File Uploads" size="sm" color="green" variant="outline" />
<Button text="User Authentication" size="sm" color="purple" variant="outline" />
<Button text="Push Notifications" size="sm" color="red" variant="outline" />

### Advanced Features to Implement:

<ListCheck>

- **User Authentication** with Convex Auth for secure login
- **Multiple Chat Rooms** with real-time room switching
- **File Sharing** using Convex's built-in file storage
- **Message Search** with full-text search capabilities
- **Typing Indicators** showing when users are typing
- **Message Reactions** with emoji support
- **Push Notifications** for mobile users
- **Message Threading** for organized conversations

</ListCheck>

### Production Considerations:

<ListCheck>

- **Rate Limiting** to prevent spam and abuse
- **Content Moderation** with automated filtering
- **Analytics Integration** for user insights
- **Error Monitoring** with tools like Sentry
- **Performance Monitoring** to track real-world usage
- **Backup Strategies** for critical data
- **Load Testing** to ensure scalability

</ListCheck>

---

## 💡 Why This Stack is Perfect for Modern Apps

The Astro + Convex combination gives you superpowers that were previously impossible:

**Astro Benefits:**
- **Instant page loads** with minimal JavaScript
- **SEO-friendly** static site generation
- **Framework flexibility** - use any UI library
- **Automatic optimizations** for images, CSS, and more

**Convex Benefits:**
- **Real-time everything** without complex WebSocket management
- **Strong consistency** prevents data corruption and race conditions
- **Serverless scalability** from prototype to millions of users
- **TypeScript integration** catches bugs before they reach production

**Together, they provide:**
- **Best-in-class performance** - Fast initial loads AND real-time updates
- **Developer experience** that makes building complex apps feel simple
- **Production readiness** with built-in scalability and reliability
- **Cost effectiveness** - pay only for what you use

<Notice type="info" title="Perfect for Startups">

This stack is ideal for startups and side projects because you can build and deploy rapidly without worrying about infrastructure, scaling, or complex backend management.

</Notice>

---

## 🏆 Conclusion

You've just built something pretty amazing! A real-time chat application that:
<ListCheck>
- **Loads instantly** thanks to Astro's static generation
- **Updates in real-time** across all connected users
- **Scales automatically** with Convex's serverless architecture
- **Maintains data consistency** even under heavy load
- **Provides excellent developer experience** with end-to-end type safety
</ListCheck>
This is just the beginning. The patterns and concepts you've learned here apply to countless other applications:

- **Collaborative tools** (think Google Docs)
- **Live dashboards** with real-time metrics
- **Gaming applications** with live leaderboards
- **E-commerce sites** with live inventory updates
- **Social platforms** with instant notifications

The web is moving towards real-time, interactive experiences, and you now have the tools to build them efficiently and reliably.

<Button text="Star This Tutorial on GitHub" size="lg" color="blue" variant="solid" link="https://github.com/your-repo/astro-convex-tutorial" external={true} icon="star" iconPosition="left" />

---

**What's next?** Try building your own real-time application! Whether it's a collaborative todo app, a live polling system, or a multiplayer game, you now have the foundation to create amazing user experiences.

Happy coding!