The Little Differences: Implementing Search/Pagination in Convex Without The Extra Documentation Trip To Paris

The Little Differences: Implementing Search/Pagination in Convex Without The Extra Documentation Trip To Paris
What Vincent Vega and search pagination have in common

Picture this: I'm sitting in my home office in Utah, staring at a React Native app with a list of forms that's about to become a performance nightmare, and I'm doing what every engineer does when they hit a wall—I'm making it worse by overthinking it.

I'm building this forms management app, right? Users need to search through their organization's forms. Simple enough. And I'm thinking, "I'll just fetch all the forms and filter them client-side. How hard could it be?"

Which is like saying "How hard could skydiving be?" right before you realize you forgot the parachute.

The "I'll Just Do It Myself" Phase (Hubris: A Developer Story)

So here's my initial plan: I've got Convex as my backend—which I'm a total fanboy for, by the way, we'll get to that—and I write this beautiful useForms hook that fetches every single form for an organization. Then I do fuzzy matching on the client.

It felt very productive. Very engineer-y. I could hear Sam Jackson's Jules from Pulp Fiction shouting "Look at the big brain on Justin!"

Except I wasn't solving problems. I was creating them with aggressive complexity.

You know that scene in Pulp Fiction where Vincent Vega is explaining the little differences in Europe to Jules? "It's the little things," he says, talking about how they call a Quarter Pounder a "Royale with Cheese" because of the metric system. That whole conversation is about noticing the details that make a difference.

That's what I completely missed about Convex. I was spending time "solving" search that I didn't notice Convex had already solved it. With better solutions. And helper functions. So many beautiful helper functions. You know, so I can focus on making things my user needs to prove my idea actually has legs.

But we'll get there. First, let me tell you about all the ways my "fancy" solution was actually terrible.

Why Client-Side Filtering is Like Bringing Sand to the Beach

Working with Cursor, its initial approach had a few... let's call them "characteristics":

The data problem: Cursor's first approach was pulling every form over the wire. All of them. For a test org with 20 forms, this feels fine. For a real organization with 500 forms? I'm basically asking someone's phone to download a small novel just to search for "Q4 Budget."

This is why AI coding still needs adult supervision. Please, please, please treat anything generated by any AI tool as you would the code of a fresh out of college Jr Dev. But I digress.

The performance problem: Now our form list implementation is primed to make the user's device do all the fuzzy matching work. Which is great if you hate your users' battery life.

The future problem: When (not if) I need to add server-side sorting, role-based filtering, or any other feature, I've painted myself into a corner. I'll have to rewrite everything anyway.

But the worst part? As soon as I saw the plan of my AI Jr. Dev I knew it was bad architecture. I just kept thinking, "Well, if I move this to the server, I'll have to hand-roll all the text matching logic. I'll have to figure out ranking. I'll have to—"

And that's when I realized I was about to reinvent a half-baked search engine in my Convex queries.

It's like that moment when you're trying to fix something in your apartment and you realize you're basically rebuilding the entire thing. "I just wanted to hang a picture, now I'm replastering the wall."

The Moment Cursor Decided to Parent Me (Because I Obviously Need Help)

Here's where things get interesting. And by interesting, I mean "embarrassing in a way that makes me question my entire career."

I'm in Cursor, and I instruct cursor to rethink its plan on the server side, and then it does something unexpected. It reads the docs, which thanks to Convex's MCP server it has ready access to.

For those not terminally on AI twitter, Model Context Protocol is this protocol that lets AI assistants actually understand external tools and services. Convex ships an MCP server, which means Cursor knows about Convex's capabilities like it knows about JavaScript syntax.

So I'm prompting a mangled series of characters on how it should do backend search, and Cursor basically says, "Hey, you know Convex has full-text search built in, right?"

I'm staring at my screen like, "What?"

It's the developer equivalent of your GPS interrupting your wrong turn with "Actually, there's a much better route" except you've been ignoring GPS for the last twenty minutes because you're "pretty sure" you know the way.

Cursor proceeds to show me—unprompted—that Convex has .search(), .withSearchIndex(), and filterFields as first-class features.

This is the big advantage Convex has over other backends as a service. It's not just the easy deploy. Its that by default Convex is AI first in the availability of its documentation and even runtime logs via its MCP server as well as TS first codebase.

The MCP Revelation (Or: When Your Tools Know More Than You Do)

Here's what Cursor showed me through Convex's MCP server:

You can define a search index in your schema:

forms.searchIndex("search_name", {
  searchField: "name",
  filterFields: ["organizationId", "deletedAt"]
})

Then in your query:

.withSearchIndex("search_name", q =>
  q.search("name", query)
   .eq("organizationId", orgId)
   .eq("deletedAt", null)
)

And that's it. Convex handles tokenization, relevance ranking, prefix matching for type-ahead. All of it.

The MCP server integration means Cursor understands Convex's API surface as a first-class citizen. It's not guessing based on training data from 2023. It's reading the actual, current Convex documentation and API definitions in real-time.

This is the "little things" moment. Not just that Convex built search properly. Not just that they documented it well. But that they built an MCP server so your coding assistant automatically knows about it. This is how MCP and Convex turn your Jr. Dev coding assistant (Cursor in my case) into something actually useful.

It's like Vincent explaining the metric system—these details seem small until you realize they completely change how the whole system works.

But Wait, There's More (The Pagination Problem)

So I'm feeling pretty good about myself. I've got search working. The app feels snappy. Forms filter as you type. I'm basically a genius who occasionally takes good advice from his AI coding assistant.

Then my reflexes started to tingle: what happens when people start using this and the 5 forms I'm testing on become 500?

And this wasn't paranoia or over optimization. In my experience when you give people a tool that works, they use it. Aggressively. Users will start creating hundreds of forms. And my beautiful search solution will start showing its age.

I immediately recognized I needed pagination, and this was a moment where an ounce of prevention now would outweigh a pound of a cure later. Of course I needed pagination.

And here's where I expected pain. Because pagination is usually a nightmare, right? You've got to manage page state, and cursors, and edge cases, and coordinate between your frontend and backend, and—

Convex (Via Cursor) Has Your Back (Again)

I start typing out my query, thinking about how I'm going to implement pagination, and Cursor interrupts again.

"You can use .paginate(paginationOpts) instead of .collect()."

I stop. I read the suggestion. Through the MCP server, Cursor is showing me the exact API I need, with proper types, with the correct return shape.

The backend changes are minimal:

// Your args now include pagination
args: { 
  organizationId: v.string(), 
  searchQuery: v.optional(v.string()),
  paginationOpts: paginationOptsValidator 
}

// Your return type changes to include page info
return await query
  .withSearchIndex("search_name", q =>
    q.search("name", searchQuery)
     .eq("organizationId", organizationId)
     .eq("deletedAt", null)
  )
  .paginate(paginationOpts);

You get back a PaginationResult with page, isDone, and continueCursor. Convex handles all the cursor logic, all the state management, all the "what happens when someone is on page 3 and a new form gets added" edge cases.

And because Cursor understands Convex's MCP server, it's not just suggesting this—it's explaining it. It shows me the types. It shows me the return shape. It shows me how to handle the cursor state in my React hook.

This is collaboration between tools that actually works. Cursor isn't just a fancy autocomplete. It's a pair programmer who's actually read the documentation.

My Royal With Cheese Moment (Or: What I Actually Learned)

Here's what I realized after all this: Convex isn't just a database. It's not even just a backend-as-a-service.

Convex is an opinionated platform that decided to actually solve common problems instead of making developers figure it out themselves. And then—and this is the crucial part—they built an MCP server so your AI coding tools actually know about these solutions.

Full-text search? Built in, and Cursor knows about it. Pagination? Built in, and Cursor knows about it. Real-time reactivity? Built in, and Cursor knows about it. Type safety with Zod validators? Built in, and Cursor knows about it.

This is the future of developer tools. Not just building great APIs, but making those APIs discoverable through the tools developers are actually using. The MCP integration means I don't have to remember every Convex feature. Cursor remembers for me. And more importantly, it suggests the right feature at the right time.

It's like they looked at all the things developers constantly reinvent badly and said, "What if we just... did this right, shipped it, and made sure AI coding assistants know it exists?"

Compare this to Firebase or Supabase. Those tools make you rebuild the same thing (search/pagination) over and over again and pay for the privilege. Your own everything. And when you're using Cursor or Copilot with them, you're stuck either:

  1. Googling for the right Stack Overflow answer
  2. Having your AI assistant hallucinate a solution based on outdated training data
  3. Reading docs manually like it's 2019

Convex respects my time. It lets me focus on my actual product instead of rebuilding search engines and pagination systems. And the MCP integration means the platform actively helps me discover its capabilities.

The Little Things (Considerations & Gotchas)

Of course, there are trade-offs. There always are.

Search is by relevance, not date: When someone searches, results come back ranked by relevance to the query, not by creation date. This is correct behavior for search, but it can surprise users who expect reverse chronological order.

Index limits exist: The search system has practical limits—around 1,024 documents scanned per search, and limits on term count and filters. For most apps this is fine, but it's worth knowing.

Pagination is cursor-based: You're not thinking in "page numbers" anymore. You're thinking in "continue from here." This is better for most use cases, but it means your UX needs to support infinite scroll or "load more" rather than "go to page 5."

Reactivity implications: Convex's real-time nature means as forms get added or deleted, your paginated results can shift under you. For most infinite scroll UIs this is fine—users are scrolling forward anyway. But it's worth understanding.

The biggest gotcha? You have to reset your cursor whenever your filters change. Search query changes? Reset cursor. Organization changes? Reset cursor. This is logical once you think about it—you're asking a different question, so you start from the beginning—but it's easy to miss if you're not careful.

Fortunately, Cursor (via the MCP server) reminded me about this when I was about to implement it wrong.

The End State (It's Actually Pretty Good)

Here's where I landed:

A forms list that uses proper server-side search, paginates automatically, loads 20 items at a time, shows skeleton states when loading, and handles infinite scroll smoothly.

The whole thing feels... professional. Like something you'd see in a production app from a real company, not a side project I threw together in React Native.

And the technical implementation isn't even that complicated. The code is readable. The architecture makes sense. Future Justin won't hate Past Justin for the decisions I made.

This is what Convex does well. It's not flashy. It's not revolutionary. It's just... thoughtful.

They looked at the metric system of backend development—all the little differences that add up—and they said, "What if we just called it a Royale with Cheese?"

What if we just made search work the way developers expect it to work? What if pagination was just a helper function instead of a multi-day project? What if the platform guided you toward good architecture instead of letting you shoot yourself in the foot? What if we built an MCP server so AI coding assistants actually know about all this?

It's the little things.

And honestly? The MCP integration might be the littlest thing that makes the biggest difference. Because it means I don't have to be smart enough to know every feature exists. My tools know. And they'll tell me. At exactly the right moment.

That's the kind of developer experience that makes you a fanboy.


What backend are you using, and does your AI coding assistant actually understand it? Drop a comment—and if you're not using Cursor yet, what are you even doing?