The Complete Convex Guide | Real-Time Backend, Type Safety, React, Serverless, Production Use
What this post covers
This is a complete guide to building a real-time backend with Convex. It covers type-safe APIs, live subscriptions, file storage, and authentication with practical examples.
From the field: After moving from Firebase to Convex, we saw stronger type safety and more capable real-time features.
Introduction: “Real-time backends feel complex”
Real-world scenarios
Scenario 1: Type safety is weak
Firebase typing is loose. Convex offers end-to-end type safety. Scenario 2: Real-time subscriptions are hard
WebSocket setup is fiddly. Convex handles it automatically. Scenario 3: I need backend logic
Hard to do everything on the client. Convex provides server functions.
1. What is Convex?
Core characteristics
Convex is a real-time backend platform. Main benefits:
- Type safety: End-to-end TypeScript
- Real time: Automatic subscriptions
- Server functions: Query, Mutation, Action
- File storage: Built in
- Auth: Clerk integration
2. Project setup
Installation
npm create convex@latest
Project structure
The layout below shows a typical implementation. Run the code yourself to see how it behaves.
my-convex-app/
├── convex/
│ ├── schema.ts
│ ├── users.ts
│ └── posts.ts
├── src/
│ └── app/
└── convex.json
3. Schema definition
// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
users: defineTable({
email: v.string(),
name: v.string(),
createdAt: v.number(),
}).index('by_email', ['email']),
posts: defineTable({
title: v.string(),
content: v.string(),
authorId: v.id('users'),
published: v.boolean(),
createdAt: v.number(),
})
.index('by_author', ['authorId'])
.index('by_published', ['published']),
});
4. Query & Mutation
Query
// convex/posts.ts
import { query } from './_generated/server';
import { v } from 'convex/values';
export const list = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query('posts').collect();
},
});
export const get = query({
args: { id: v.id('posts') },
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
});
Mutation
import { mutation } from './_generated/server';
import { v } from 'convex/values';
export const create = mutation({
args: {
title: v.string(),
content: v.string(),
authorId: v.id('users'),
},
handler: async (ctx, args) => {
const postId = await ctx.db.insert('posts', {
...args,
published: false,
createdAt: Date.now(),
});
return postId;
},
});
export const update = mutation({
args: {
id: v.id('posts'),
title: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.patch(args.id, { title: args.title });
},
});
5. React integration
Provider
// app/ConvexClientProvider.tsx
'use client';
import { ConvexProvider, ConvexReactClient } from 'convex/react';
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export default function ConvexClientProvider({ children }) {
return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}
useQuery
The following is a detailed TypeScript example: import the modules you need, iterate over data in loops, and branch with conditionals. Read through each part to see what it does.
'use client';
import { useQuery } from 'convex/react';
import { api } from '../convex/_generated/api';
export default function Posts() {
const posts = useQuery(api.posts.list);
if (posts === undefined) return <div>Loading...</div>;
return (
<ul>
{posts.map((post) => (
<li key={post._id}>{post.title}</li>
))}
</ul>
);
}
useMutation
'use client';
import { useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
export default function CreatePost() {
const createPost = useMutation(api.posts.create);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
await createPost({
title: formData.get('title') as string,
content: formData.get('content') as string,
authorId: 'user-id',
});
};
return (
<form onSubmit={handleSubmit}>
<input name="title" required />
<textarea name="content" />
<button type="submit">Create Post</button>
</form>
);
}
6. Action (external APIs)
// convex/actions.ts
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) => {
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
personalizations: [{ to: [{ email: args.to }] }],
from: { email: '[email protected]' },
subject: args.subject,
content: [{ type: 'text/plain', value: args.body }],
}),
});
return response.ok;
},
});
7. File storage
// convex/files.ts
import { mutation } from './_generated/server';
export const generateUploadUrl = mutation({
args: {},
handler: async (ctx) => {
return await ctx.storage.generateUploadUrl();
},
});
export const saveFile = mutation({
args: { storageId: v.string() },
handler: async (ctx, args) => {
await ctx.db.insert('files', {
storageId: args.storageId,
createdAt: Date.now(),
});
},
});
// Client
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
const saveFile = useMutation(api.files.saveFile);
const handleUpload = async (file: File) => {
const uploadUrl = await generateUploadUrl();
const response = await fetch(uploadUrl, {
method: 'POST',
body: file,
});
const { storageId } = await response.json();
await saveFile({ storageId });
};
Summary and checklist
Key takeaways
- Convex: Real-time backend
- Type safety: End-to-end TypeScript
- Real time: Automatic subscriptions
- Server functions: Query, Mutation, Action
- File storage: Built in
- Auth: Clerk integration
Implementation checklist
- Create a Convex project
- Define the schema
- Implement queries
- Implement mutations
- Integrate with React
- Implement actions
- Implement file storage
Related reading
- Supabase complete guide
- tRPC complete guide
- Firebase complete guide
Keywords in this post
Convex, Backend, Realtime, TypeScript, React, Serverless, Database
Frequently asked questions (FAQ)
Q. How does it compare to Firebase?
A. Convex has much stronger type safety. Firebase is more mature and has a broader feature set.
Q. How does it compare to Supabase?
A. Convex’s real-time features are stronger. Supabase is PostgreSQL-based and more flexible for SQL-centric workflows.
Q. Can I use it for free?
A. Yes, there is a free tier: up to 1GB of data and 1GB of file storage at no cost.
Q. Is it suitable for production?
A. Yes—many startups run Convex in production.