Firebase is complex. Supabase is PostgreSQL (not real-time by default). Convex gives you a real-time reactive database with TypeScript functions.
What is Convex?
Convex is a reactive backend-as-a-service. Write TypeScript functions, get a real-time database, and your UI updates automatically when data changes.
Free Tier
- 1M function calls/month
- 1 GB storage
- 16 MB file storage
- Automatic real-time sync
Quick Start
npm create convex@latest
Define Your Schema
// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
tasks: defineTable({
text: v.string(),
completed: v.boolean(),
userId: v.id('users'),
createdAt: v.number(),
}).index('by_user', ['userId']),
users: defineTable({
name: v.string(),
email: v.string(),
}),
});
Queries (Real-Time by Default)
// convex/tasks.ts
import { query } from './_generated/server';
import { v } from 'convex/values';
export const list = query({
args: { userId: v.id('users') },
handler: async (ctx, args) => {
return await ctx.db
.query('tasks')
.withIndex('by_user', q => q.eq('userId', args.userId))
.order('desc')
.collect();
},
});
Mutations
import { mutation } from './_generated/server';
import { v } from 'convex/values';
export const create = mutation({
args: {
text: v.string(),
userId: v.id('users'),
},
handler: async (ctx, args) => {
return await ctx.db.insert('tasks', {
text: args.text,
completed: false,
userId: args.userId,
createdAt: Date.now(),
});
},
});
export const toggle = mutation({
args: { id: v.id('tasks') },
handler: async (ctx, args) => {
const task = await ctx.db.get(args.id);
if (!task) throw new Error('Task not found');
await ctx.db.patch(args.id, { completed: !task.completed });
},
});
React Integration (Real-Time!)
import { useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function TaskList({ userId }) {
// This automatically re-renders when tasks change!
const tasks = useQuery(api.tasks.list, { userId });
const createTask = useMutation(api.tasks.create);
const toggleTask = useMutation(api.tasks.toggle);
if (tasks === undefined) return <p>Loading...</p>;
return (
<div>
<button onClick={() => createTask({ text: 'New task', userId })}>
Add Task
</button>
<ul>
{tasks.map(task => (
<li key={task._id} onClick={() => toggleTask({ id: task._id })}>
{task.completed ? 'Done' : 'Todo'}: {task.text}
</li>
))}
</ul>
</div>
);
}
Open two browser tabs — changes appear in both instantly. No WebSocket setup, no polling.
Actions (External APIs)
import { action } from './_generated/server';
import { v } from 'convex/values';
export const sendEmail = action({
args: { to: v.string(), subject: v.string(), body: v.string() },
handler: async (ctx, args) => {
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.RESEND_API_KEY}` },
body: JSON.stringify({ from: 'app@example.com', ...args }),
});
},
});
Convex vs Firebase vs Supabase
| Feature | Convex | Firebase | Supabase |
|---|---|---|---|
| Real-time | Automatic | Listeners | Manual |
| TypeScript | Full | Partial | Good |
| Server Logic | TypeScript functions | Cloud Functions | Edge Functions |
| Schema | Typed | Schemaless | PostgreSQL |
| Transactions | ACID | Limited | Full SQL |
| File Storage | Built-in | Built-in | Built-in |
| Auth | Plugin | Built-in | Built-in |
Need real-time data feeds? Check out my Apify actors — real-time web data for your Convex app. For custom solutions, email spinov001@gmail.com.
Top comments (0)