You share a product demo with a prospect. Five minutes later, they watch it. You don't find out until the next morning when you check your email.
If your team lives in Slack, you want that notification in Slack — not buried in your inbox. You want to see it pop up in real time so you can follow up while the video is still fresh in the viewer's mind.
SendRec already had email notifications for views and comments. We added Slack as a second notification channel that works independently — even if you have email notifications turned off.
Why incoming webhooks
Slack offers several integration methods: OAuth apps, bot tokens, and incoming webhooks. We chose incoming webhooks because they're the simplest path for self-hosters.
An incoming webhook is just a URL. You create one in your Slack workspace, paste it into SendRec's settings, and you're done. No OAuth flow, no bot to install, no scopes to configure. The webhook URL is the entire integration.
This also means zero Slack-specific infrastructure on the SendRec side. No OAuth tokens to store and refresh, no Slack API client library, no webhook verification. Just an HTTP POST to a URL with a JSON payload.
The database change
One column added to the existing notification_preferences table:
ALTER TABLE notification_preferences ADD COLUMN slack_webhook_url TEXT;
That's it. The webhook URL is stored per user. If it's NULL, no Slack notifications are sent. The URL itself is the on/off switch — no separate toggle needed.
The Slack client
The client is a small Go struct that takes the database connection pool. When asked to send a notification, it first looks up the user's webhook URL by joining on their email:
func (c *Client) lookupWebhookURL(ctx context.Context, userEmail string) (string, error) {
var webhookURL string
err := c.db.QueryRow(ctx,
`SELECT np.slack_webhook_url
FROM notification_preferences np
JOIN users u ON u.id = np.user_id
WHERE u.email = $1 AND np.slack_webhook_url IS NOT NULL`,
userEmail,
).Scan(&webhookURL)
if err != nil {
return "", err
}
return webhookURL, nil
}
If no webhook is configured, the method returns early. No error, no log noise — just a silent skip.
Block Kit messages
Slack's Block Kit lets you build structured messages with sections, context blocks, and markdown formatting. Each notification type gets its own layout.
View notifications:
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":eyes: *Someone viewed your video*\n<https://app.sendrec.eu/watch/abc123|Product Demo>"
}
},
{
"type": "context",
"elements": [
{ "type": "mrkdwn", "text": "1 view so far" }
]
}
]
}
Comment notifications include the author and comment body in a blockquote:
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":speech_balloon: *New comment on your video*\n<https://app.sendrec.eu/watch/abc123|Product Demo>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Jane* said:\n> Looks great, can you walk me through the API next?"
}
}
]
}
Daily digest notifications list all videos with their view and comment counts in a compact bullet format.
Decoupling from email notifications
This was the interesting design decision. SendRec has a notification mode setting for email: off, views only, comments only, views and comments, or daily digest. The original implementation gated all notification channels on this mode — if you set it to "off", nothing fired.
That's wrong for Slack. You might want Slack notifications but no emails. The webhook URL is your opt-in to Slack. The email mode controls email.
We decoupled them in resolveAndNotify:
func (h *Handler) resolveAndNotify(ctx context.Context, ...) {
if viewerUserID != "" && viewerUserID == ownerID {
return
}
watchURL := h.baseURL + "/watch/" + shareToken
// Slack: always send (client gates on webhook URL presence)
if h.slackNotifier != nil {
h.slackNotifier.SendViewNotification(ctx, ownerEmail, ...)
}
// Email: gated on notification mode
if h.viewNotifier == nil {
return
}
// ... mode-based email logic ...
}
Slack fires first, before the email mode gate. The Slack client itself handles the "is this user configured?" check by looking up the webhook URL. If no URL exists, it returns immediately.
The same pattern applies to comment notifications — Slack and email are evaluated independently.
Test message endpoint
When you paste a webhook URL in settings, you want to know it works before waiting for a real view. We added a test endpoint:
POST /api/settings/notifications/test-slack
It looks up the user's saved webhook URL and sends a confirmation message directly to Slack. The message uses a green checkmark and tells you notifications are working. No database lookup by email — the handler reads the URL from notification_preferences directly using the authenticated user's ID.
The settings UI
The Settings page has a new "Slack Notifications" section with a text input for the webhook URL. Save it, and you can immediately click "Send test message" to verify the connection.
We also renamed the existing notification mode dropdown section from "Notifications" to "Email Notifications" to make the distinction clear. Email and Slack are separate sections, separate controls, separate channels.
Try it
SendRec is open source (AGPL-3.0) and self-hostable. Slack webhook notifications are live at app.sendrec.eu — go to Settings, paste your webhook URL, and send a test message. The Slack client implementation is in slack.go.
Top comments (0)