
Sessions: Wrappers as "Middleware"

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:
-
In addition to a
ConvexProvider
, wrap your app with aSessionProvider
: (defined here):<ConvexProvider client={convex}> <SessionProvider storageLocation={"sessionStorage"}> <App /> </SessionProvider> </ConvexProvider>
-
Use
queryWithSession
ormutationWithSession
as your function (defined here):export default mutationWithSession(async ({ db, session }, { body }) => { const message = { body, sessionId: session._id }; await db.insert("messages", message); });
-
Use
useSessionQuery
oruseSessionQuery
in your React client (defined here):const sendMessage = useSessionQuery('sendMessage', {}); ... sendMessage(body);
-
[Optional] Write any data that you want to be available in subsequent session requests to the
sessions
table :db.patch(session._id, { anonymousUserId });
-
[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.
- 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 insessionStorage
or, optionally,localStorage
. Notes on that are below. - It passes that session ID as the first parameter in each
query
ormutation
where you useuseSessionQuery
oruseSessionMutation
. It passesnull
if the session hasn’t been created yet. - 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 thedb
andauth
objects), along with the rest of the parameters. It passesnull
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.
Summary
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. 🙏
Convex is the backend application platform with everything you need to build your project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.