🧠 3-tier learning page: https://dev48v.infy.uk/tech/day44-convex.html
Day 44 of my TechFromZero series. Today: Convex — the reactive backend that compresses Postgres + Prisma + Express + Socket.io + Redis into a single product, and lets you ship realtime collab features in literally 3 lines of React.
Why it matters
Building a realtime collab feature today usually means:
- Set up Postgres (Supabase / Neon / Railway)
- Add Prisma + migrations
- Stand up an Express / Hono server
- Wire WebSocket auth + reconnect logic
- Build a subscribe/unsubscribe lifecycle
- Add optimistic UI
That's ~3 days of plumbing before the first feature ships. Convex collapses the entire stack:
┌────────────────────────────────────────────┐
│ useQuery + useMutation (React) │ 3 lines of React
└──────────────────┬─────────────────────────┘
│ WebSocket (handled for you)
┌──────────────────▼─────────────────────────┐
│ query() / mutation() / action() (TS) │
│ + typed document DB │ ← One product
└────────────────────────────────────────────┘
The 3 React lines
"use client";
import { useQuery, useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
const count = useQuery(api.counters.get, { name: "global" }) ?? 0;
const bump = useMutation(api.counters.bump);
<button onClick={() => bump({ name: "global" })}>
Count: {count}
</button>
Open the page in 5 tabs. Click in one. All 5 update instantly. No subscribe code. No retry logic. No optimistic-UI plumbing. Convex's client handles every transport detail.
The backend (one schema + one query + one mutation)
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
counters: defineTable({
name: v.string(),
value: v.number(),
}).index("by_name", ["name"]),
});
// convex/counters.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const get = query({
args: { name: v.string() },
handler: async (ctx, { name }) => {
const c = await ctx.db
.query("counters")
.withIndex("by_name", q => q.eq("name", name))
.first();
return c?.value ?? 0;
}
});
export const bump = mutation({
args: { name: v.string() },
handler: async (ctx, { name }) => {
const existing = await ctx.db.query("counters")
.withIndex("by_name", q => q.eq("name", name))
.first();
if (existing) {
await ctx.db.patch(existing._id, { value: existing.value + 1 });
} else {
await ctx.db.insert("counters", { name, value: 1 });
}
}
});
That's the whole backend. Two TypeScript files, ~30 lines.
Three function types — query / mutation / action
| Type | Reads DB | Writes DB | Calls external APIs | Auto-subscribed |
|---|---|---|---|---|
query() |
✅ | ❌ | ❌ | ✅ (the magic) |
mutation() |
✅ | ✅ (in tx) | ❌ | — |
action() |
✅ via runQuery | ✅ via runMutation | ✅ (fetch, etc.) | — |
query() is the heart of the magic — Convex tracks which rows your query function reads, and when any mutation later writes those rows, every subscribed client gets pushed the new result automatically.
action() is for external API calls (LLMs, Stripe, fetches) since queries + mutations are deterministic-replay. Actions call back into mutations to commit results.
What you skip
- Postgres setup + Prisma migrations — 2 hours
- Express + WebSocket auth — 1 day
- subscribe/unsubscribe lifecycle — half day
- Optimistic UI plumbing — 1 day
Total saved per realtime feature: ~4 days.
When NOT to use Convex
| Skip Convex | Use |
|---|---|
| 100M+ row OLAP queries | Postgres + Clickhouse |
| Video streaming / large blob storage | S3 + Cloudflare R2 |
| Strict SQL joins on 10+ tables | Postgres + Prisma |
| Regulated data (HIPAA self-host) | Self-hosted Postgres |
For everything else — collab apps, AI chat, dashboards, MVPs, hackathons — Convex is the highest-leverage backend choice in 2025.
Bonus: vector search built in
You don't need Pinecone for small/medium RAG. Convex has native vector indexes:
defineTable({
embedding: v.array(v.float64()),
text: v.string(),
}).vectorIndex("by_embedding", {
vectorField: "embedding",
dimensions: 1536,
});
// Search:
const results = await ctx.vectorSearch(
"messages",
"by_embedding",
{ vector: queryEmbedding, limit: 5 }
);
Free tier covers most prototypes.
Try it now
The 3-tier learning page has a live simulated collab counter + 9 click-through steps on each piece (schema, query, mutation, useQuery, useMutation, action, vector search, deploy):
https://dev48v.infy.uk/tech/day44-convex.html
Build it in 20 minutes
npx create-next-app@latest convex-from-zero --typescript --tailwind --app
cd convex-from-zero
npm install convex
npx convex dev # opens browser, log in, free tier
Convex dev starts a watcher that pushes your convex/*.ts files to your hosted dev deployment as you save. Free tier: 2 GB storage, 1M function calls / month.
What's next in TechFromZero
Day 45 picks tomorrow. Probably TanStack Start (new SSR framework) or View Transitions API (animated native page transitions).
🌐 All days: https://dev48v.infy.uk/techfromzero.php
Top comments (0)