Inspiration

Anonymous Q&A tools for meetings already exist, but most of them live outside the tool people are already in — you paste a link, open a separate app, and hope everyone bothers to join. We wanted something that lived directly inside Slack, since that's already where most companies run their meetings, standups, and all-hands.

The other half of the inspiration was a worry, not a feature request: anonymity tools have a well-known failure mode — they become a shield for people to be cruel instead of honest. We didn't want to build a tool that quietly enabled that. So from day one, "anonymous" and "safe" were treated as one requirement, not two.

How we built it

Pulse is a Slack bot (Node.js + Bolt, running in Socket Mode, so no public URL is needed) wired around Claude for every judgment call the app has to make, and Slack's Real-Time Search API for grounding those judgment calls in what's already been said in the workspace.

We built it as a hexagonal architecture (ports & adapters): the domain layer (sessions, questions, pulses) has zero knowledge that Slack or Claude even exist. Everything Slack- or AI-specific is an adapter behind an interface, so the business rules — one active session per channel, one vote per person, moderator-only actions, anonymity thresholds — are testable and swappable in isolation.

Claude does five distinct jobs in the pipeline, each behind its own port:

  1. Detect + translate an incoming DM to the board's working language, so people can ask in their own language without splitting the conversation.
  2. Moderate the question before it's ever public, blocking harassment or personal attacks while still allowing pointed, uncomfortable questions about decisions.
  3. Deduplicate semantically similar questions instead of letting the board fill up with five phrasings of the same thing.
  4. Judge whether a Real-Time Search result already answers the question, before bothering the room with it again.
  5. Summarize the session on close into a short executive summary of what got answered and what didn't.

One detail we're proud of: not all five are treated the same way on failure. Translation, deduplication, and the "already answered" check are fail-open — if Claude errors out, the question still goes through unmodified, because a translation hiccup shouldn't block someone's question. Moderation is the one exception: it's fail-closed. If the safety check can't complete, the question is held rather than published unfiltered. Anonymity should never be the thing that lets a broken API call slip a personal attack onto the board.

State is deliberately in-memory only — no database. A session's questions, votes, and identities exist only while the meeting is happening and are discarded the moment it ends, which was less an engineering shortcut than a privacy decision: nothing to store means nothing to leak.

Challenges we ran into

The Real-Time Search API turned out to be the trickiest integration. The installed @slack/web-api SDK version predates a typed method for it, so we had to call it through the generic client.apiCall escape hatch and hand-shape the request/response ourselves against the raw docs.

The other real challenge wasn't code, it was judgment: tuning Claude's moderation prompt to block genuine toxicity without also swallowing legitimate, sharp criticism of a decision or a process — a "your rollout plan has a hole in it" question needs to survive; a personal insult doesn't. That line moved several times during testing.

We also had to think carefully about anonymity as a statistical property, not just a logging policy — mood polls and custom polls only reveal results once at least 5 people have responded, and the lighter-weight "did this answer land?" micro-poll needs at least 2. That way, no single person's response can ever be read straight off the published result.

What we learned

That "safe by design" often means picking, deliberately, which failures are allowed to fail open and which aren't — and that this is an architectural decision, not an afterthought bolted on later. And that hexagonal architecture, which can feel like over-engineering on a small hackathon project, paid for itself almost immediately: swapping in a fake Claude adapter for tests, or reasoning about "what happens if this AI call throws," was trivial because every AI call was a port with one clear contract.

Built With

  • bolt
  • claude
  • hexagonal
  • jest
  • node.js
  • render
  • slack-real-time-search-api
  • socket
Share this project:

Updates