Convex is a reactive backend where queries automatically update when data changes. No polling, no WebSocket setup, no cache invalidation — just write a query and it stays in sync.
Why Convex?
- Reactive queries — UI updates instantly when data changes
- TypeScript end-to-end — schema, queries, mutations — all typed
- No infrastructure — managed database, file storage, scheduled jobs
- Free tier — generous for hobby projects
Quick Start
npm create convex@latest
npx convex dev # Starts dev server + syncs schema
Schema
// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
avatar: v.optional(v.string()),
}).index('by_email', ['email']),
messages: defineTable({
text: v.string(),
author: v.id('users'),
channel: v.string(),
}).index('by_channel', ['channel']),
});
Queries (Reactive!)
// convex/messages.ts
import { query } from './_generated/server';
import { v } from 'convex/values';
export const list = query({
args: { channel: v.string() },
handler: async (ctx, { channel }) => {
return await ctx.db
.query('messages')
.withIndex('by_channel', (q) => q.eq('channel', channel))
.order('desc')
.take(50);
},
});
// React component — auto-updates!
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
function Chat({ channel }: { channel: string }) {
const messages = useQuery(api.messages.list, { channel });
return (
<ul>
{messages?.map((msg) => (
<li key={msg._id}>{msg.text}</li>
))}
</ul>
);
}
Mutations
// convex/messages.ts
import { mutation } from './_generated/server';
export const send = mutation({
args: { text: v.string(), channel: v.string(), author: v.id('users') },
handler: async (ctx, { text, channel, author }) => {
await ctx.db.insert('messages', { text, channel, author });
},
});
import { useMutation } from 'convex/react';
function SendMessage() {
const send = useMutation(api.messages.send);
const handleSend = () => {
send({ text: 'Hello!', channel: 'general', author: userId });
// All useQuery(api.messages.list) components update INSTANTLY
};
}
Actions (External APIs)
import { action } from './_generated/server';
export const generateSummary = action({
args: { text: v.string() },
handler: async (ctx, { text }) => {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.OPENAI_KEY}` },
body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: text }] }),
});
return response.json();
},
});
Scheduled Functions
export const cleanup = mutation({
handler: async (ctx) => {
const old = await ctx.db.query('messages')
.filter((q) => q.lt(q.field('_creationTime'), Date.now() - 86400000))
.collect();
for (const msg of old) await ctx.db.delete(msg._id);
},
});
// Schedule in Convex dashboard or via ctx.scheduler
Need real-time data for your app? Check out my Apify actors for web scraping feeds, or email spinov001@gmail.com for custom reactive backends.
Convex, Supabase, or Firebase — which reactive backend do you use? Share below!
Top comments (0)