Building a Slack Deploy Queue Bot: Lessons from NestJS, BullMQ, and Redis in Production
Over the past few months I built a side project that taught me more about production system design than any course. A Slack bot for deploy queue management. This isn't about the business side of it, it's a technical breakdown of the architecture decisions, the real problems I hit building a Slack app with NestJS, and what I learned solving each one. The problem Every engineering team has lived this. Two people deploy at the same time, one overwrites the other, and it turns into "who's touching prod right now?" shouted into a Slack channel. Sounds simple. Solving it properly across multiple teams, multiple environments, with timeouts, without ever locking anyone out, is not. Stack and why NestJS + TypeScript on the backend, PostgreSQL via Prisma, Redis + BullMQ for background jobs, @slack/bolt for the Slack integration. Choosing NestJS wasn't just preference. Its modular architecture (modules, providers, guards, interceptors) mapped really well to the domain. Every entity (Workspace, Project, Environment, Queue) became its own module, with the repository pattern keeping Prisma out of the business logic layer. The hardest problem: dynamic modals in Slack Block Kit Slack's Block Kit doesn't natively support a select input that reloads its options based on another select's value, within the same form. If you want "pick a project, then load that project's environments," there's no built-in prop for that. The solution combines two Bolt event types. A block_actions listener on the project select fetches the environments, then re-renders the whole modal via views.update : app . action ( ' project_select ' , async ({ ack , body , client }) => { await ack (); const selectedProjectId = body . actions [ 0 ]. selected_option . value ; const environments = await environmentService . findByProject ( selectedProjectId ); await client . views . update ({ view_id : body . view . id , view : buildModalWithEnvironments ( environments ), }); }); The detail that tripped me up the most: t