HopeBot — A Quiet Companion for Slack
Inspiration
Somewhere between the "🎉 ship it!" emojis and the 2 a.m. "sorry, still debugging" messages, a lot of real distress hides in plain sight on Slack. We kept hearing the same story from friends in startups, student orgs, and open‑source communities: "By the time we noticed they were struggling, they'd already left."
We didn't want to build another dashboard that ranks people by "sentiment score." We wanted the opposite — something almost invisible, that reads the room with care, and quietly reaches out with a warm DM when someone sounds like they're having a hard time. That became HopeBot.
The guiding principle was simple:
Notice gently. Reach out privately. Never expose the person.
What it does
HopeBot is a privacy‑first Slack agent that:
- Listens to channels it's invited to and scores messages for distress using an LLM through the Lovable AI Gateway.
- When a message crosses a distress threshold, sends a private, compassionate DM to that person with grounding language and resources — never a public reply, never an @mention.
- Publishes an anonymized community wellness dashboard: counts, a rolling 0–100 wellness score, and week‑over‑week trends — no names, no message text, no per‑user drill‑down.
- Gives admins a minimal
/admin/dm-attemptsview to debug delivery (token lookup, channel open, DM send) without ever exposing message contents.
How we built it
Stack
- TanStack Start (React 19 + Vite 7) on Cloudflare Workers for SSR and server functions.
- Lovable Cloud (Supabase) for Postgres, RLS, and auth.
- Lovable AI Gateway for the distress‑classification model — no API keys to juggle.
- Slack Events API + Web API for message ingestion and DM delivery.
- Tailwind v4 with semantic tokens for the dashboard UI.
Architecture at a glance
Slack workspace
│ events
▼
/api/public/slack/events ── signature verified ──► hopebot-analysis.server
│
▼
Lovable AI Gateway (LLM)
│
distress_level ∈ [0, 10]
│
if level ≥ τ ─► slack.server → chat.postMessage (DM)
│
▼
Supabase: flags table
│
┌────────────────────────────┴─────────────────────────┐
▼ ▼
/dashboard (aggregates only) /admin/dm-attempts (token-gated)
The distress score → wellness score mapping
The public dashboard never exposes individual events. It rolls flags from the past 7 days into one number:
$$ W \;=\; \max!\Bigl(0,\; \min!\bigl(100,\; \lfloor 100 - 8 \cdot \bar{d}\rfloor\bigr)\Bigr) $$
where $\bar{d} = \frac{1}{n} \sum_{i=1}^{n} d_i$ is the mean distress level of flags this week and $d_i \in [0, 10]$. With no flags, we anchor at $W = 95$ — a high baseline, but never a hollow $100$.
What we learned
- Privacy is a product feature, not a compliance checkbox. The single best design decision we made was refusing to build a per‑user drilldown. It changed how we modeled data, wrote RLS policies, and even how we shaped the DM copy.
- SSR + protected server functions is a sharp edge. Public routes prerender with no bearer token, so any
requireSupabaseAuth‑protected loader will 401 during build. We hit this exact bug on/dashboardand fixed it by making the aggregate function truly public (aggregates only, no PII), rather than smuggling auth into a public route. - Slack failure modes are their own skill tree.
token_lookup,channel_not_found,not_in_channel,cannot_dm_bot— each needs a different remediation. We ended up logging every DM attempt with a structuredstage+outcomeso we could actually debug in production instead of guessing. - LLMs are excellent classifiers when you constrain them. Asking for a strict integer $d \in [0, 10]$ plus a short reason produced far more stable behavior than free‑form "how is this person feeling?" prompts.
Challenges we faced
- The silent 500. Our first deploy returned
{"unhandled":true,"message":"HTTPError"}on every route with no stack trace anywhere. Cloudflare's h3 runtime was swallowing pre‑dispatch throws. We had to wire a lazy server entry, a response normalizer, andglobalThiserror listeners just to see our own errors. - The
/dashboard401 loop. Because the dashboard loader called an auth‑protected server function, SSR requests had noAuthorizationheader and threwUnauthorized: No authorization header providedon every render. The fix was philosophical as much as technical: if the data is truly public, the endpoint must be too. - DMs that never arrived. Early on, the bot would "detect" distress but silently fail to DM. Root cause was a missing
SLACK_BOT_TOKENserver secret — the token lookup fell through to an env fallback that didn't exist. We added the/admin/dm-attemptsview so this kind of failure becomes obvious, not invisible. - Writing the DM itself. This was the hardest and least technical challenge. We rewrote the message maybe 15 times. The final version is short, names one feeling, offers one resource, and explicitly says "you don't have to reply."
What's next
- Per‑workspace tuning of the distress threshold $\tau$.
- Optional peer‑support routing (with consent) instead of resources‑only DMs.
- A weekly, opt‑in "how are you, really?" nudge — same privacy rules, same gentle tone.
If HopeBot ever makes one person feel a little less alone at 2 a.m., it will have done its job.
Built With
- lovableapigateway
- lovablecloud
- slaceventsapi
- supabase
- tanstack
Log in or sign up for Devpost to join the conversation.