How to track multi-item user onboarding state with ExpressJS

Learn how to track user onboarding state without using req.session

Bahadir Balban

Started Tech Buzz


Multi-item Onboarding State Tracking Problem

When developing the Tech Buzz backend, there have been numerous onboarding cases I needed to track when onboarding a new user.

Onboarding checklist, often needs ordering with priorities

Is this user joining a site as part of signing up?

Should I show the onboarding screens first, or process payments first?

If user is starting a new site, do we show site onboarding, or user onboarding as an individual?

How do you track completion of the above, and prioritize them?

What kind of state maintenance should I use?

State Maintenance Options

The state maintenance options at hand are as below:

  • Database

  • Cookies

  • Query strings

  • Session state kept under req.session

The problem with the database is the first intent of the user (e.g. paying for something) determines the onboarding precedence, and we cannot maintain state in the database as the user has not signed in yet.

Of all of the above, I initially used query strings and req.session as a way to maintain onboard state information about an onboarding user. I knew that req.session is a straightforward way to keep data about a user, and query strings will work to pass data without a user being logged in yet. But I faced problems with both, which is why onboarding has been somewhat tricky with corner-case bugs.

Problem #1: query strings are tedious for tracking state

Query strings are strings you pass in a url such as the following:

where ? starts the query string section and each variable is separated by an ampersand ‘&’.

The problem with this method for onboard state tracking is, there are many routes this information needs to be kept across, and each route has to have custom logic to track the query string state. This means n number of routes each handle special cases and trying to track how much of onboarding is done and not done by looking at the query string state.

Problem #2: The state tracking should be done in an ordered queue

The nature of onboarding state revealed itself as a use case for a queue that is ordered. As an example:

  • if a user hasn’t been added to a site/group yet, we need to show that screen, but with the lowest priority.

  • If a user has started a payment intent in addition to sign up, the payment redirect should have the highest priority after creating an account.

Otherwise there is a queue of onboarding items to do:

  1. Payment for site/group creation

  2. Onboarding for site/group

  3. Onboarding for individual user.

Other onboarding items have been: Process invite from user, process invite from site/group owner.

So an ordered queue of todo items need to be tracked in a state mechanism. Not having such a data structure caused me to check onboard state at many routes, and use many unneeded decision making logic to arrive at where to redirect. Here is an example of such unnecessary if/then logic:

var successRedirect;
if (req.session.defer_user_onboard === true) {
	// Return immediately if user onboarding is deferred.
	successRedirect = req.session.returnTo
} else {
	// Go to user onboarding routes first.
	successRedirect = "/o/" + u.username + "/pick-topics";
	// If this sign up was caused by a redirect query string, set it up as
	// final destination.
	if (req.query.redirect !== undefined) {
		req.session.returnTo = req.query.redirect;
		delete req.query.redirect;

Problem #3: req.session is not reliable during onboarding a new user

On top of the complication of query string tracking, req.session has not been a reliable way to track onboarding state for a user. If you set a variable in req.session before the user account has been created, if the user signs up via OAuth method such as Google Auth, the req.session state will be cleared. This means that pre-onboarding intent information is lost after the account is created. It is a showstopper for tracking state. While there is some online claims that this should normally work, it wasn’t the case for us.

How req.session works

In case you might be wondering, req.session works as follows: We at Tech Buzz use a session state maintenance via a Redis server. Redis is an in-memory key value store. In ExpressJS when you use redis sessions, after your login, the relevant plugins in ExpressJS set a cookie called connect.sid that uniquely identifies a user. This user id is then used as a key in the Redis server to set values.

So, the session mechanism uses a special cookie called connect.sid to identify a user and maintain rest of the information in Redis. When you set req.session.var = 1, Redis sets the variable var for the user identified by connect.sid

I thought since, this session mechanism uses cookies, why not use req.session directly, instead of trying to set cookies? Also since cookies are used in this method, and this method is not reliable, I was out of solutions. But not quite so. After checking more about setting other cookies explicitly, I found the solution.

Solution: Cookies to the rescue!

The solution I came up with involves setting a separate cookie for onboarding state. I know this is a simple one that took me time to arrive at, but with so many options and relying on req.session for any user state data made it tough to come to this resolution.

I have tested on both Google Chrome and Safari that cookies do not have any of the drawbacks listed above. You can set a cookie and access it at any route for additional information on this user.

The cookies you set prior to account creation are still available after the account is created, even after Oauth login.

I am sharing the following simple code that I used to test that the cookies are set and cleared reliably in ExpressJS, and can confirm this mechanism works both prior and after the account creation:

Creating the routes:

router.get('/cookie-set', base.cookie_set); 
router.get('/cookie-show', base.cookie_show); 
router.get('/cookie-clear', base.cookie_clear);

The controllers:

exports.cookie_set = function(req, res, next) {
	res.cookie('name', 'express').send('cookie set');
exports.cookie_show = function(req, res, next) {
	res.render('cookie', { cookie: });
exports.cookie_clear = function(req, res, next) {
	res.send('cookie cleared');

With the above code you will find out that is set to a value and is maintained after user account creation, and cleared when you go to the cookie-clear route.

The next minor step left is to create a prioritized queue-like JSON data structure to keep onboard information, and clear it once all are complete.


  1. Use cookies as above for maintaining onboarding state in an ExpressJS application.

  2. Use a priority queue to identify next onboard route, to avoid adding lots of custom logic to find out where to redirect.

  3. Use req.session for any other session related information for a logged in user.

You Might Also Like...

Join The Discussion