Jesse Contreras

← Back to Blog
Jun 21, 2025 6 min read Engineering

What the heck is Shufflepik?

A quick story about a Discord meme bot, what I used to ship it, and what I learned about friction, PWAs, and token life cycles.

Cover image

Why Shufflepik

Shufflepik started as a small weekend idea. I wanted a way to share random memes with my nephews and friends, but only the ones we actually posted ourselves. The idea was to bring our own memes into the mix since most of the ones from existing meme bots lacked personlization.

Each Discord server had its own Server Pool. Anything a user uploaded to Shufflepik could appear only in the corresponding server pools, keeping everything scoped to that community and giving the feed a personal feel. If ten people each dropped in five memes, that server’s pool had fifty. When someone ran /shufflepik, the bot pulled one at random from that pool and dropped it into chat. It was quick, simple, and the kind of small spark that made a channel feel alive.

That was the inspiration for the project, building something fun that stayed close to the people using it, not just another app sitting on top of an API. Shufflepik was never about reach or metrics, it was about getting a laugh from your own corner of the internet, and for a while it did just that.

The stack

API server

  • Node with Express for routes and middleware.
  • MongoDB for users, guilds, and image pools.
  • Nodemailer with Handlebars for email templates, verification and email send.
  • Sharp to resize and process uploads before storage.
  • PM2 for process management.

Discord bot

  • discord.js as the runtime with a ShardingManager for scale.
  • Shared MongoDB with the API to keep data in one place.
  • PM2 for restarts and uptime.

Frontend web app

  • Angular with RxJS and the Angular Service Worker so the app works like a PWA.
  • SCSS for styles.

Lessons learned

Email registration with verification was the wrong move. The drop off between signup and verification was huge. We told ourselves we needed a way to hold people accountable in case of bad uploads. In practice we were solving a problem that did not exist and we paid with friction. The better path was clear. Sign in with Discord and keep the flow inside Discord. Meet people where they already are.

Tip. When in doubt, choose the path with the least friction and add guardrails only after you see real signals.

Tech pains and Wins

Real time state in a PWA

Uploads and deletes need to show up right away. PWAs cache to save bandwidth and feel fast. That means a service worker will not refetch only because the app wants it to. Even with observables pushing updates a cached response can hang around.

My fix was a light cache busting plan. Versioned request keys for hot endpoints and short lived entries for pool counts and recent uploads. The app stayed quick and did not serve stale data for long.

JWT life cycle and silent refresh

I wrote a small middleware that keeps people moving without surprise sign outs. If a token is valid we verify it and attach the user to res.locals. If it is expired we try a refresh with the user id from the payload. If the stored refresh token is valid we issue a new pair. If both checks fail we return 401. Most people never see this because the refresh happens in the background.

Why res.locals helps.

res.locals is scoped to the current request. Once the middleware verifies the token, the user object is available to every downstream handler and controller in that same request without another verify or database lookup. That keeps the pipeline simple and avoids repeat work inside a single round trip.

It is not a cache across requests. A new request still needs its own token check, which keeps the API stateless and secure. Use res.locals for per request context like the user, the new jwt, or a refreshed token that the response will set as a cookie. Keep sensitive fields out of it and attach only what the next handlers need.

Demo video

This was the first full run through.

Demo of the flow end to end.

What I would change next time

  • Use Discord auth from day one.
  • Design the cache story first for any app that needs fresh data.

Final thought

Building for people is different from building for a rubric. Shufflepik worked. The real enemy was friction. It's important to shape the backend around the path the user already wants to take.