Convex: A Functional DB Approach In A Dysfunctional World.
I've spent the last few projects using Convex, which is a funny thing to say because it sounds like I chose this path, like I sat down with a spreadsheet and did a careful analysis. That's not what happened. What actually happened is I kept making the same mistakes over and over again with Firebase and Supabase, and eventually someone on the internet was like "hey, there's a thing called Convex," and I was like "oh thank God, another option to feel uncertain about."
The Origin Story: Why Convex? Or: Why Am I Like This?
Okay, so here's what happened. I used Firebase for a while. Firebase is great. It feels great. Everything just works and your app updates in real-time and you feel like a genius. You know that feeling when you're at a party and you're telling a story and everyone's laughing and you think "wow, I'm killing this"? That's Firebase. That's the party where everything's going well.
And then you realize you need to do something that Firebase isn't great at. Like a join. You know, basic relational data. The thing that's been solved since the 1970s. And Firebase is like "uhhhh, multiple queries?" and you're like "oh sure, yes, I'll just make the app slower and more complicated." And then you hit the query and document size limits—1 MiB per document—and you think "why am I like this?"
The security rules in Firebase are their own special kind of nightmare. They're scattered all over the place, and you're supposed to reason about what's actually locked down, and it's like trying to understand a legal contract written by someone who learned English from Reddit comments.
So I tried Supabase. Supabase is like the responsible older sibling. It's got SQL via Postgress, which is nice. It's got power. It's got all the things Firebase doesn't have. But also, like most structured databases, it has schema migrations. Adding this extra chore to your development flow, especially when you're a tiny team, is my least favorite activity that isn't directly causing physical pain.
And the realtime scaling costs? Row-level security policies? They're all real, and they all add friction, and at some point you're sitting at midnight trying to debug RLS rules and you're like "I could be doing literally anything else right now."
Then someone told me about Convex. And I was skeptical. I was like "another backend? Really? This is the third time I'm doing this. This is like that bit where you keep ordering the same thing at a restaurant and it keeps being bad, but you keep going back." Except this time—and I swear this is true—the data recipes are actually tasty. It was the first time I picked a backend and didn't immediately think "oh no, what have I done?"
If you want the technical explanation of what Convex actually is, their docs have that. I'm just here to explain my feelings about it.
Why Convex Clicks for Me
Or: The Part Where I Stop Complaining and Actually Explain Something
It's Just JavaScript Functions, Which Sounds Simple But Also Sounds Like a Trap
Okay, so with Convex, you write TypeScript functions. That's your entire backend. You write a query function, that's your query. You write a mutation function, that's your mutation. Your real-time subscriptions? Also functions.
I know what you're thinking: "That sounds too good to be true." Yeah, it kind of is, actually, but not in the way you think. It's not a trap. It's just... good. You don't have to learn a query language. You don't have to SSH into a server. You don't have to think about infrastructure. You just write JavaScript.
Enforcing a functional api for your datalayer is something you start to appreciate when you've seen multiple data layer queries written in NextJS UI logic (which both Firebase and Supabase allow) and you wonder if that Stanford ML CTO who hired you should have really been given keys to the kingdom.
And then—this is the part that messed with my head the first time—the real-time reactive model just works. You define a query, you subscribe to it, and when the data changes, your component re-renders. You don't have to think about invalidation. You don't have to think about polling. You don't have to think about WebSockets. It's all just... handled.
I kept waiting for the other shoe to drop. Like "surely there's a catch," but I've been using it for a while now and the shoe is still up in the air. It's unsettling. I'm not used to things working this smoothly.
Another thing—and I say this as someone who has spent embarrassing amounts of time debugging eventual consistency bugs in production—is that Convex gives you strong consistency and ACID transactions. Your data is correct. Not "probably correct." Not "eventually correct." Correct. Immediately. It's wild.
The Data Model That's Like... Good?
So you know how Firestore is like "just throw everything in documents"? And SQL is like "make seventeen tables and normalize everything"? Convex is like "what if we did something in the middle," and I was like "that's a good idea actually."
You store documents with explicit ID references instead of trying to nest things or pretend relationships don't exist. It's type-safe. You can traverse them. And here's the key thing: it doesn't make you want to scream.
With Firestore, you either nest data (which breaks when you try to update it from multiple places) or you lose the relationship entirely and have to manually join things. With SQL, you get powerful joins but you're contractually obligated to write migrations forever, which is like being stuck in a relationship where you have to keep apologizing for things your past self did.
Convex gives you explicit Id<"table"> references that are type-safe and work. It's honestly refreshing.
You Can Start Organized and Never Get Locked Into It
Okay, so I actually do plan ahead. I sit down at the start of a project and I write out my schema. I think about my data structures. I'm organized about it. And this is where Convex stops being annoying and becomes genuinely useful.
With most backends, updating your schema isn't forbidden—it's just miserable. Let's say you're using Supabase. You change your mind about something three weeks in. You need to add a field, or remove one, or change a type. So you write a migration. You test it locally. You think about data compatibility. You run it on production. You hope nothing breaks. You're keeping track of which migrations have run where. You're dealing with versioning and rollbacks and the whole ceremony. It's not that you can't change your schema. It's that every change requires this ritual.
But with Convex, you can design your schema thoughtfully at the start and then actually change it without ceremony. Your schema.ts file is just TypeScript. You can add validators, remove fields, restructure things—all at build time. Your tools tell you what breaks. You fix it. You move on.
Even better: if you do remove an entity property early on, everything's TypeScript, so your type system catches it at build time and tells you exactly what needs to change. No surprise runtime errors. No migrations in the dark. Just "oh, this is now unused, let me clean it up."
This is fundamentally different from every other approach. With SQL, you're married to your decisions. With Firebase, you're not thinking about schema at all and then you regret it. With Convex, you can be thoughtful upfront but then stay flexible as you learn. It's like having all the benefits of planning without any of the rigidity. It means you can move fast, stay organized, and actually evolve your thinking without getting stuck.
Bonus: The MCP Dev Server Actually Helps
Here's something that shouldn't be surprising but is: Convex was built in the age of AI, and it shows. They've got an MCP dev server that's legitimately useful. When you do a schema upgrade and accidentally break your staging environment, you can use the MCP server to reach into the void and actually debug what's happening. You get real introspection into your data and your functions.
I would've killed for that kind of insight back when I was working with Firebase. Being able to peer into what's actually going on without resorting to console logs and guessing is a game-changer.
The Things You're Going to Do Wrong and Then Learn From Them
It's Not SQL, and That's When You Realize You Actually Liked SQL
When you first start using Convex, you'll look for JOIN. There is no JOIN. And you'll be like "wait, what?" and then you'll be mad about it for like forty-five minutes.
But here's the thing: it's actually fine. You traverse relationships through IDs. It's not as elegant as a SQL query, but it's clearer and you have full control. Instead of GROUP BY, you just use JavaScript. Map, reduce, filter—things you already know how to do.
Reading data is about fetching documents and following their references. Once you accept that in your brain, it actually clicks and you realize you were just being precious about SQL.
How to Model Data Without Making Yourself Regret It
There's a temptation when you start with Convex to either embed everything (like Firebase) or over-normalize (like SQL). The answer is usually somewhere in between, which sounds like I'm saying "balance," which sounds inspirational, which is the last thing I want.
Here's what actually works:
// Users store their org ID
users: { _id, orgId, name }
// Memberships is separate
memberships: { userId, orgId, role }
// Orgs exist on their own
orgs: { _id, name }
The rule is: if you're going to query on it, store it as an ID and index it. Light denormalization is fine—like, cache orgName and role on the user if you're fetching it constantly. But don't embed entire arrays unless you're absolutely certain they won't change independently.
Think of it as "one query per screen." If you can get what you need in a single Convex function without doing seventeen post-processing steps, you've got good schema.
I learned this the hard way, which brings me to my next point.
You're Going to Forget About Indexes and Then Everything Will Be Slow
Indexes are not optional. I say that like I'm being emphatic, but really I'm trying to warn you from the future. Define an index for every field you're querying. Use .withIndex() instead of .filter() on big datasets.
It feels premature when you're starting out. You think "I'm only going to have a few documents." And then you don't, and suddenly everything is slow, and you're sitting there wondering why, and then you remember "oh right, I have to actually index things."
When you're writing reactive queries, watch your payload size. They re-run every time data changes. Bloated payloads compound. The pricing page has details if you want to understand the economics of it, but the TL;DR is: keep your payloads small if you're doing realtime stuff.
Your Schema Is Actually Going to Matter
Use validators like v.string() and v.id(). Version your schema alongside your code. You don't get migrations because Convex doesn't do them—instead, you get freedom and responsibility in equal measure, which is basically the premise of adulthood.
This requires discipline. I do a lot of work with AI models to produce structured output and the powers that be (Langchain) have decided Zod is the go-to type enforcer. I like defining my models in Zod first, then converting them over to Convex's validator types with their helper tools before including base entities in my schema. This way you can transact in consistent types across both your AI and your business logic.
Put Your Auth on the Server and Just... Leave It There
All data access goes through Convex functions. That's where you enforce permissions. That's it. There's no client-side auth logic to bypass. There's no security rules file where you can accidentally lock everyone out. You control the gate.
This is simpler and safer than Firebase security rules or Supabase RLS, which—and I say this with all the love in my heart—are ways of encoding your security concerns in a language that's actively trying to give you a migraine.
See Convex authentication to implement their in house auth system (be warned as of writing this its still in beta), or you can integrate Auth0, Clerk, or many of your favorite providers.
But the philosophy is simple: your function runs on the server, you check if the user is allowed to do the thing, and if they're not, you don't do the thing. Revolutionary stuff.
How Convex Compares to Other Things
Or: The Part Where I Actually Make a Chart
Convex vs SQL (Postgres, Supabase)
| Feature | SQL | Convex |
|---|---|---|
| Model | Tables and joins | JSON-like documents |
| Joins | Native ✅ | Via IDs 🔁 |
| Migrations | Required (ugh) | Auto with schema.ts |
| Realtime | You have to add it | Built-in |
| Auth | RLS policies | Server functions |
| Consistency | Strong | Strong |
| Dev speed | Slower | Fast |
SQL is powerful for complex queries and heavy analytics. Convex is powerful for apps that are mostly CRUD with realtime updates. If you're running SELECT COUNT(*) GROUP BY a lot, SQL is your friend. If you're building a collaborative app, Convex is going to make you happier.
Convex vs Firebase and MongoDB
| Feature | Firebase | MongoDB | Convex |
|---|---|---|---|
| Schema | None | Flexible | Optional + typed |
| Transactions | Partial (it's complicated) | Full | Full |
| Realtime | Yes | Plugin | Yes |
| Security | Client rules (yikes) | Custom logic | Server functions |
| Relationships | Weak (doesn't like commitment) | Manual (exhausting) | Explicit refs (healthy) |
| Setup | Hosted | Self-managed | Managed (or self-hosted) |
Convex vs Firebase and Convex vs MongoDB have more detailed comparisons if you want to get into the weeds. The short version: Firebase feels great until it doesn't. MongoDB is flexible but requires you to care about consistency. Convex tries to give you Firebase's speed with an actual backend's structure.
Also, if you absolutely need to run Convex in your own infrastructure for compliance reasons or because you're like "I need to own my infrastructure," you can self-host it. It's hosted by default, but the option exists.
The Real Story: How I Made Mistakes and Paid For Them
Alright, so early on—and I'm not proud of this, but I'm also not that embarrassed—I stored a giant memberships array directly on the user document.
Think about what this means. Every time someone's role changed, that whole array got updated. And since queries are reactive by default, every client subscribed to that user got the whole array, all over again. And if you had like a hundred users, and you're showing a list of users with their memberships... well, you see where this is going. Everything was slow. Everything was terrible. I was confused about why.
The fix was obvious in retrospect—separate memberships into its own collection, index it properly, cache only the small, stable stuff on the user. Clean separation of concerns. Lean payloads. Everything fast again.
Here's what I carry forward:
- Filter it? Index it. Don't try to be clever about lazy loading or "it'll be fine."
- Is it going to be live? Keep it small. Reactive queries re-run constantly. Your payloads should reflect that.
- Is it stable? Okay to denormalize. Caching
orgNameon a user is fine if it basically never changes.
The result was faster updates, smaller payloads, and cleaner auth logic. It's not glamorous. It's just good engineering.
When Convex Is the Wrong Call
Or: I'm Not Going to Pretend This Works for Everything
If you're running heavy analytical queries or massive GROUP BY operations, Convex isn't your answer. It doesn't have a SQL engine under the hood, so you fetch data and process it in JavaScript. That's fine for small datasets but it falls apart at scale. At that point, just use Postgres.
If you're inheriting a large legacy SQL schema that already fits cleanly into Postgres, forcing it into Convex would be stupid. Respect the previous decision.
And one last thing: Convex is managed by default, which is great for most projects because you don't have to think about it. But if you work somewhere that requires on-premises infrastructure, their self-hosting guide explains how to run it yourself. It's an option.
The Conclusion That I've Been Circling Around This Whole Time
Convex isn't SQL. It doesn't pretend to be. And that's what makes it actually good for the kinds of things I'm building.
The way to think about it: design with documents and IDs. Add indexes early. Let your schema evolve. Keep auth on the server. Think about payload size. Don't embed arrays unless you really need to. Get comfortable not writing SQL.
If you do those things, you get something rare: a backend that's faster to build with than SQL, safer than unstructured NoSQL, and realtime by default. You get to write JavaScript functions and have them just work. You get to move fast without painting yourself into a corner.
That's the sweet spot. That's what I've been looking for this whole time without knowing it.
Resources (If You Want to Actually Read the Documentation)
- Convex Docs Overview
- Understanding Convex
- Relational Data in Convex
- Best Practices
- Schema Philosophy
- Document IDs and References
- Reading Data and Queries
- Indexes and Query Performance
- Database Schemas
- Authentication
- Optimistic Concurrency Control (ACID Transactions)
- Pricing
- Convex vs Firebase
- Convex vs MongoDB
- Self-Hosting Guide
- Firebase Firestore Joins
- Firebase Firestore Quotas
- Supabase Realtime Quotas