The paradox of choice is real. When your database provider announces they're shutting down, suddenly every database in existence becomes a candidate. Postgres. MySQL. SQLite. Document stores. Graph databases. Time-series databases. The list goes on.
I had 60 days to pick something, learn it, migrate to it, and deploy to production. Analysis paralysis wasn't an option.
So I made a list of what I actually needed, not what would be nice to have. Then I evaluated candidates against that list ruthlessly.
My Non-Negotiables
Before looking at any specific technology, I wrote down my requirements:
1. No self-hosting. I covered this in the first post, but it bears repeating. I'm a solo developer building a SaaS. Every hour I spend on infrastructure is an hour I'm not spending on features. I want someone else to handle scaling, backups, security patches, and 3 AM incidents.
2. TypeScript-first. My entire codebase is TypeScript. I want my database layer to be TypeScript too. Not "TypeScript-compatible" with a thin wrapper around SQL strings. Actually TypeScript, with real type inference and compile-time safety.
3. Good developer experience. This is subjective, but I know it when I see it. Clear documentation. Sensible defaults. Error messages that help you fix problems instead of sending you to Stack Overflow. A workflow that doesn't fight you at every step.
4. Vercel integration. My frontend runs on Vercel. I didn't want to deal with complex networking between my frontend and database. Bonus points for preview deployments that actually work.
5. Auth flexibility. Gel had built-in auth. I knew I'd lose that. But I needed whatever I chose to work with third-party auth solutions, ideally with minimal friction.
With these criteria in mind, I started evaluating.
The Alternatives I Considered
Supabase
Supabase was the obvious first candidate. It's Postgres under the hood, has a generous free tier, great documentation, and a passionate community.
What I liked:
Real-time subscriptions out of the box
Row-level security (similar to Gel's access policies)
Built-in auth
Good TypeScript support via generated types
What gave me pause:
I'd be back to writing SQL. After a year of EdgeQL, this felt like a step backward.
The real-time feature uses a different mechanism than regular queries. You're essentially managing two different ways of fetching data.
Connection pooling in serverless environments is a known pain point, even with their pooler.
Supabase is genuinely good. If I were starting fresh and didn't know what I was missing from EdgeQL, I'd probably be happy with it. But I did know, and I wasn't sure I wanted to go back to SQL.
PlanetScale
PlanetScale is MySQL, but serverless and with some clever features like branching and non-blocking schema changes.
What I liked:
True serverless (no connection pooling headaches)
Database branching for development
Good Vercel integration
What gave me pause:
MySQL. I've never loved MySQL's quirks.
No built-in real-time. I'd need to add that separately.
No built-in auth. Another thing to solve separately.
Their pricing model changed significantly in 2024, which made me nervous about long-term costs.
PlanetScale solves the serverless connection problem elegantly, but it's fundamentally still "just" a database. I'd need to build everything else myself.
Neon
Neon is serverless Postgres. It addresses the connection pooling issue that plagues Postgres in serverless environments and has some interesting features like branching and scale-to-zero.
What I liked:
Postgres compatibility (huge ecosystem)
True serverless with scale-to-zero
Branching for development/preview environments
Good pricing model
What gave me pause:
Still SQL. Same concern as Supabase.
No built-in real-time or auth.
Relatively new compared to the alternatives.
Neon is technically impressive, but it's solving a different problem than what I needed. It's making Postgres work better in serverless environments. I wanted something that rethought the entire backend stack.
Self-Hosting Gel
For completeness, I did briefly consider self-hosting Gel. It's open source, and you can run it on top of Postgres.
This lasted about five minutes. I already explained why self-hosting isn't for me. Moving from Gel Cloud to self-hosted Gel would trade one set of problems for another, worse set of problems.
Hard pass.
Why Convex Won
Then I found Convex. And something clicked.
Convex isn't trying to be a better database. It's trying to be a better backend. The database is just one piece of a larger, integrated system.
Here's what caught my attention:
Real-Time by Default
In Convex, every query is automatically reactive. When the underlying data changes, any component subscribed to that query updates automatically. No WebSocket configuration. No manual invalidation. No separate real-time API.
// This query automatically updates when the data changesexport const getProjects = query({ args: { userId: v.id("users") }, handler: async (ctx, { userId }) => { return await ctx.db .query("projects") .withIndex("by_userId", (q) => q.eq(
On the client side:
// useQuery subscribes to changes - when data updates, the component re-rendersconst projects = useQuery(api.projects.getProjects, { userId });
That's it. The data stays in sync automatically. After building UIs that manually refetch data after every mutation, this felt like magic.
TypeScript End-to-End
Convex is TypeScript all the way down. Your schema is TypeScript. Your queries are TypeScript. Your mutations are TypeScript. The types flow from the database to the client automatically.
// Schema definition - this IS your type systemconst projectsTable = zodTable("projects", { name: z.string(), userId: zid("users"), status: z.enum(["draft", "active", "archived"]), createdAt: z.number(),});// The query is fully typed based on your schemaexport const getProject = zQuery({ args: { projectId: zid
When you call this query from the client, TypeScript knows exactly what shape the data will be. Change a field name in your schema? Your IDE immediately shows you every place that needs updating.
This isn't a thin TypeScript wrapper over SQL strings. The type safety is real and pervasive.
No Connection Pooling Headaches
If you've ever deployed a traditional database in a serverless environment, you've hit the "too many connections" wall. Serverless functions spin up and down constantly, each wanting its own database connection. Connection pools help, but they add complexity and have their own failure modes.
Convex doesn't have this problem. There are no connections to manage. You call functions, and they run. The infrastructure handles everything else.
This might seem like a small thing, but it eliminates an entire category of production incidents. One less thing to debug at 3 AM.
Functions, Not Just Data
Convex isn't just a database. It's a backend platform. You have:
Queries: Read data, automatically reactive
Mutations: Write data, transactional
Actions: Run arbitrary code, call external APIs, use Node.js libraries
// A mutation that writes dataexport const createProject = zMutation({ args: { name: z.string(), status: z.enum(["draft", "active", "archived"]), }, returns: zid("projects"), handler: async (ctx, { name, status }) => { const user
Actions can call mutations, mutations can schedule actions, and everything is typed and transactional where appropriate. No separate API layer needed.
The Convex Helpers Ecosystem
The Convex community has built incredible tooling. The convex-helpers library provides utilities for common patterns:
Relationship helpers for traversing links between tables
Zod integration for runtime validation
Authentication helpers
And more
This ecosystem gave me confidence that I wasn't adopting something fringe. Real developers are building real things with Convex and sharing their solutions.
The Decision Moment
I spent an evening reading the Convex docs. Then I built a quick prototype: a simple CRUD app with real-time updates and authentication.
Within a few hours, I had something working. The real-time sync just... worked. The TypeScript inference just... worked. The deployment to Vercel just... worked.
I kept waiting for the catch. The hidden complexity. The "oh, but to do X you need to..." moment.
It didn't come.
The Concerns I Had
I'd be lying if I said I had no reservations. Convex is a bet on a specific platform, and that comes with risks.
Vendor Lock-In
Convex isn't just a database. It's a whole backend paradigm. If I ever need to leave, I can't just swap in a different database. I'd need to rewrite my entire backend layer.
I made peace with this by recognizing that: (1) vendor lock-in already happened with Gel, and I survived; (2) Convex is designed around patterns that aren't unique to Convex, so the knowledge transfers; and (3) the productivity gains are worth the risk.
Pricing at Scale
Convex's pricing is based on function calls and storage. For my current scale, it's very reasonable. But I don't have a clear picture of what happens when I 10x or 100x.
I decided to cross that bridge when I come to it. If my SaaS grows that much, I'll have the resources to handle whatever pricing looks like.
Learning Another System
I just spent a year learning Gel. Now I was going to spend time learning Convex. That's a lot of context switching.
But here's the thing: Convex is simpler than Gel in many ways. EdgeQL is its own language with its own syntax. Convex is just TypeScript. The learning curve was much gentler than I expected.
The Verdict
After a week of research and prototyping, I committed to Convex.
It wasn't because Convex was perfect. It's because Convex best fit my specific needs: serverless, TypeScript-first, real-time by default, great DX, and good Vercel integration.
Would I recommend Convex for everyone? No. If you love SQL and want maximum flexibility, Supabase or Neon might be better fits. If you need a specific database feature that Convex doesn't have, look elsewhere.
But for my use case, building a real-time SaaS as a solo developer, Convex was the right choice.
What's Next
Choosing Convex was the easy part. Now I had to actually migrate.
My Gel schema was full of nested objects, computed fields, and access policies. Convex has flat tables, explicit relationships, and no built-in row-level security.
In the next post, I'll show you how I flattened my schema from nested objects to flat tables, and what I learned about data modeling in the process.
The real work was just beginning.
Enjoyed this post?
Subscribe to get notified when I publish new articles. No spam, unsubscribe anytime.