At Knock we're building notifications-as-a-service so that product and engineering teams never have to build notifications infrastructure again. One of the more meta parts about building an infrastructure company is using your product to power your product, and in this post we share just how we're doing that.

How we send user invite notifications

We recently shipped our invite flow, whereby team members can invite other members into the Knock dashboard to create, maintain, and iterate on notifications. When a user is invited to Knock, we send them an email notification. It's a critical part of any invite flow, closing the loop of an invite and letting invited users know they can join their team on a given product.

Before Knock, we'd build this notification into our backend codebase and add an email template. We'd also have to account for logic that reminds invitees about pending invites. With Knock, once we've created the invite in our database we make a notify call to the Knock API to trigger the workflow for our invite. Knock just needs to know about the user (so we know where to send an email) and the workflow you want to trigger.

const knockClient = new Knock(process.env.KNOCK_API_KEY);

async function createInvite(account, email, inviter) {
  const invite = await invites.create(account, {
    email,
    invitedById: inviter.id,
  });

  // Let Knock know about the newly invited user
  await knockClient.identify(email, { email, name: "Unknown" });

  // Initiate the workflow for the user
  await knockClient.notify("account-invite", {
    actor: inviter.id,
    recipients: [email],
    cancellationKey: invite.id,
    data: {
      inviteToken: invite.token,
    },
  });

  return invite;
}

You can see in the above call we pass Knock the inviteToken so we can use it in our email when the recipient clicks the invite link.

In the Knock dashboard we've created a workflow for account-invite that will send out the initial email, delay for 3 days, and then send out a reminder email.

Invite workflow in Knock

We can edit the invite email content, preview it, and test out the links we've set in the Knock editor.

Invite email in Knock

To make sure that we don't send the invite reminder once a user has accepted the invite, we use workflow cancellation to cancel the reminder from being sent. In this case we're using the invite's unique id as a cancellation_key for Knock so that we can uniquely reference the invite notification for this specific user:

const knockClient = new Knock(process.env.KNOCK_API_KEY);

async function acceptInvite(token, user) {
  const invite = await invites.accept(token, user);

  // Cancel the pending reminder
  await knockClient.workflows.cancel(`account-invite`, token.id);

  return invite;
}

How we send in-app notifications

We're already using the pre-built React notification feed in our dashboard so we can display in-app notifications to our users. This feed component handles real-time updating, accurate badge counts, as well as marking notifications as 'seen' and 'read'.

Real-time feed in Knock

We've setup another workflow here so that users on the account will get notified via an in-app notification when the user has accepted the invite. We can add the notify call again to our acceptInvite function to handle this. Notice that this time we're sending the notification to all of the users on the account:

const knockClient = new Knock(process.env.KNOCK_API_KEY);

async function acceptInvite(token, user) {
  // ...rest of the function left the same as above

  // Generate a list of all users on the account
  const recipients = await getUsersOnAccount(invite.account);

  // Initiate the workflow on Knock
  await knockClient.notify(`account-invite-accepted`, {
    actor: user.id,
    recipients: recipients.map((u) => u.id),
    data: {
      accountId: invite.account.id,
      accountSlug: invite.account.slug,
      accountName: invite.account.name,
    },
  });

  return invite;
}

Once the user has accepted the invite, any account members will see the notification the next time they are on the dashboard. Because we're using Knock here, we could also extend this design so that if they don't see the in-app notification within some window of time, they will receive a message over email as well, all without changing our code.

Welcoming our new users

Lastly we want to send the new user a nice welcome message when they sign up for Knock. We'll do this as an email and as an in-app message, so that they have a notification waiting for them when they first open their dashboard.

const knockClient = new Knock(process.env.KNOCK_API_KEY);

async function signUpUser(attrs) {
  const user = await users.create(attrs);

  // Tell Knock about the new user
  await knock.identify(user.id, { name: user.name, email: user.email });

  // Trigger the welcome flow
  await knock.notify("welcome-to-knock", {
    actor: user.id,
    recipients: [user.id],
  });

  return user;
}

Wrapping up

On the engineering side, Knock gives us a nice abstraction in our codebase so that our code never has to care about the implementation details of building or delivering these notifications. On top of that, our code now doesn't need to know about a user's notification preferences as Knock handles that for us.

On the product side, Knock enables us to iterate on the notifications that we're sending, both in terms of the channels they are going to and the content of the notifications themselves, without the need for engineers to make updates. It also helps us debug the messages sent to our customers, which we can see in the dashboard, as well as standardizing the engagement data (clicks, tracks, opens) back from the notifications.

This is just a few ways in which we're using Knock at Knock to power a fantastic notifications experience for our customers.

If you'd like to try Knock in your product, please sign up for our mailing list below and we'll add you to our waitlist.