All posts
convexdatabaserealtime

Convex: A Practical Guide for Full-Stack Developers

A practical guide to Convex — the reactive backend-as-a-database with real-time queries, transactional mutations, and TypeScript-native functions.

SR

Suhail Roushan

August 6, 2026

·
5 min read
·
0 views

Convex starts from a different premise than most databases: what if every query in your app was reactive by default, updating automatically when the underlying data changes, without you writing any subscription or invalidation logic yourself?

Convex is a backend platform combining a transactional document database with server-side functions (queries, mutations, actions) written directly in TypeScript, and a reactive query system that automatically pushes updates to connected clients when relevant data changes. It blurs the line between "database" and "backend" — your data layer and your business logic live in the same TypeScript codebase, deployed together.

Why Convex Matters (and When to Skip It)

Building real-time features traditionally means manually wiring WebSocket subscriptions, cache invalidation, and re-fetch logic for every piece of data that needs to stay live. Convex's queries are reactive by default — a React component using a Convex query hook automatically re-renders when the underlying data changes, with zero manual subscription code. This collapses a huge amount of real-time application complexity into "just write a normal query."

Skip Convex if you need a traditional relational data model with complex multi-table joins and SQL-standard querying — Convex's document model and query API are different enough from SQL that teams with deep existing SQL/relational investment may find the migration cost not worth it without a specific reactive-data need driving the decision.

Getting Started with Convex

Define a query and a mutation as TypeScript functions:

// convex/posts.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";

export const list = query({
  handler: async (ctx) => {
    return await ctx.db.query("posts").order("desc").take(20);
  },
});

export const create = mutation({
  args: { title: v.string(), authorId: v.id("users") },
  handler: async (ctx, args) => {
    return await ctx.db.insert("posts", { title: args.title, authorId: args.authorId });
  },
});

Use it reactively from a React client:

import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

function PostList() {
  const posts = useQuery(api.posts.list); // auto-updates on any change
  const createPost = useMutation(api.posts.create);

  return <ul>{posts?.map((p) => <li key={p._id}>{p.title}</li>)}</ul>;
}

Core Convex Concepts Every Developer Should Know

Queries are automatically reactive — no manual subscription setup. Convex tracks exactly which data a query reads and re-runs it (pushing the new result to subscribed clients) whenever that underlying data changes, giving you real-time UI updates as a default behavior rather than an opt-in feature you build yourself.

Mutations are fully transactional. Every mutation runs as an atomic transaction against the database — multiple writes inside one mutation function either all succeed or all roll back, the same guarantee as a SQL transaction, without you managing transaction boundaries explicitly:

export const transferCredits = mutation({
  args: { fromId: v.id("users"), toId: v.id("users"), amount: v.number() },
  handler: async (ctx, { fromId, toId, amount }) => {
    const from = await ctx.db.get(fromId);
    const to = await ctx.db.get(toId);
    await ctx.db.patch(fromId, { credits: from!.credits - amount });
    await ctx.db.patch(toId, { credits: to!.credits + amount });
  },
});

Actions handle non-deterministic work (external API calls, sending emails) that doesn't belong in a transactional mutation — Convex separates pure, transactional database logic (queries/mutations) from side-effecting work (actions) as a deliberate architectural boundary.

Schema validation happens at the function level using Convex's validators (v.string(), v.id(), etc.), giving you runtime type safety on function arguments in addition to TypeScript's compile-time checking.

Common Convex Mistakes and How to Fix Them

Mistake 1: putting side-effecting logic (external API calls) inside a mutation. Mutations are meant to be deterministic and transactional — calling an external API inside one breaks that guarantee and can cause issues on retry. Fix: use actions for anything involving external calls or non-determinism, and have actions call mutations for the actual data writes.

Mistake 2: not indexing fields used in query filters. Same principle as any database — Convex queries filtering on unindexed fields scan more data than necessary. Fix: define indexes in your schema for fields you filter or sort on regularly.

Mistake 3: treating Convex's document model like a relational database and over-normalizing. Splitting data across many small documents that constantly need to be joined fights against Convex's strengths. Fix: model data closer to how it's actually read together, embedding where it makes sense, similar to general document-database modeling principles.

When Should You Use Convex Instead of a Traditional Backend + Database?

Use Convex when real-time reactivity is a core requirement (collaborative apps, live dashboards, chat) and you want your data layer and backend functions unified in one TypeScript codebase without building the real-time plumbing yourself. Use a traditional backend + database (Postgres + a REST/GraphQL API) when you need SQL-standard relational querying, are integrating with an existing non-TypeScript backend ecosystem, or don't have a strong real-time requirement driving the architecture.

Convex in Production

Lean into actions for anything touching third-party services, and keep mutations focused purely on data writes — this separation keeps your transactional guarantees clean and makes retry behavior predictable. Also design your schema and indexes deliberately from the start, the same discipline as any production database, since Convex's flexibility during prototyping can mask index needs that show up under real query volume.

If your app has a genuine real-time requirement — live collaboration, presence, dashboards that need to update without polling — Convex is worth evaluating specifically for how much subscription/invalidation code it eliminates versus a traditional stack.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch