STORY
How I'm Going to Build a Badges/Achievements system in BOLT.FUN
AUTHOR
Joined 2022.06.04
DATE
VOTES
sats
COMMENTS

How I'm Going to Build a Badges/Achievements system in BOLT.FUN

So I was today working on planning the development of the badges feature & what we will need.

We talked about the UX of this feature in a previous article, this one is more about the technical side of it.

You can read them here:
- Thoughts on the Nostr Badges Feature for BOLT.FUN
- Bolt.Fun Badges

So basically, we have 2 types of badges, I'll call them:

  • One Time Badges

  • Incremental Badges

The first type is the badges that the user will get by doing something one time (or it might be a badge that a single user can get).

Examples:

  • Completing your profile

  • Publishing your first story

  • Winning 1st place in Legends of Lightning

The second type of badges are the ones that the user will get after doing something for a certain amount of times:

Examples:

  • Write 50 stories

  • Publish 3 projects

  • Reply on 10 questions

So this means basically that we need to keep track of each user's "progress" on each "Incremental Badge" we have.

But since we could have many badges on many different things, we will have to add this badge-tracking-related logic in so many parts of the code...

And this doesn't look good to me at all.

First, it pollutes the code & make it less readable.

It also mixes 2 non-related things in one function (the query/mutation resolver).

I thought there has to be a better way.

Most video games have an "Achievements" system that you could say share a lot of things with what we want to build.

They also track many many different totally unrelated things.

& they also have one-time & incremental achievements.

& they also don't want to make the code a mess.

So I went to look at how game developers go about implementing such a system & read several articles about that.

I found different ways to go about this usually, but I took the one that has the most commonality with our own case.

Which is using something similar to the "Observer Pattern"

We have a central single "Awards Manager Service" that listens to different events that can be emitted from any place in our code.

So the createStory mutation resolve needs to just emit an event saying: "Hey, this user created this story", and it doesn't need to care about anything else.

The "Awards Manager Service" will then see this event, & do whatever logic it wants depending on the type & the data of this event. (Maybe it doesn't care about it at all, & maybe it affects multiple badges)

Okay, so far so good.

I then realized that we have a problem implementing this kind of system in our own project.

Mainly because we use serverless functions for everything.

So we will most likely not be able to have the "Awards Manager Service" run its logic during the execution time of a user's request.

This is something that should run async outside of the user's request. (Maybe even a little bit later).

And we don't have a queue service for the serverless environment itself, so we will have to do this using the database.

I thought then that maybe we can store a list of different users' actions that happened in the past 15 minutes for example, & then process them in a single batch using Netlify's scheduled functions.

However, Netlify's scheduled functions also have a time limit of 10 seconds...

So there's a chance that the job fail due to function timeout...

So a possible solution could be to:

Have a scheduled function trigger a background function that will be responsible for processing the batch of users' actions!

And a background function has a maximum run time of 15 minutes, so it should be more than enough!!

A little convoluted more than I like, but couldn't come up with a better approach for now. So any ideas are welcome!

So...

- The user makes an action.

- The resolver function will create a "UserAction" & store it in the DB with some relevant data.

- The schedule job run & triggers the background function

- The background function runs & fetches all the new UserActions from the DB

- The background function do its thing

Now what is exactly 'do its thing'???

Well...

This brings us to the second part of this: The badges service.

I'm still not 100% sure about this one, but here's how my last plan for this service looks:

We have a 'Badge' table where we store all the badges that we have in the platform.

Badge
=====
id
slug @unique
title
description
badgeDefinitionNostrEventId
incremenetsNeeded?
incrementOnAction? (FK)

If the badge is of the "Incremental" type, then we will have the last 2 fields filled.

Now we also need to keep track of how much "progress" the user has made on any of the "Incremental" badges, so we have another table:

UserBadgeProgress 
=================
badgeId
userId
progress 

Finally, whenever the user actually gets issued a badge, we will store that in a different table:

UserBadge
=========
userId
badgeId 
badgeEventOnNostrId
badgeAwardNostrEventId
awardedAt

Here, you might ask: "Why create a new table for this??

Why not just figure/compute this from the UserBadgeProgress table??"

That's what I thought of at the beginning, but I then remembered that the "Single-time" badges will likely not have any entry in the UserBadgeProgress table, or we will have to create a fake one, so I figure doing this with a different table would be cleaner.

Okay, so that was the part related to the core of the Awards Service itself.

We will also need 2 other tables to manage the "listening to users' actions" part.

For this, we will mainly need 2 tables:

Action
======
id
name @unique (e.g. "create-story", "create-project", "create-comment", "update-profile", ...etc)

UserAction
==========
userId
actionId
actionPayload json (should be as minimal as possible (like { id: number })
isProcessed  ## will become `true` after the background function successfully finish processing it

In Action table, we will define all the different types of actions that incremental badges will care about.

& in UserAction table, resolvers functions will create an object whenever a user do something, & the Awards Service will later read from it & delete the events it handled.

Phewwwwwh... 😤😤

I think that's almost it.

If you've read this far, thank you!!

I just planned this today, & I haven't built a similar system before.

So there's most likely a lot of room for improvements.

So if you've built something similar, or have any ideas/suggestions/feedback, I'd greatly appreciate it!

Until next time 👋