DEV Community

loading...
Cover image for You don't know Redis (Part 2)

You don't know Redis (Part 2)

SandorTuranszky
I dig into web tech, document my learning journey in plain English and blog about it!
・5 min read

In the first part of You don't know Redis, I built an app using Redis as a primary database. For most people, it might sound unusual simply because the key-value data structure seems suboptimal for handling complex data models.

In practice, the choice of a database often depends on the application’s data-access patterns as well as the current and possible future requirements.

Redis was a perfect database for a Q&A board. I described how I took advantage of sorted sets and hashes data types to build features efficiently with less code.

Now I need to extend the Q&A board with registration/login functionality.

I will use Redis again. There are two reasons for that.

Firstly, I want to avoid the extra complexity that comes with adding yet another database.

Secondly, based on the requirements that I have, Redis is suitable for the task.

Important to note, that user registration and login is not always about only email and password handling. Users may have a lot of relations with other data which can grow complex over time.

Despite Redis being suitable for my task, it may not be a good choice for other projects.

Always define what data structure you need now and may need in the future to pick the right database.

Implementation

I use serverless functions, the ioredis library and Upstash Serverless Redis.

I can’t help but talk about serverless all the time because it greatly simplifies development. I love when complexity is removed whenever possible and Upstash is doing just that for me.

I have zero work with setting up Redis. Moreover, I am using Upstash both in development and production.

Registration flow

During registration, we collect the user name, email and password. Before registering a user, we need to make sure that the email has not been registered already (is unique in the system).

Redis does not support constraints. However, we can keep track of all registered emails using a sorted set named emails.

On every new registration, we can use the ZSCORE command to check whether the provided email is already registered.

If the email is taken, we need to notify the user about it.

⚠️ Note, that this isn’t the best option because by telling that a given email is registered we provide a simple way for anyone to check whether someone is registered with a particular service, albeit it’s not a big security issue.

Before we can save a new user, we need to:

  • Generate a unique user ID.

We can use the INCR command to always get a unique value by incrementing a number stored at a key by one. If the key does not exist, Redis will set it to 0 before performing the operation. This means that the initial value will be 1.

const id = await redis.incr('user_ids') // -> 1
Enter fullscreen mode Exit fullscreen mode

Whenever you need to create a counter, INCR is a great choice. Or you can build a rate-limiter to protect your API from being overwhelmed by using INCR together with EXPIRE.

  • Hash the password with the bcrypt library.
const hash = await bcrypt.hash(password, 10)
Enter fullscreen mode Exit fullscreen mode

Now that we have the unique user ID (e.g. user ID is 7) and the hashed password, we can:
1. Store user details in a hash under the user:{ID} key.

redis.hmset('user:7', { 7, name, email, hash })
Enter fullscreen mode Exit fullscreen mode

Knowing the ID, we can easily get all user details using the HGETALL command:

redis.hgetall('user:7');
Enter fullscreen mode Exit fullscreen mode

2. Add the user’s email to the emails sorted set.

redis.zadd('emails', -Math.abs(7), email)
Enter fullscreen mode Exit fullscreen mode

This allows us to lookup emails to check if they are registered or get the user's ID by email which is exactly what we need for the login process.

redis.zscore('emails', email) will return the score which is the ID or nil if the email is not found.

Notice how we use this sorted set for two important features, namely ensuring unique emails and looking up users by email.

But we are taking it one step further and set scores (which represent user IDs) as negative numbers to mark emails as unverified: -Math.abs(7). Then, when the email is verified, we simply convert it to a positive number.

redis.zadd('emails', Math.abs(7), email)
Enter fullscreen mode Exit fullscreen mode

If a specified email is already a member of the emails sorted set, Redis will update the score only.

During the login process, we can always check for negative numbers and request users to verify their email instead of logging them in.

Retrieving all unverified emails is a trivial operation done with the ZRANGEBYSCORE command.

redis.zrangebyscore('emails', '-inf', -1, 'WITHSCORES');
Enter fullscreen mode Exit fullscreen mode

Registration function source code

Login flow

Before logging in the user, we check if the provided email exists in our database. As mentioned before, the score is the user ID.

const userId = await redis.zscore('emails', email);
Enter fullscreen mode Exit fullscreen mode

If so, we first check if the email is verified by making sure the ID is a positive number. If not, we ask users to verify their email.

If the email is verified, we get the password hash that we stored for the user:

const hash = await redis.hget('user:7', 'hash');
Enter fullscreen mode Exit fullscreen mode

and check whether the password is correct:

const match = await bcrypt.compare(password, hash);
Enter fullscreen mode Exit fullscreen mode

If the password is correct, we generate a token and return it to the client.

And we are done.

Login function source code

Conclusion

As you can see, we needed four Redis commands for registration and only two for login.

Probably you noticed that while describing the registration and login process with Redis we also revealed two more use cases for Redis, namely counter and rate-limiting.

Redis has a lot more use cases beyond cache and learning about them will only make you even more efficient.

Follow me to read about how I am implementing a secure production-ready registration flow with email verification and password recovery backed by Redis.


Check out my article on how I implemented the LinkedIn-like reactions with Serverless Redis.

Discussion (5)

Collapse
danbamikiya profile image
Dan Bamikiya

Hi, I enjoyed both the part 1 and the part 2 of this post. I'm somewhat new to Redis. I'm currently using it as a primary database for a URL shortener but I'm currently stuck. Can you please help me out? If you are down for it I've currently posted the question on stackoverflow. Plenty thanks in advance!

Collapse
sandorturanszky profile image
SandorTuranszky Author

Hi. I replied to your question stackoverflow.com/a/68117359/11292998

Collapse
danbamikiya profile image
Dan Bamikiya

Thanks a lot for helping me and the resources you linked helped me learn a lot more about Redis. I really appreciate your help! 🙏

Thread Thread
sandorturanszky profile image
SandorTuranszky Author • Edited

Not worries! Feel free to reach out if you need help.

Thread Thread
danbamikiya profile image
Dan Bamikiya

Thanks!