My Journey with Convex

7 min read

August 28, 2025

I was very comfortable with creating API routes in Next.js to perform request/response operations rather than server actions. The simplicity and straightforward nature didn't appeal to me enough to pick them over my traditional way of using REST APIs. But, considering all the good traits from an economical standpoint, using server actions is way more understandable. But, considering all the benefits, I have decided to use server actions in my projects. The speed at which you can ship a feature is also increasing. Unless you need to create an API for clients to access without barriers or your project uses webhooks, there's no reason not to use server actions. It excels at working with server-side rendering and utilizing the same type system across the project.

Recently, I have been experimenting with the Convex tool. Honestly, the integration is even simpler in development and production. The simple architecture and automatic type generation help me focus on features instead of draining my entire energy to create types separately. It is neither creating API endpoints nor equivalent to server actions. It is different and the developer experience is off the chart. If you've used tRPC, you can relate to that. I have used tRPC and React Query in a few projects. It felt like a huge upgrade for me as a developer who uses either API route handlers and server actions to communicate with data in the database. The methods you write in both tRPC and Convex are completely accessible in the frontend. It prevents you from writing methods that aren't part of the project. As much as TypeScript forces you to write type-safe code, tRPC and Convex force you to write pre-coded methods in the backend.

In this blog I want to share my experience with Convex while I am keeping tRPC for some other day. Convex is a backend-as-a-service. It offers multiple solutions for your project. Traditionally, it would take a lot of time and energy to configure things like database, ORM, WebSockets, type safety, caching layers, etc. I use a lot of these products personally. The amount of time I have invested to make everything work is insane. But, I am never going to argue it is not worth configuring. Every tool I have used in development solved a potential problem. The key point is that I had an amazing developer experience. Convex replaces everything as a single solution with a clear and more simplified format. I fell for its simplicity. It actually drives me to focus on shipping new features.

If you don't know where to start with Convex, this blog would be a perfect kick start for you. I wanted to understand the data transaction in Convex. So, I have built a very simple todo app as it covers all the fundamental operations. Please use the following commands to install and setup Convex.

I am using Next.js for this demo. If you are using Next as your tech stack, you can follow the steps easily. Please refer to this link if your tech stack is not Next.js.

pnpm add convex

npx convex dev

When you execute npx convex dev, it will ask you to login. You can use your GitHub account to login. Once you finish your authentication part successfully, it will create a convex folder in your root of the project and it will put the following env variables in the .env.local file.

CONVEX_DEPLOYMENT = "dev:***";
NEXT_PUBLIC_CONVEX_URL = "https://***.convex.cloud";

Every time you make changes in this directory, it will ensure that your cloud is updated according to your local files. It is like every time you change a file in Next.js, the .next file is updated and eventually you can see the changes on the page. In addition to this, based on the data you pass through mutations, it generates the table schema. It can be used as a data type for your further data transfer.

//convex/schema.ts

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  todos: defineTable({
    text: v.string(),
    createdAt: v.optional(v.number()),
  }),
});
// page.tsx

"use client";
import { ChangeEvent, FC, FormEvent, useState } from "react";
import { api } from "../../../../convex/_generated/api";
import { useMutation, useQuery } from "convex/react";

interface pageProps {}

const page: FC<pageProps> = ({}) => {
  const [inputValue, setInputValue] = useState<string>("");
  const createTodo = useMutation(api.todos.createTodo);
  const getTodos = useQuery(api.todos.getTodos);

  const submitHandler = async function (e: FormEvent) {
    e.preventDefault();
    await createTodo({ text: inputValue });
    setInputValue("");
  };

  const changeHandler = function (e: ChangeEvent<HTMLInputElement>) {
    setInputValue(e.target.value);
  };

  return (
    <section className="w-full flex flex-col items-center justify-center min-h-screen">
      <form
        onSubmit={submitHandler}
        className="min-w-xl p-4 flex items-center justify-center flex-col gap-3"
      >
        <input
          value={inputValue}
          onChange={changeHandler}
          type="text"
          className="w-full border-white border"
        />
        <button
          className="bg-green-500 cursor-pointer px-4 py-2 rounded-md"
          type="submit"
        >
          submit
        </button>
      </form>
      <ul className="flex flex-col items-start justify-center gap-2">
        {getTodos?.map((item, i) => (
          <li className="text-white" key={i}>
            {item.text}
          </li>
        ))}
      </ul>
    </section>
  );
};

export default page;
//convex/todos.ts
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";

export const createTodo = mutation({
  args: {
    text: v.string(),
  },
  handler: async function (ctx, args) {
    try {
      console.log(args);
      const value = await ctx.db.insert("todos", {
        text: args.text,
      });
      console.log(value);
    } catch (err) {
      console.log(err);
    }
  },
});

export const getTodos = query({
  handler: async (ctx) => {
    return await ctx.db.query("todos").collect();
  },
});

I have created a simple form in the frontend. When you submit the form, the createTodo method will be called from the convex directory. Before you do any logic with your data inside the method, similar to how you use validation libraries to check whether the request data is correctly typed, here you use the args validation with Convex's built-in validation system (v.string(), v.number(), etc.) to ensure data integrity. Then you use the validated data to store it in the database.

The functions are executed in the serverless environment, so they cannot persist the connection with the frontend for a very long time. Once the function serves its purpose, it goes into hibernation mode. It won't be executed until you specifically call the function. However, whenever the database values change, Convex's real-time system detects these changes and pushes updates through persistent WebSocket connections to notify all connected clients to refresh their data. Let's say you are adding a todo to the todos table - it will automatically update the data in all components that use todos data across all connected browsers.

When you set up Convex, it establishes two types of connections:

  • HTTP connections for mutations (like createTodo) - these are request/response and close after completion
  • Persistent WebSocket connection for real-time updates - this stays open to push data changes

The WebSocket connection you see in your developer tools looks like this:

wss://your-deployment.convex.cloud/api/1.26.1/sync

This persistent connection is what enables the real-time magic. When someone adds a todo, the database change triggers Convex's change detection system, which then pushes updates through this WebSocket to all connected clients. This is how your useQuery hooks automatically receive fresh data without manual refetching.

The serverless functions (mutations and queries) execute on-demand and hibernate, but the WebSocket infrastructure runs continuously on Convex's managed servers, bridging the gap between serverless execution and real-time communication.