Backend · TypeScript

Why one of TypeScript's loudest Convex skeptics rebuilt his whole product on it, and what the rest of us can learn from the switch.

Few developers have spent more energy arguing for modular, swap-anything tooling than Theo Browne, the creator behind t3.gg and the person who coined the "T3 stack." So when he published a long, unscripted breakdown of why he now runs T3 Chat entirely on Convex, it was worth paying attention. Not because a popular creator likes a product, but because the reasons he gives map neatly onto problems almost every full-stack TypeScript team has quietly accepted as normal.

This piece walks through his argument, the parts I find genuinely convincing, and the places where you should still say no.

Disclosure context: Theo states that Convex sponsors ad slots on his channel, but that this particular video was not sponsored by them and that he was free to criticize the product. Read his enthusiasm with that in mind, as you should with any vendor story.

Starting from the skeptic's side

The interesting part of this story is where it begins. Theo's whole philosophy was built around three non-negotiables: modular pieces you can swap out, full-stack type safety, and a single source of truth for anything your code describes. That philosophy is why tRPC clicked for him back in 2021. You write a function on the server, call it on the client, and the types flow across the boundary automatically.

When he first heard about Convex around that same time, it sounded like the opposite of everything he valued. To him it looked like an all-in-one framework, closer to Rails than to a toolkit, wrapped inside a closed-source startup that had borrowed ideas from open source. He wrote it off as something for toy apps and held that position for roughly four years.

Two things moved him. First, people whose judgment he trusts, including React Query creator Tanner Linsley, started taking Convex seriously. Second, he learned the platform had been fully open-sourced and that its team actively keeps it self-hostable. The lock-in objection, his biggest one, started to look weaker than he assumed.

The click: infrastructure that is actually code

The moment it really landed for him was structural. A Convex project has a single convex/ folder. Your schema lives there. Your auth config lives there. Your file storage, your queries, your mutations, all of it lives there as TypeScript.

His realization was that this makes Convex the closest thing to true infrastructure-as-code he had seen. Not YAML glued to a dashboard. Not a Terraform plan that drifts from what is actually deployed. Hand that folder to another developer and they can stand up the same backend on their own account with essentially no clicking around.

If you want the database shaped differently, you change a file. That is the whole workflow.

Why this matters even more for AI

Theo ties this directly to AI app builders. In his experience, tools like Bolt, Lovable and v0 are great at scaffolding UI, but they routinely fail at things like adding auth or file uploads, because those tasks require signing up for services, pasting API keys and configuring dashboards. Language models are good at writing code and bad at clicking through IAM settings.

If every backend concern is just a TypeScript file, the model never has to leave the codebase. It does not need an MCP server to inspect your database schema; it reads the schema file. That insight led to Convex shipping Chef, an AI app builder on top of Convex. Theo's demo was a generated Slack clone where he edited a message directly in the database dashboard and watched the running app update instantly, with no refresh and no sync code written by anyone.

For developers that feels like magic. For a non-developer, as he points out, it is simply how they would expect software to behave in the first place.

"React for your backend" is the right analogy

The core thesis is that Convex is not an ORM, and not a Firebase clone. It is an alternative set of primitives, in the same way React was.

What React replaced

Hunting for DOM nodes by ID, checking an element exists before touching it, and keeping state in one place and UI in another.

What Convex replaces

Manual cache invalidation, hand-written sync layers, migrations and permission dashboards, and SQL living inside application code.

React did not add features on top of the browser; it gave you better building blocks, and once those clicked you stopped thinking about the old problems. Theo argues Convex does the same for data. The one big difference: React could be adopted one component at a time, while Convex tends to be your whole data layer from day one.

What the code actually looks like

Here is a simplified example in the shape Theo walks through: list messages in a channel, along with each author's name.

// convex/messages.ts
export const list = query({
  args: { channelId: v.id("channels") },
  handler: async (ctx, args) => {
    const messages = await ctx.db
      .query("messages")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .collect();

    return Promise.all(
      messages.map(async (m) => {
        const author = await ctx.db.get(m.authorId);
        return { ...m, authorName: author?.name ?? "Unknown" };
      })
    );
  },
});

// In a React component
const messages = useQuery(api.messages.list, { channelId });

A few things stand out. The argument validator checks that channelId is a real ID from the channels table before your handler even runs. There is no SQL join; you just loop and fetch. And on the client, sending a new message is a single useMutation call with no "on success, invalidate this query" step. The list updates by itself.

Why the "naive" code is fast

Any experienced backend developer will look at awaits inside a map and wince. Theo's explanation is that this code does not run on Node. Convex runs functions in its own Rust-based runtime that, in his description, treats your TypeScript almost like a compiler input: it flattens the reads and writes into a single transaction. Mutations either fully commit or fully roll back, so a failure on step 20 of 20 never leaves partial state behind.

He also mentions small details, like the runtime keeping values such as the current date consistent across a whole request, so sloppy code does not produce subtly mismatched timestamps.

Where the live updates come from

The same introspection that builds the transaction also builds a cache key. Convex knows which rows a query depended on, so when a write touches any of them, that key is invalidated and fresh data is pushed to subscribed clients. That is why editing a row directly in the dashboard updates the UI too. Theo says the large majority of T3 Chat's queries are served from that cache without touching the database at all.

End-to-end types, with errors in the right place

This is the tRPC magic carried over. Convex does generate code, but the generated files point at your source types rather than copying them. Rename a field in a query's return value and the error appears in the component that consumes it, immediately, even before saving and even with the dev server stopped.

Theo's point is that the error shows up where the breakage actually manifests. That is great for humans and, again, even better for AI agents, which otherwise tend to chase type errors through several files before finding the real problem.

A lesson from AI streaming

One of the more useful sections is about what Convex does not do. Theo originally wanted every streamed AI token to flow through the database to the user. Sync engines are a bad fit for that: they resend the whole updated row, not the diff, and storing each token as its own row is absurd at the scale of a chat product.

His solution, inspired by conversations with the Convex team, was to separate two concerns:

  • The finished message is written to Convex and synced to every client like normal data.
  • The in-progress stream is treated as an optimistic update, sent via server-sent events and backed by Redis pub/sub so it can be resumed if you open the chat in another tab.

It is a nice example of a platform being honest about its limits, and of good primitives leaving room for you to solve the remaining piece yourself.

When you should not use Convex

Theo is clear that this is not a universal answer. His rule of thumb: if your database is used only by your app and your backend, and both are TypeScript, it is hard to justify anything else. It stops making sense when:

  • You have a separate backend written in Go, Rust or another language that needs the same data.
  • Multiple teams or services need direct access to one shared database.

For analytics, his take is blunt: do not point your data team at your application database at all. Stream the data out to a dedicated analytics store and let each system do its own job.

The common objections

"Isn't this just Firebase, querying the database from the client?"

No. Clients cannot touch tables directly. The only way in or out is through queries and mutations you define on the server. That closes off the whole class of "forgot to lock down a table" breaches that have hit Firebase apps.

"It's a startup. Will it scale?"

Theo notes that Convex's founders previously worked on scaling infrastructure at Dropbox, and that Convex now runs on PlanetScale. T3 Chat, a product he says has overwhelmed other databases under load, runs on it today.

"What about lock-in?"

This is the most thoughtful part of the argument. He separates lock-in into two kinds:

  • Provider-specific code you would have to delete and rewrite to leave.
  • Work the platform saves you, which you would have to write yourself if you left, but which you would have had to write anyway without it.

Convex has some of both. The convex/ folder is Convex-specific. But because the backend is open source and self-hostable, leaving the hosted service does not mean leaving the code. His honest framing: much of what Convex provides is stuff he had already built himself, badly, before switching.

"What does it cost?"

Theo reports that Convex costs T3 Chat around $600 a month, while the switch has cut a larger amount from their Vercel and PlanetScale bills combined. Your numbers will differ, but it is a useful data point from a high-traffic app.

The rough edges

To his credit, Theo does not pretend it is perfect. His gripes are small but real:

  • useQuery shares a name with React Query's hook, so you can never tell at a glance which one a file is using.
  • Skipping a query means passing the magic string "skip" instead of a config object.
  • The hook returns undefined while loading and throws on error, rather than giving you separate loading and error states.
  • The validator is imported as v, which collides with Valibot's common alias.

My take

The strongest thing about this story is not that a well-known developer changed his mind. It is the specific reason he did. He realized he had been hand-building a worse version of the same system: type-safe endpoints, a sync layer, cache invalidation, websocket fan-out, permission checks. Each piece was reasonable on its own. Together they were a full-time maintenance burden that only he understood.

If you are a TypeScript team shipping a product whose data lives inside that product, Convex deserves a real evaluation, not a dismissal based on what it looked like years ago. If your data is shared across languages and teams, his own advice is to pass. Either way, the "React for your backend" framing is a useful lens for judging any data tool: does it give you better primitives, or just more configuration?

Written by Lyle Heartman. Based on Theo Browne's video discussion of his move to Convex on the t3.gg channel. Claims about T3 Chat's costs, performance and architecture are as described by Theo.