Keeping track of account subscriptions with Redis and Node.js

This is part 4 of my Node/Redis series. You can read Part 1, Dancing around strings in Node.js and Redis, Part 2, Store Javascript objects in Redis with Node.js the right way, Part 3 Using the Redis multi object in Node.js for fun and profit, Part 5 Managing modularity and Redis connections in Node.js, Part 6 Redis, Express and Streaming with Node.js and Classic Literature(?) Part 7 Untangling Redis sort results with Node.js and lodash and Part 8 Redis, set, node.

I’m writing a subscription-based web app backed by Redis and written in Node.js using the Express framework. In my app, users can purchase a year long subscriptions and I needed a way to keep track if a user has a valid, paid account.

My first thought was to just store a date timestamp in a user account hash, then compare that timestamp to today’s date. This solution seemed a bit inelegant and I’ve run into issues where timestamps varying between servers, believe it or not. How could I keep the logic mostly in Redis?

Let’s say that each user has an user ID and it is stored something like this (1 being the user ID for user “John Doe”):

HSET my-app:accounts:1 name "John Doe"

We’ll also assume that whatever authorization middleware will keep track of your user IDs as it isn’t really important to the technique I’m outlining here.

Now, let’s setup a way to keep track of a valid, time-base subscription. We’ll set a string with the key to the account hash.

SET my-app:valid-account:1 my-app:accounts:1

Let’s set a TTL for that account, 31536000000(milliseconds in a year):

PEXPIRE my-app:valid-account:1 31536000000

You wouldn’t be doing this all in redis-cli — this is what it looks like using node-redis:

var
redis = require(‘redis’),
client = redis.createClient(),
keyRoot = ‘my-app’,
rKeys = {};

//see “Dancing around strings in Node.js and Redis” https://medium.com/@stockholmux/dancing-around-strings-in-node-js-and-redis-2a8f91ebe0bf
function rk() {
return Array.prototype.slice.call(arguments).join(‘:’)
}
rKeys.validAccount = rk(keyRoot, ‘valid-account’); //:userId, string
rKeys.accountInfo = rk(keyRoot,’accounts’); //:userId, hash

function createUser(userId, name, validForMs, cb) {
var
accountKey = rk(rKeys.accountInfo,userId),
validityKey = rk(rKeys.validAccount,userId);
  client.multi()
.hset(accountKey,’name’, name)
.set(validityKey, accountKey)
.pexpire(validityKey, validForMs )
.exec(cb);
}
createUser(1,’John Doe’, 31536000000, function(err) {
if (err) { throw err; } else {
console.log(‘created user’);
}
client.quit()
});

After reading this, you may as “Why didn’t you just put a TTL on the hash itself?” There is no technical reason that you couldn’t, but it would be unwise from a user management standpoint — keeping the account (e.g. the hash) around in case the user wants to resubscribe is a good business case for separating the expiry and the account.

Checking to see if a subscription is active

The flow in checking to see if a user is active is to get the relevant accountValid string, the to use the return of that string to get all the values (hgetall) out of the accountInfo hash. Since this is a two-step process we’ll use async’s seq function (have I mentioned that I love async?). It’s great for creating a sequential waterfall of asynchronous functions while taking care of any err callbacks pretty seamlessly.

var
...
async = require('async'),
getValidUser;
...
function isValidUser(userId,cb) {
client.get(rk(rKeys.validAccount,userId), function(err,userAccountKey) {
if (err) { cb(err); } else {
cb(err,userAccountKey);
}
});
}
function getUserProfileData(userAccountKey,cb) {
client.hgetall(userAccountKey,function(err,userValues) {
if (err) { cb(err); } else {
cb(err,userValues);
}
});
}
getValidUser = async.seq(isValidUser,getUserProfileData);
getValidUser(1,function(err,userData){
if (err) { throw err; } else {
if (userData) {
console.log(‘User is valid. Here is the profile data’, userData);
} else {
console.log(‘User is invalid’);
}
}
client.quit();
});

It’s a silly example at this point — but you can see what is going on — we’re just getting the value of validAccount in isValidUser, passing it along and then using that value as the key in the hgetall in getUserProfileData. In a case where the user is invalid or expired, isValidUser will pass along undefined, which will cause the hgetall to return undefined as well.

Imagine this as an Express middleware. Again, we’re going to assume that your authentication system is passing the userId somehow — we’ll just use req.userId in the example.

function validityMiddleware(req,res,next) {
getValidUser(req.userId,function(err,userData){
if (err) { next(err); } else {
if (userData) {
req.userData = userData;
next();
} else {
res.status(401).end(‘Unauthorized’);
}
}
});
}
app.get('/your/private/route',validityMiddleware, ... );

By taking advantage of the TTL properties in Redis, you have a simple system for tracking the expiration of accounts using some simple node glue and a couple of Redis requests.

Show your support

Clapping shows how much you appreciated Kyle’s story.