<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Efe Karasakal</title>
    <description>The latest articles on DEV Community by Efe Karasakal (@efekrskl).</description>
    <link>https://dev.to/efekrskl</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3071028%2F0c3dfc14-19d6-4d49-9ee6-41ee9edcb3bd.jpg</url>
      <title>DEV Community: Efe Karasakal</title>
      <link>https://dev.to/efekrskl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/efekrskl"/>
    <language>en</language>
    <item>
      <title>Building a Multiplayer Game with Convex Over a Weekend</title>
      <dc:creator>Efe Karasakal</dc:creator>
      <pubDate>Tue, 29 Jul 2025 18:07:11 +0000</pubDate>
      <link>https://dev.to/efekrskl/building-a-multiplayer-game-with-convex-over-a-weekend-1o59</link>
      <guid>https://dev.to/efekrskl/building-a-multiplayer-game-with-convex-over-a-weekend-1o59</guid>
      <description>&lt;p&gt;&lt;em&gt;This article originally appeared on &lt;a href="http://efe.dev/blog/convex-multiplayer-game" rel="noopener noreferrer"&gt;my website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I first heard about &lt;a href="https://www.convex.dev/" rel="noopener noreferrer"&gt;Convex&lt;/a&gt; through &lt;a href="https://t3.chat/" rel="noopener noreferrer"&gt;t3.chat&lt;/a&gt; months ago, and ever since then, it's been buried deep in my "someday" learning list. When I finally had a weekend to nerd out, I decided to give it a go.&lt;/p&gt;

&lt;p&gt;I already had an app idea in mind that seemed like a perfect fit for Convex: a real-time, multiplayer browser clicker game. A small homage to &lt;a href="https://www.erepublik.com/en" rel="noopener noreferrer"&gt;eRepublik&lt;/a&gt;, which I loved playing as a kid.&lt;/p&gt;

&lt;p&gt;In this article, I will share my experience building a multiplayer game with Convex over a weekend, walking you through the steps I took, the challenges I faced, and the solutions I found. During which we will cover the basics of Convex.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the Project?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feddc0g11vi76cmww2a6p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feddc0g11vi76cmww2a6p.png" alt="GeoWar is a multiplayer game based on world conquest" width="800" height="390"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Ah yes, the Swedish Empire where the sun never sets&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This was the end result: &lt;a href="https://www.geowar.io/" rel="noopener noreferrer"&gt;GeoWar.io&lt;/a&gt;, a game based on world conquest. The rules are simple, you automatically get assigned to your country of origin. You either defend or attack a territory by clicking on it. Each territory has a point of its own and if you reduce a territory's points to zero, you conquer it. Countries that are currently invaded are marked with stripes.&lt;/p&gt;

&lt;p&gt;The entire backend is built with Convex. The frontend is built with &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt; and the map is made with &lt;a href="https://www.amcharts.com/" rel="noopener noreferrer"&gt;amcharts&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  What even is Convex?
&lt;/h3&gt;

&lt;p&gt;Convex gives you a backend entirely with TypeScript, including database, backend functions, authentication and real-time syncing. No servers, no REST APIs, no manual WebSockets. End-to-end type safety makes for a great developer experience. It also has an extensive set of libraries that help you to build your app faster.&lt;/p&gt;

&lt;p&gt;Building with Convex instead of a traditional backend allowed me to focus on the game logic instead of struggling with boilerplate code.&lt;/p&gt;
&lt;h3&gt;
  
  
  Our first schema
&lt;/h3&gt;

&lt;p&gt;Convex stores JSON-like documents and can be used with or without a schema. Here's an example schema from my database, I simplified it a bit to focus on the core concepts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// convex/schema.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;defineSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defineTable&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;convex/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;convex/values&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineSchema&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;defineTable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// Name of the country&lt;/span&gt;
        &lt;span class="na"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// If the point reaches 0, the country gets conquered&lt;/span&gt;
        &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;union&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// ISO 3166-1 alpha-2 country code&lt;/span&gt;
            &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// ... other countries&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;by_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="c1"&gt;// An index by country code&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other schemas&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The validator builder &lt;code&gt;v&lt;/code&gt; is used to define the type of documents in each table. It supports various simple types, as well as more complex structures like string literals, records or optional fields.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxaa5d3lzr9onzu9o6xa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkxaa5d3lzr9onzu9o6xa.png" alt="Viewing tables in Convex Dashboard" width="800" height="395"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;What the schema above looks like in Convex Dashboard&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We interact with our database through &lt;strong&gt;mutations&lt;/strong&gt; and &lt;strong&gt;queries&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mutations&lt;/strong&gt; are used to make changes to data in the database. They can also handle authentication checks or other business logic, and may return a response to the client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queries&lt;/strong&gt; are the core of your backend API. They retrieve data from the database, optionally perform authentication or business logic, and return the result to the client.&lt;/p&gt;
&lt;h3&gt;
  
  
  Mutating data
&lt;/h3&gt;

&lt;p&gt;Let's start with a mutation. Again, this is a simplified version of a real code snippet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// convex/user.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./_generated/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;convex/values&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;targetCountryCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;union&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="c1"&gt;// ... other countries&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Get the corresponding country&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetCountry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;countries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;by_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetCountryCode&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Rest of the business logic removed for simplicity&lt;/span&gt;

        &lt;span class="c1"&gt;// Update the country's points&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetCountry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targetCountry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;points&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down what's happening here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We created a new public &lt;strong&gt;mutation&lt;/strong&gt;, which gets exposed automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;args&lt;/strong&gt; takes care of validation and types for us, using v similarly to how we defined our schemas.&lt;/li&gt;
&lt;li&gt;We first queried our database with &lt;code&gt;ctx.db.query&lt;/code&gt; using the indexed field we defined before.&lt;/li&gt;
&lt;li&gt;Then we updated this record with &lt;code&gt;ctx.db.patch&lt;/code&gt;, reducing the points by one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is pretty standard and simple. However, in a typical backend setup, this simple looking code would have a hidden danger: &lt;strong&gt;race conditions&lt;/strong&gt;. Luckily, Convex solves this for us.&lt;/p&gt;

&lt;p&gt;Let's imagine two players clicking simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both read the country &lt;code&gt;DE&lt;/code&gt; at ten points&lt;/li&gt;
&lt;li&gt;Both calculate &lt;code&gt;points - 1 = 9&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Both update the record as &lt;code&gt;points = 9&lt;/code&gt;, meaning one update overwrites the other, losing data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a classic &lt;strong&gt;race condition&lt;/strong&gt;, more specifically a &lt;strong&gt;lost update problem&lt;/strong&gt;, where two users overwrite each&lt;br&gt;
other’s updates.&lt;/p&gt;

&lt;p&gt;...but how does Convex solve this issue for us?&lt;/p&gt;

&lt;p&gt;Convex runs every mutation as an isolated, serializable transaction. Convex mutations are also deterministic, so they can be retried. This allows you to write your code as if everything happens in order, even if it's not.&lt;/p&gt;

&lt;p&gt;No locks, no manually written transactions, all of it just works magically.&lt;/p&gt;

&lt;p&gt;If you want to learn more about how Convex works under the hood, I recommend reading their &lt;a href="https://docs.convex.dev/understanding/" rel="noopener noreferrer"&gt;Understand Convex&lt;/a&gt; section.&lt;/p&gt;
&lt;h3&gt;
  
  
  Reading data
&lt;/h3&gt;

&lt;p&gt;That's enough boring talk. Let's take a look at a query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// convex/user.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./_generated/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getAllCountries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;countries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;countries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just like our mutation, this creates a public &lt;code&gt;query&lt;/code&gt;. Queries are how we read data using Convex. Through WebSockets, they subscribe to any changes that happen in our&lt;br&gt;
database.&lt;/p&gt;

&lt;p&gt;Let's start using our Convex functions in our frontend starting with our mutation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;convex/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../convex/_generated/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleClick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Out of the box, Convex supports many UI libraries/frameworks, this allows you to focus on building your business logic. Here we see the &lt;code&gt;useMutation&lt;/code&gt; hook of &lt;code&gt;convex/react&lt;/code&gt;. As an argument of this hook, we pass our autogenerated api object.&lt;/p&gt;

&lt;p&gt;Notice how &lt;code&gt;api.user.handleClick&lt;/code&gt; maps to our function &lt;code&gt;handleClick&lt;/code&gt;, inside the &lt;code&gt;convex/user&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk7rabvqmawz8w61fvekn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk7rabvqmawz8w61fvekn.png" alt="Everything is already typed!" width="800" height="120"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Everything is already typed!&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Trigger the mutation&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleClick&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;targetCountryCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since our arguments are already typed, everything is type safe in development time, and we have our validation in place for any runtime surprises. This makes for a great DX.&lt;/p&gt;

&lt;p&gt;This should give us the basics of clicking on a territory and reducing its points. However, we would have a slight delay in user interactions because as of writing this Convex only has servers in the US. We are building a game, we should improve that.&lt;/p&gt;

&lt;p&gt;Luckily, we can manipulate Convex's local state to implement &lt;strong&gt;optimistic updates&lt;/strong&gt;, a technique where the UI pretends the update succeeded before the server confirms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handleClick&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;withOptimisticUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localQueryStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Get the local state&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localCountries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localQueryStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getAllCountries&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Rest of the business logic removed for simplicity&lt;/span&gt;

        &lt;span class="c1"&gt;// Update the local state&lt;/span&gt;
        &lt;span class="nx"&gt;localQueryStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getAllCountries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nx"&gt;updatedCountries&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let’s use our query to get live data. We could use the query counterpart of &lt;code&gt;useMutation&lt;/code&gt;, aka &lt;code&gt;useQuery&lt;/code&gt;, but since we are using Next.js, we could benefit from some Next-specific hooks. This way we can use the full power of server-side rendering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Preload the query in a server component...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;preloadedCountries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;preloadQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getAllCountries&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ... and use it in a client component&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;countries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePreloadedQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preloadedCountries&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. We don't have states like &lt;code&gt;loading&lt;/code&gt;, or an additional function like &lt;code&gt;refetch&lt;/code&gt; to query the backend again.&lt;/p&gt;

&lt;p&gt;Convex queries are &lt;strong&gt;reactive&lt;/strong&gt; by default. Any change made to the database is reflected in our frontend.&lt;/p&gt;

&lt;p&gt;We have everything in place ready for the world domination. However, right now the users can spam clicks (or even worse they can bombard the WebSocket connection, like my friends!)&lt;/p&gt;

&lt;p&gt;We could implement a simple client side solution, but that wouldn't solve the core issue. Convex endpoints are &lt;strong&gt;public&lt;/strong&gt; unless we define them explicitly as internal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Convex components to rescue!
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.convex.dev/components" rel="noopener noreferrer"&gt;Convex components&lt;/a&gt; are modular building blocks that allow us to add new features to our Convex backends, and they happen to have a &lt;a href="https://www.convex.dev/components/rate-limiter" rel="noopener noreferrer"&gt;Rate Limiter&lt;/a&gt; component. This makes it a &lt;em&gt;breeze&lt;/em&gt; to add a rate limiter to our app.&lt;/p&gt;

&lt;p&gt;We start by creating our Rate Limiter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;RateLimiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SECOND&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@convex-dev/rate-limiter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rateLimiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RateLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rateLimiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;click&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token bucket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SECOND&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check if the rate limit is exceeded or not in our mutation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;targetCountryCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;union&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;literal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="c1"&gt;// ... other countries&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetCountry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;countries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;by_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetCountryCode&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;some-user-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rateLimiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Rate limit exceeded by &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Rest of the business logic removed for simplicity&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetCountry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targetCountry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;points&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a lot more Convex Components like crons, migrations, or presence data.&lt;/p&gt;

&lt;p&gt;With that we've pretty much covered all the basics of Convex; while there's still a lot to learn about Convex this is enough for us to build a game.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's not all perfect
&lt;/h3&gt;

&lt;p&gt;Before wrapping up, I want to mention some "downsides" of using Convex. After all, there's no silver bullet in software engineering.&lt;/p&gt;

&lt;h4&gt;
  
  
  Some features are lacking
&lt;/h4&gt;

&lt;p&gt;There were some cases that felt like Convex was missing some basics. Like how you can't select only a certain fields from a table (similar to SQL's &lt;code&gt;SELECT x&lt;/code&gt;), which quickly increases the database bandwidth usage. (In fact, I switched to self-hosted for this reason) To be fair, our main query is poorly optimized, so the blame is partially on me. Their solution for this problem is to&lt;br&gt;
split your tables.&lt;/p&gt;

&lt;p&gt;Another example would be how you can't get the remote IP address in a Convex mutation/query. I had to hack my way around safely getting the IP address, which was a bit annoying.&lt;/p&gt;

&lt;p&gt;It's worth noting "batteries included" solutions always lack &lt;strong&gt;something&lt;/strong&gt; that we want. This is also, hilariously, mentioned in &lt;a href="https://www.convex.sucks/" rel="noopener noreferrer"&gt;convex.sucks&lt;/a&gt;. (no really, click on that link)&lt;/p&gt;

&lt;h4&gt;
  
  
  Convex might require a different mental model
&lt;/h4&gt;

&lt;p&gt;This is not necessarily a downside, but Convex isn't a traditional backend solution. Especially since Convex mutations and queries must be &lt;strong&gt;deterministic&lt;/strong&gt;, so there are some limitations to what you can do. Considering how Convex works, this limitation makes sense, but it might require a different mental model at times.&lt;/p&gt;

&lt;h3&gt;
  
  
  That's it
&lt;/h3&gt;

&lt;p&gt;I think Convex is an awesome tool. Is it perfect? No, it's not. But amongst all the similar tools, I think it is the most convenient one. The developers behind it are brilliant people, and they are active on all platforms.&lt;/p&gt;

&lt;p&gt;So definitely give it a go and make sure to play some &lt;a href="https://www.geowar.io/" rel="noopener noreferrer"&gt;GeoWar&lt;/a&gt; to increase my cloud bills.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>gamedev</category>
      <category>nextjs</category>
      <category>convex</category>
    </item>
    <item>
      <title>How I Finally Made Work Journaling a Habit</title>
      <dc:creator>Efe Karasakal</dc:creator>
      <pubDate>Wed, 30 Apr 2025 15:30:33 +0000</pubDate>
      <link>https://dev.to/efekrskl/how-i-finally-made-work-journaling-a-habit-1m70</link>
      <guid>https://dev.to/efekrskl/how-i-finally-made-work-journaling-a-habit-1m70</guid>
      <description>&lt;p&gt;&lt;em&gt;This article originally appeared on &lt;a href="https://www.efe.dev/blog/work-journal-habit" rel="noopener noreferrer"&gt;my website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As a kid, I loved the idea of keeping a journal. I'd buy a fresh notebook every year, write for two days, and forget about it completely.&lt;/p&gt;

&lt;p&gt;Fast-forward to adulthood: I'm a software engineer, and that habit hadn't improved. The first time I heard about work journals and "brag documents," I was intrigued. I gave it a shot, then dropped it. Again.&lt;/p&gt;

&lt;p&gt;But this time, I was determined to make it stick. It took a few rounds of trial and error, but eventually, I found a system that worked. Here's what actually helped me turn work journaling from a good intention into a real habit.&lt;/p&gt;

&lt;h3&gt;
  
  
  I stopped trying to be perfect
&lt;/h3&gt;

&lt;p&gt;I skipped a day of journaling? Whatever. I would just go back and journal what I could remember. Once I gave myself permission to be inconsistent, I ironically became more consistent.&lt;/p&gt;

&lt;p&gt;That mindset shift - from perfect to "good enough" - made the habit sustainable.&lt;/p&gt;

&lt;h3&gt;
  
  
  I used tools I actually enjoy
&lt;/h3&gt;

&lt;p&gt;There's no "best" tool for journaling - there's just the one you'll actually use. For me, that was Notion. It felt natural because I already used it for docs and personal notes. For others, it might be a physical notebook, a Markdown file, or even a &lt;code&gt;notes.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If opening the tool feels annoying, you won't stick with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  I reduced the friction
&lt;/h3&gt;

&lt;p&gt;If you've read &lt;em&gt;Atomic Habits&lt;/em&gt; by James Clear, you've probably seen this idea: make the habit so easy it's hard to avoid.&lt;/p&gt;

&lt;p&gt;That's what I did. &lt;a href="https://github.com/efekrskl/gj" rel="noopener noreferrer"&gt;I built a tiny CLI app in Rust called &lt;code&gt;gj&lt;/code&gt;&lt;/a&gt; that lets me log entries straight from the terminal to Notion. The terminal is the one tool I use constantly - adding journaling into that flow meant zero friction.&lt;/p&gt;

&lt;p&gt;Merged a big PR? I log it in two seconds, right where I am. No context switch, no excuses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ygghjk93qb7d1qb3vkd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ygghjk93qb7d1qb3vkd.png" alt="What logging with  raw `gj` endraw  looks like — zero fluff, sent straight to Notion." width="800" height="253"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;What logging with &lt;code&gt;gj&lt;/code&gt; looks like — zero fluff, sent straight to Notion.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I'm not saying you should use my tool — just find what makes it effortless for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  I started seeing the impact — and that kept me going
&lt;/h3&gt;

&lt;p&gt;Once I had some consistency, what really kept the habit alive wasn't the tool or routine — it was the effect it had on me. Because journaling became easy, I started logging more often: small wins, tricky bugs, even small things like helping a teammate. Most of it wouldn't show up in Jira — but it mattered.&lt;/p&gt;

&lt;p&gt;Seeing that progress, in my own words, helped me feel more confident. It didn't erase impostor syndrome, but it gave me something real to look back on.&lt;/p&gt;




&lt;p&gt;Now, it's not just a habit. It's a quiet tool that helps me stay focused, reflect on what I've done, and feel a little more confident on the hard days.&lt;/p&gt;

&lt;p&gt;If you've tried and failed before, that's fine. What worked for me might not work for you — the key is finding something you'll actually stick with.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>habits</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
