Venn Docs

Notifications

Organizers can be told whenever someone responds to a poll, changes their answers or removes them. There are two ways:

Organizers set these up from the Notifications button on their poll. Email can also be turned on when creating a poll.

What the organizer gets

Copies for people who answer

When email is on, the poll page asks people who answer for their email, as an optional field. "Email me a copy of my answer" is ticked by default, so anyone who gives an address gets an email listing their answers, with a link to the poll and their personal link to change their answer. They can untick it, or leave the address out.

Turning on email

Every provider needs the address emails come from. It must be on a domain the provider has verified, and doesn't need to be a real inbox:

Setting Example
EMAIL_FROM Venn <notifications@example.com>

Then set up one provider. If several are set up, the first in this list is used, or the one named by EMAIL_PROVIDER (cloudflare or resend).

Cloudflare Email Sending (Cloudflare only)

No API key: the Worker sends through a binding.

  1. In the Cloudflare dashboard, open Compute & AI → Email Service → Email Sending → Onboard Domain, and pick your domain (or run npx wrangler email sending enable example.com). This adds the SPF and DKIM records.

  2. Add the binding to the Worker. With the deploy workflow, set the repository variable CLOUDFLARE_EMAIL to true. Deploying by hand, add this to wrangler.toml:

    [[send_email]]
    name = "EMAIL"
    
  3. Set the sender: npx wrangler secret put EMAIL_FROM.

Resend (Cloudflare or Node)

  1. Create a Resend account and verify your domain. If the domain's DNS is on Cloudflare, Resend can add the records for you.

  2. Create an API key with Sending access.

  3. Set RESEND_API_KEY and EMAIL_FROM. On Cloudflare:

    npx wrangler secret put RESEND_API_KEY
    npx wrangler secret put EMAIL_FROM
    

    With Node, set them as environment variables.

Once a provider and EMAIL_FROM are set, GET /api/config reports "email": true. The organizer's email becomes a required field on the create pages, and email becomes the first choice in the Notifications panel. Until then, both say that email isn't switched on, and point to custom notifications.

Adding an email provider

Providers live in src/email.js. Each one is a small object:

{
  name: 'postmark',
  configured: (env) => Boolean(env.POSTMARK_TOKEN),
  async send(env, { to, subject, text, html, headers }, { fetchImpl }) {
    // Send one message. Throw if the provider doesn't accept it.
  },
}

Add it to EMAIL_PROVIDERS, add a test next to the others in test/email.test.js, and document its settings here. Outbound requests run on the server, so the browser's Content Security Policy doesn't change.

Every email shows Venn's dog (public/img/venn-dog.png), loaded from your own instance's address. To use another picture, replace that file.

If emails land in spam

A new sending domain has no reputation yet, so the first emails can land in spam. To help:

How email stays safe

Webhooks

Paste any https:// URL. Venn sends a POST with a JSON body each time someone responds:

{
  "event": "response.created",
  "sentAt": "2026-11-02T15:04:05.000Z",
  "poll": {
    "id": "kz4d48daet",
    "title": "Design review",
    "url": "https://venn.example.com/e/kz4d48daet",
    "responses": 6,
    "finalStart": null
  },
  "response": { "name": "Ana" },
  "bestTime": {
    "start": "2026-11-04T20:00:00.000Z",
    "end": "2026-11-04T21:00:00.000Z",
    "available": 5,
    "ifNeeded": 1,
    "label": "Wed, Nov 4, 3:00 PM – 4:00 PM EST"
  }
}

event is one of response.created, response.updated, response.deleted, or test (from the Send test button).

Slack (hooks.slack.com) and Discord (discord.com/api/webhooks/…) URLs are recognized and receive a readable message instead, with Venn's dog beside it, such as:

Ana responded to “Design review”. Best time so far: Wed, Nov 4, 3:00 PM – 4:00 PM EST (6 of 6 can make it). https://venn.example.com/e/kz4d48daet

Verifying requests

Each request is signed with the poll's signing secret (whsec_…, shown in the Notifications panel):

import { createHmac, timingSafeEqual } from 'node:crypto';

function isFromVenn(rawBody, headers, secret) {
  const timestamp = headers['x-venn-timestamp'];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // reject replays
  const expected = 'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  const given = headers['x-venn-signature'] || '';
  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

Delivery rules

Edit this page on GitHub