Complete Convex Complete Guide | Realtime Backend
이 글의 핵심
Complete guide to building realtime backend with Convex. From type-safe APIs, realtime subscriptions, file storage to authentication with practical exam...
Key Takeaways
Complete guide to building realtime backend with Convex. From type-safe APIs, realtime subscriptions, file storage to authentication with practical examples.
Real-World Experience: Sharing experience of switching from Firebase to Convex, improving type safety and making realtime features more powerful.
Introduction: “Realtime Backend Is Complex”
Real-World Problem Scenarios
Scenario 1: Lack of Type Safety
Firebase has weak typing. Convex provides perfect type safety.
Scenario 2: Difficult Realtime Subscriptions
WebSocket setup is complex. Convex handles it automatically.
Scenario 3: Need Backend Logic
Difficult to handle on client. Convex provides server functions.
1. What is Convex?
Core Features
Convex is a realtime backend platform. Key Advantages:
- Type Safety: End-to-End TypeScript
- Realtime: Automatic subscriptions
- Server Functions: Query, Mutation, Action
- File Storage: Built-in
- Authentication: Clerk integration
2. Project Setup
Installation
npm create convex@latest
Project Structure
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
'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 API)
// 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: 'noreply@example.com' },
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 Summary
- Convex: Realtime backend
- Type Safety: End-to-End TypeScript
- Realtime: Automatic subscriptions
- Server Functions: Query, Mutation, Action
- File Storage: Built-in
- Authentication: Clerk integration
Implementation Checklist
- Create Convex project
- Define schema
- Implement queries
- Implement mutations
- Integrate React
- Implement actions
- Implement file storage
Related Articles
- Complete Supabase Guide
- Complete tRPC Guide
- Complete Firebase Guide
Keywords Covered
Convex, Backend, Realtime, TypeScript, React, Serverless, Database
Frequently Asked Questions (FAQ)
Q. How does it compare to Firebase?
A. Convex has much better type safety. Firebase is more mature with more features.
Q. How does it compare to Supabase?
A. Convex has more powerful realtime features. Supabase is PostgreSQL-based and more flexible.
Q. Can I use it for free?
A. Yes, there’s a free plan. Free up to 1GB data and 1GB file storage.
Q. Is it safe to use in production?
A. Yes, many startups are using it.
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Complete guide to building realtime backend with Convex.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- Supabase 가이드 — Firebase 대체 오픈소스 BaaS, Postgres + Auth +
- tRPC 가이드 — End-to-end 타입 안전 API, REST·GraphQL 없이 TypeScript만
Keywords Covered in This Article (Related Search Terms)
This article covers Convex, Backend, Realtime, TypeScript, React, Serverless, Database.