Mitigating signup spam spikes with LaunchDarkly and Twilio Verify in an ExpressJS app featured image

Web and mobile applications that allow users to sign up must balance ease of onboarding with signup spam. Often the signup spam comes in waves over a few hours or days, rather than in a constant stream. In this tutorial, we’ll build an ExpressJS demo app that has the flexibility to toggle extra verification layers on during spam attacks. 


This demo combines Twilio Verify, a fraud verification API that can be used across many communications channels, with LaunchDarkly feature flags. Take that, spammers.

Development configuration and dependencies

To complete this tutorial you'll need:

What is verification?

When a user adds new credentials to their account, that’s verification. Verification can happen during sign-up or when updating an account to add a new phone number or email address.

Verification is different from authentication (although you can use the Verify API for both.) Authentication happens on an ongoing basis during potentially risky interactions, such as:

  • when a user logs in to their account 
  • high-dollar transactions
  • password reset flows
  • deleting data

In our verification flow, the user enters their phone number during signup. They will be sent a verification code. Once they receive the code, they enter it into a form on the app to prove that the phone number belongs to them. 

Although we’re only using SMS here to keep things simple, Twilio Verify supports many other channels such as email and WhatsApp. To learn more about verification, read Twilio’s docs.

This tutorial starts with an example app that includes a signup flow. This app was inspired by my pet rabbit, Hester, who cares deeply about optimizing carrot acquisition processes. 

First we’ll get the example app up and running. Then we’ll add Twilio credentials and test the verification flow. Finally, let’s add a LaunchDarkly feature flag to enable and disable the extra verification step at will.

Getting started with an ExpressJS and Twilio Verify example application

Run the following commands in your terminal to clone the repository, install dependencies, and start the server.

git clone https://github.com/annthurium/expressjs-twilio-verify-starter 
cd expressjs-twilio-verify-starter
npm install
npm start

Open http://localhost:3000/signup.html in a browser and you’ll see this form:

Signup form for "Hester's Bunny Food Mart" that contains a password field, phone number field, and Submit button.

To sign up, the user will input their phone number and a password. 

This form isn’t fully plumbed yet. When the full tutorial has been completed, Twilio will send the user a verification code via SMS. On the next page of the form, the user will input that verification code to complete the signup process.

Verification form for Hester's Bunny food mart. It contains an input field for the SMS verification, and a Submit button.

Note: this demo is front end only. To connect to a proper backend, you’d most likely reach for an authentication SDK like Auth0 or Clerk. Rolling your own security features is a risky choice.

You can’t actually go through the dummy signup flow until we add Twilio credentials, so let’s tackle that.

Creating a Twilio Verify service

Log in to your Twilio account or create a new one.

Open the env.example file in your editor. Grab your Twilio account SID and auth token from the console. 


If you’re using a free trial account, you’ll find these right in the center of your screen:

screenshot of the Twilio Console for a trial account, demonstrating where to copy the Account SID and auth token from.

Paste these credentials into your .env.example file. Rename the file to .env and save it. This step ensures that you don’t accidentally commit credentials to source control and compromise security.

Go to this link to create a new Verify Service so we can configure our verification flow.

Screenshot of the Twilio console, in an empty state, waiting for Verify services to be created.

Name your service “launchdarkly-signup-form.” When Twilio sends your users the verification code, it will show them the service name. Select the “SMS” verification channel. Check the box that says “Authorize the use of friendly name” to ensure that you have the right to this name and that it can be used in SMS communication.

Optional but recommended: enable Fraud Guard. On the next screen, you’ll see a Verify service SID. Copy and paste that value into your .env file and save it.

Screenshot of Twilio Verify service settings, where the Verify Service SID can be found.

Open index.js in your code editor. Uncomment line 12 to initialize the Twilio server.

const twilioClient = twilio(accountSid, authToken);

Restart the server just in case it didn’t automatically pick up the new variables. Reload http://localhost:3000/signup.html in your browser. Type your cell phone number and a password into the form. Click “Submit.”

On your cell phone, you should receive a SMS verification code from Twilio: “Your launchdarkly-signup-form verification code is: 12345”. If your Twilio account is in free trial mode, this message will say SAMPLE APP instead of the Verify Service name.

Part of Verify's magic is that you don’t need to provision a phone number—the API automatically handles that for you.

This app uses Twilio Lookup’s phone number formatting capabilities to gracefully handle imperfectly formatted user input. It also passes the user’s phone number back through to the verification form, so they don’t need to retype it.

Type the verification code into the form and click “Submit.” You’ll see a confirmation page.

Hester's Bunny Food Mart successful signup confirmation page.

Adding a LaunchDarkly flag to conditionally enable SMS verification

Head over to the LaunchDarkly app. Click on either of the “Create flag” buttons.

empty state of the LaunchDarkly app showing two "Create flag" buttons.

Create a flag using this configuration:

  • Name: extra-verification-new-users
  • Description: When flag is enabled, require verification of the user’s phone number in order to complete the signup flow.
  • Configuration: Custom
  • Type: boolean

Flag configuration screenshot.

Set up the variations like so:

  • extra verification on: true
  • extra verification off: false

Screenshot of flag variations configuration.

Click “Create flag” at the bottom of the page. Turn your flag On. Click "Review and save."

Screenshot showing how to toggle the extra-verification-new-users flag On.

If LaunchDarkly is configured to require confirmation, you'll need to add a comment to explain why you're enabling the flag. In this case it's a spam attack. Type the comment. Click “Save changes.”

Screenshot of flag changes confirmation modal.

You’ll need the LaunchDarkly SDK key to evaluate a feature flag in the app. Click the menu with 3 dots. Select “SDK Key” from the dropdown menu to copy it. Paste the SDK key into your .env file. Save the .env file, which is the last configuration step.

Screenshot demonstrating where to copy your LaunchDarkly SDK key.

Next, we’ll tweak this code to add a LaunchDarkly feature flag. Open index.js. You can copy the whole file, or modify the commented functions and lines below for a more detailed understanding.

const express = require("express");
const path = require("path");
const serveStatic = require("serve-static");
const bodyParser = require("body-parser");
require("dotenv").config();
const app = express();


const twilio = require("twilio");


// add the following line to import the LaunchDarkly Server SDK
const LaunchDarkly = require("@launchdarkly/node-server-sdk");


const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const twilioClient = twilio(accountSid, authToken);
const verifyServiceSid = process.env.VERIFY_SERVICE_SID;


async function createSMSVerification(phoneNumber) {
 const verification = await twilioClient.verify.v2
   .services(verifyServiceSid)
   .verifications.create({
     channel: "sms",
     to: phoneNumber,
   });


 console.log("Twilio verification created:", verification.sid);
}


// add the following line to initialize the LaunchDarkly client
const ldClient = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY);


app.use(serveStatic(path.join(__dirname, "public")));
app.use(bodyParser.urlencoded({ extended: false }));


async function formatPhoneNumber(phoneNumber) {
 try {
   const lookup = await twilioClient.lookups.v2
     .phoneNumbers(phoneNumber)
     .fetch();
   return lookup.phoneNumber;
 } catch (error) {
   console.error("Error formatting phone number:", error);
   return phoneNumber; // Return original number if lookup fails
 }
}


async function checkVerificationCode(phoneNumber, verificationCode) {
 try {
   const verificationCheck = await twilioClient.verify.v2
     .services(verifyServiceSid)
     .verificationChecks.create({
       to: phoneNumber,
       code: verificationCode,
     });


   console.log(`Verification status: ${verificationCheck.status}`);
   return verificationCheck.status === "approved";
 } catch (error) {
   console.error("Error checking verification code:", error);
   return false;
 }
}


// modify this function to add a LaunchDarkly flag evaluation
// and logic to use the flag to determine whether to show the extra verification step
app.post("/verify", async (req, res) => {
 const context = {
   kind: "user",
   anonymous: true,
   key: "anonymous-123",
 };
 const extraVerification = await ldClient.variation(
   "extra-verification-new-users",
   context,
   false
 );
 console.log("extraVerification", extraVerification);


 if (extraVerification === false) {
   return res.redirect("/success.html");
 }
 const phoneNumber = req.body["phone"];
 // Use Twilio Lookup to format the phone number as E.164
 const e164PhoneNumber = await formatPhoneNumber(phoneNumber);
 await createSMSVerification(e164PhoneNumber);
 // Redirect to the verification form page with the phone number as a query parameter
 // so the user doesn't have to re-enter it
 const encodedPhoneNumber = encodeURIComponent(e164PhoneNumber);
 res.redirect(`/verification-form.html?phone=${encodedPhoneNumber}`);
});


app.post("/submit-verification-code", async (req, res) => {
 const verificationCode = req.body["verification-code"];
 const phoneNumber = req.body["phone"];


 if (!phoneNumber || !verificationCode) {
   return res
     .status(400)
     .send("Phone number and verification code are required.");
 }


 const isVerified = await checkVerificationCode(phoneNumber, verificationCode);


 if (isVerified) {
   res.redirect("/success.html");
 } else {
   res.status(400).send("Invalid verification code. Please try again.");
 }
});


// Add the waitForInitialization function to ensure the client is ready before starting the server
const timeoutInSeconds = 5;
ldClient.waitForInitialization({ timeout: timeoutInSeconds }).then(() => {
 const port = 3000;
 const server = app.listen(port, function (err) {
   if (err) console.log("Error in server setup");
   console.log(`Server listening on http://localhost:${port}`);
 });
});


// Add the following new function to gracefully close the connection to the LaunchDarkly server.
process.on("SIGTERM", () => {
 debug("SIGTERM signal received: closing HTTP server");
 ld.close();
 server.close(() => {
   debug("HTTP server closed");
   ldClient.close();
 });
});

If you go through the signup flow at http://localhost:3000/signup.html, SMS verification should be enabled by default. 💥

Try turning off the feature flag. In the LaunchDarkly app, toggle the extra-verification-new-users flag off.

Screenshot showing how to toggle the extra-verification-new-users flag Off.

If you try again, the signup flow should take you straight through to the success page rather than requiring SMS verification.

Conclusion: using LaunchDarkly feature flags to stop fraudulent signup requests

If you’ve been following along, you’ve integrated LaunchDarkly feature flags into your signup flow to conditionally enable Twilio Verify during spam spikes. Nice job!

As engineers, we often wrangle two opposing forces such as ease of use and fraud. Tooling that is flexible enough to let us make adjustments based on local conditions is hugely helpful, especially when dealing with a mission-critical path like signups.

If you enjoyed this post, you might be interested in:


Thanks for reading! If you have any questions, or you want to see some cute rabbit pics, you can reach me via email (tthurium@launchdarkly.com), X/Twitter, Mastodon, Discord or LinkedIn.

Hester the golden dutch rabbit and Ellie the v0id cat. They have a complex sibling rivalrly.

Like what you read?
Get a demo
Related Content

More about De-risked releases

October 17, 2024