Bright ideas and techniques for building with Convex.
Profile image
Ian Macartney
2 months ago

Sessions: Wrappers as "Middleware"

Store per-session data in Convex

Session tracking is a common practice for application servers. While most of your data is associated with a user or another document, sometimes you have data specific to a user’s tab, or associated with a user who isn’t logged in. Some of this data is stored on the client, such as in the browser’s sessionStorage or localStorage, while other data is stored on the server. This post will dive into how to implement session storage with Convex using some helper functions we wrote. The code is in the convex-helpers GitHub repo.

User data:

Typically user data is stored on the server to avoid accidental leakage of personal data on public computers. Because this data can exist without a logged-in user, it can enable representing and capturing data about anonymous users. This is great news for building multiplayer experiences where you don’t want to require logging in. In legacy systems, this might also be where you cached the signed-in user. With Convex, auth is managed automatically, your serverless functions execute close to the database, and queries are cached, so you don’t have to worry about this optimization.

Ephemeral state:

Storing session data also provides a more continuous experience for a logged-in user because you can have per-tab information on where they are in the application. Suppose there is a complex multi-step flow, like booking an appointment. In that case, they can book two different appointments simultaneously without losing their progress if they refresh the page and without storing that sensitive data in the browser’s storage.

Typically this data is stored in a separate database because typically databases are slow, far away from the application server, and because this data is frequently queried. Traditional databases use pessimistic locking, which creates a bottleneck when many transactions reference the same rows. With Convex, we can store it in a first-class table, giving us a transactionally consistent view of all our data. Convex also uses optimistic concurrency control, which alleviates the bottleneck concern. Read more on that here.

How to implement sessions with Convex

Continuing the series of Wrappers as “Middleware,” I built some functions to wrap your serverless functions to provide session data. It stores your session data in a “sessions” table in your Convex backend. Because this also requires keeping track of the session ID in the client, I’ve also written some wrappers for useQuery and useMutation to make it easy.

Using sessions:

  1. In addition to a ConvexProvider, wrap your app with a SessionProvider: (defined here):

    <ConvexProvider client={convex}>
      <SessionProvider storageLocation={"sessionStorage"}>
        <App />
  2. Use queryWithSession or mutationWithSession as your function (defined here):

    export default mutationWithSession(async ({ db, session }, body) => {
      const message = { body, sessionId: session._id };
      await db.insert("messages", message);
  3. Use useSessionQuery or useSessionQuery in your React client (defined here):

    const sendMessage = useSessionQuery('sendMessage');
  4. [Optional] Write any data that you want to be available in subsequent session requests to the sessions table :

    db.patch(session._id, { anonymousUserId });
  5. [Optional] Update the sessions:create function (defined here) to initialize the session data.

How it works

Under the hood, what it is doing is quite simple.

  1. It creates a new session in the SessionProvider context. This creates a new document in a “sessions” table and returns its ID to the frontend, which stores the session ID in sessionStorage or, optionally, localStorage. Notes on that are below.
  2. It passes that session ID as the first parameter in each query or mutation where you use useSessionQuery or useSessionMutation. It passes null if the session hasn’t been created yet.
  3. The serverless functions using withSession define an extra first parameter of a session ID and use it to look up the session, passing it in the function’s first argument (that usually has the db and auth objects), along with the rest of the parameters. It passes null if the session ID wasn’t provided.

sessionStorage vs. localStorage

If you want the session to be shared between all tabs in a browser. I like the behavior of sessionStorage for general use:

  • When you refresh a page, the data persists. The data is tied to a specific tab.
  • If you open a new tab, you start fresh.
  • If you use the “Reopen Closed Tab” feature on Chrome, the data is still there.

Thoughts on localStorage:

  • When multiple browser tabs are reading and writing the same data, there are a lot of opportunities for bugs and confusion.
  • Some data may make sense to be stored at the browser level, such as answering questions like “have I seen this browser before.”
  • Keep public computers in mind.

Initialization behavior

Before the session ID is created, your serverless functions may receive a null session. You can decide how to gracefully handle this state. In mutations I generally assert the session’s existence, and in queries I fall back gracefully. Thankfully this only happens for brand new sessions, since the ID is stored client-side and read on initialization.


In this post we looked at implementing session storage in Convex, using a custom table and some convenience wrappers which make it easy to use session-specific data in your server-side code. We look forward to seeing what you build with it.

Check out the code here and submit a pull request if you think of any improvements. 🙏

Start Building with Convex

Build real-time reactive apps, deploy backends in an instant, scale your data with automatic caching, and much more.

Try Convex free