본문으로 건너뛰기 The Complete Convex Guide | Real-Time Backend, Type Safety, React, Serverless, Production Use

The Complete Convex Guide | Real-Time Backend, Type Safety, React, Serverless, Production Use

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

  • 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.