Sessions: Wrappers as "Middleware"
Loading...
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 here.
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. This might also be where you store the signed-in user. With Convex, auth is built in, your serverless functions execute close to the database, and queries are cached, so you don’t have to worry about user caching. You can just store a userId
and look up the latest data each time.
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.
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.
Note: Since the wrappers series, my recommended workflow for wrapping server-side functions has changed to use "custom functions" which you can read more about here. The syntax for usage is generally the same, but is easier to work with.
Using sessions:
-
In addition to a
ConvexProvider
, wrap your app with aSessionProvider
:<ConvexProvider client={convex}> <SessionProvider> <App /> </SessionProvider> </ConvexProvider>
-
Use
queryWithSession
ormutationWithSession
as your function:export const send = mutationWithSession({ args: { body: v.string() }, handler: async (ctx, { body }) => { const userId = await getOrCreateUserId(ctx.db, ctx.auth, ctx.sessionId); await ctx.db.insert("messages", { body, userId }); }, });
Use useSessionQuery
or useSessionMutation
in your React client:
const sendMessage = useSessionMutation(api.messages.send);
...
sendMessage({body});
-
Write any data that you want to be available in subsequent session requests to the
sessions
table. E.g. in ourgetOrCreateUserId
function we could do this:const anonymousUserId = await db.insert('users', { anonymous: true }); db.patch(sessionId, { userId: anonymousUserId });
Note on session table vs. ID: In this post I outline a strategy for having the
sessionId
be client-created, and instead of having a "sessions" table, using the session ID as a foreign key reference into the tables where you store data. The advantage of that, other than avoiding the server roundtrip to make a session document to get the ID, is that your session queries won't al load (and therefore depend) on the same document. If every query loads the session document, then on every update to that document your queries will all be invalidated, even if they didn't need that field of the session document. By storing the session-related data in more targeted tables, you can only be loading the data you need. Read more about query optimizations here.
How it works
Under the hood, what it is doing is quite simple.
- It creates a new session in the
SessionProvider
context. Whether it creates a server-side document and uses its ID, such as this older implementation, or creating a session ID client-side like this post, it then stores the session ID insessionStorage
or, optionally,localStorage
.1 Notes on one versus the other are below. - It passes that
sessionId
as a parameter in eachquery
ormutation
where you useuseSessionQuery
oruseSessionMutation
. - The serverless functions define a
sessionId
parameter manually or automatically with a custom wrapper, and pass it along as a field ofctx
so the other function arguments aren't cluttered with it.
sessionStorage
vs. localStorage
If you want the session to be shared between all tabs in a browser, use localStorage
. 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.
For localStorage
:
localStorage
is a great place for custom authentication information, since a user generally doesn't want to re-authenticate on every tab they open. However, you should be able to invalidate the sessions server-side, and clear or replace thesessionId
on the client when they log out. See this post for more info.- Remember to account for multiple tabs interacting with the same data. For instance, if you're keeping track of a user's shopping cart, one tab can go through checkout, while the other tab is in a payment selection modal. Thanks to Convex, the data will automatically update on each tab, but it's up to you to design a UI that responds to those updates in a user-friendly way.
- 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 - the same
localStorage
doesn't always map to the same human.
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 old code here or the newer approach here.
Footnotes
-
In the newer
convex-helpers
package you can specify a customuseStorage
hook that isn't limited tolocalStorage
orsessionStorage
. ↩
Convex is the sync platform with everything you need to build your full-stack project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.