Using Heroku to Quickly Build a SaaS Startup - Part 2 Integrating Twilio

Michael Bogan - Sep 23 '20 - - Dev Community

In the "Using Heroku to Quickly Build a Multi-Tenant SaaS Product" article, I documented the foundation for a new SaaS solution that I am building (initially for my sister-in-law) — utilizing the Heroku ecosystem. At the end of that article, I had planned to write about the core technologies (Spring Boot, Angular 9, ClearDB, Okta, GitLab and Heroku) in place, as we raced for the 1.0.0 release of the solution.

However, things have been moving fast around here, and I largely attribute the speed of development and end-user adoption to everything that Heroku brings to the table for this service. As I have noted before, using Heroku as the destination for my service and GitLab's built-in CI/CD pipelines — updates that are merged from a feature branch into the master branch of the client or service are automatically deployed into production (Heroku) without any further intervention.

Instead of starting the 1.0.1 release, the project is actually finishing up the 1.0.3 release. Below is an updated feature map:

ht1

Progress Via Screenshots

Since using Spring JPA to create the initial schema, which it did without any issues, I have been keeping the fitness.sql up to date manually as model updates have been required. Using IntelliJ IDEA and built in database tooling (including yFiles), I was able to quickly create the following diagram of the current MySQL/ClearDB database structure:

ht2

As noted in the prior article, everything ties back to the ID of the tenant (or fitness trainer) - which is also enforced in the base JPA Specifications employed at the service level.

These changes, along with screens for Client configuration and Workout configuration, allowed the Session screen to be introduced:

ht3

The Sessions (which is an instance of a workout and at least one client) are presented in an optional calendar view to give the trainer a view which is easy to comprehend at a glance:

ht4

In the example above, there is only one event scheduled - since the screen shot is from my test account in the production instance of the SaaS solution.

There is also a training-mode version of the Session screen, which is mainly a read-only adaptation and consolidates data for quick reference.

ht5

The icons on this screen are still active, so that that trainer can not only check-in clients, but also provide session information (score and comments) too:

ht6

Along the Way ... Twilio Was Discovered

One of items which was slated for the 1.0.4 release was referred to as Client Check-In. We soon realized the having a mechanism for the trainer to communicate with the clients was an important aspect, which needed to be handled next.

Below are the requirements that were employed for this feature:

  • send reminders to clients ~24 hours before their scheduled session
  • send a workout summary after their workout
  • resend a reminder for a session
  • allow trainer to send a broadcast text message (e,g, "I am going to be on vacation")
  • provide a link to allow client to confirm/cancel their session (planned for a future article)

Since the concept of a mobile client is beyond the current feature roadmap, I needed to find a solution that would work with most clients. Knowing that each client was communicating with my sister-in-law via a mobile device, I decided to use SMS (or text messages) to provide a lightweight client for the application.

It did not take long to settle on Twilio — especially since two colleagues (roberttables and blendedsoftware) who broadcast on Twitch had been participating in Twilio's "TwilioQuest" awesome learning experience. In fact, I would not be surprised if both still have videos on their channels of them playing the "TwilioQuest" game.

Twilio provides the functionality I need with a robust API. I was able to quickly set up a trial account (which communicates only with mobile numbers that are added to the system), but that was enough for me to validate the functionality.

Preparing for Twilio

The first step is to create a new account in Twilio by visiting the following URL:

https://www.twilio.com/try-twilio

Which should direct first-time users to a screen similar to what is shown below:

ht7

Once everything is setup, the Twilio dashboard will appear as shown below:

ht8

The Account SSID and Auth Tokens are accessible via this screen.

I wanted to configure Twilio programmatically in mode project, so that using my production instance of Twilio was based upon environment variables passed in from Heroku. Below is a list of the attributes that I am managing in my API:

    jvc:
     sms:
       twilio:
         enabled: ${TWILIO_ENABLED}
         account-sid: ${TWILIO_ACCOUNT_SID}
         auth-token: ${TWILIO_AUTH_TOKEN}
         phone-number: ${TWILIO_PHONE_NUMBER}
         enable-reply: ${TWILIO_ENABLE_REPLY}
         reply-host: ${TWILIO_REPLY_HOST}
         trial: ${TWILIO_IS_TRIAL}
         trial-phone-number: ${TWILIO_TRIAL_PHONE_NUMBER}
         cron-schedule-reminder: "0 17 * * * ?"
         cron-schedule-summary: "0 27 * * * ?"
Enter fullscreen mode Exit fullscreen mode

The README.md for my API repository provides the documentation details for the environment-specific values:

    ${TWILIO_ENABLED} - Twilio (SMS) enabled [jvc.sms.twilio.enabled] (set to false in order to disable SMS)
    ${TWILIO_ACCOUNT_SID} - Twilio (SMS) account SID [jvc.sms.twilio.account-sid] (leave unset to disable SMS, recommended for non-Production environments)
    ${TWILIO_AUTH_TOKEN} - Twilio (SMS) auth token [jvc.sms.twilio.auth-token]  (leave unset to disable SMS, recommended for non-Production environments)
    ${TWILIO_PHONE_NUMBER} - Twilio (SMS) phone number to use for sending messages [jvc.sms.twilio.phone-number]
    ${TWILIO_ENABLE_REPLY} - Twilio (SMS) enablement of reply functionality [jvc.sms.twilio.enable-reply]
    ${TWILIO_REPLY_HOST} - Twilio (SMS) reply host URL (of Angular client) to use when reply functionality is enabled [jvc.sms.twilio.reply-host] (value should not end with a /)
    ${TWILIO_IS_TRIAL} - Twilio (SMS) trial indicator [jvc.sms.twilio.trial] (use for non-Production instances)
    ${TWILIO_TRIAL_PHONE_NUMBER} - Twilio (SMS) trial phone number to send to [jvc.sms.twilio.trial-phone-number] (use for non-Production instances)
Enter fullscreen mode Exit fullscreen mode

The cron based items determine the schedule of the session reminder and session summary jobs which are running in each environment (if enabled).

Within Spring Boot, a simple TwilioConfigurationProperties class was created:

    @Data
    @Configuration("twilioConfigurationProperties")
    @ConfigurationProperties("jvc.sms.twilio")
    public class TwilioConfigurationProperties {
       private boolean enabled;
       private String accountSid;
       private String authToken;
       private String phoneNumber;
       private boolean enableReply;
       private String replyHost;
       private boolean trial;
       private String trialPhoneNumber;
       private String cronScheduleReminder;
       private String cronScheduleSummary;
    }
Enter fullscreen mode Exit fullscreen mode

With these values set up, I can use the Run/Debug dialog in IntelliJ IDEA to pass in the expected values:

ht9

In Heroku, these values are set up in the environment:

ht10

Keep in mind, the Heroku values are different, because that is actually the production instance that I have running.

The SMS Service

Within the SmsService, the following public methods are available:

    @RequiredArgsConstructor
    @Slf4j
    @Service
    @Transactional
    public class SmsService {
     @Resource(name = "requestScopeUserData")
     private UserData userData;
     private final AttendeeRepository attendeeRepository;
     private final MotivationalQuoteRepository motivationalQuoteRepository;
     private final SessionRepository sessionRepository;
     private final TwilioConfigurationProperties twilioConfigurationProperties;
     public void sendGroupMessage(GroupSmsMessage groupSmsMessage) throws FitnessException {
       // allows a trainer to send a SMS message to a select group of customers
     }
     public void sendSimpleMessage(SimpleSmsMessage simpleSmsMessage) {
       // allows the system to send a single SMS message, which is used to send a message to the trainer each time a client confirms or cancels their session
     }
     public void resendSingleReminder(long attendeeId) {
       // allows the trainer to render the session reminder.
     }
     @Scheduled(cron = "${jvc.sms.twilio.cron-schedule-reminder}")
     public void sendSmsMessages() {
       // allows the trainer to render the session reminder.
     }
     @Scheduled(cron = "${jvc.sms.twilio.cron-schedule-summary}")
     public void sendSmsSummaryMessages() {
       // ran hourly, provides a workout summary (including a random fitness quote) to the customer - which can include:
       // -  the score from the session
       // - any comments provided by the trainer
       // - a random fitness quote
     }
    }
Enter fullscreen mode Exit fullscreen mode

These methods are called either by a scheduled task or triggered from a request from the Angular client. The next section presents and example of how the Angular client interacts with the Spring Boot service in order to communicate with the Twilio service.

Example - Sending a Group Message

Sending a Group Message is initiated by the Angular client. An example is shown below:

ht11

The request is passed to the following controller in Spring Boot:

    @PostMapping(value = "/sms")
    public ResponseEntity<Void> postSession(@RequestBody GroupSmsMessage groupSmsMessage) {
       try {
           smsService.sendGroupMessage(groupSmsMessage);
           return new ResponseEntity<>(HttpStatus.CREATED);
       } catch (FitnessException e) {
           log.error(e.getMessage());
           return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
       }
    }
Enter fullscreen mode Exit fullscreen mode

The GroupSmsMessage object has the following properties:

    @Data
    public class GroupSmsMessage {
       private String messageBody;
       private List<ClientDto> distributionList = new ArrayList<>();
    }
Enter fullscreen mode Exit fullscreen mode

The controller then calls the following method:

    public void sendGroupMessage(GroupSmsMessage groupSmsMessage) throws FitnessException {

       log.info("sendGroupMessage(groupSmsMessage={})", groupSmsMessage);

       if (CollectionUtils.isEmpty(groupSmsMessage.getDistributionList())) {
           throw new FitnessException(FitnessException.ENTITIES_MISSING, "Could not send group SMS message to an empty distribution list.");
       }

       if (StringUtils.isEmpty(groupSmsMessage.getMessageBody())) {
           throw new FitnessException(FitnessException.ENTITIES_MISSING, "Could not send group SMS message with an empty message body.");
       }

       if (!smsEnabled()) {
           throw new FitnessException(FitnessException.UNAUTHORIZED, "sendGroupMessage() SMS is NOT enabled to run on this instance");
       }

       Twilio.init(twilioConfigurationProperties.getAccountSid(), twilioConfigurationProperties.getAuthToken());

       log.info("sendGroupMessage() started");
       logSmsInformation();
       int successCount = 0;
       int errorCount = 0;

       for (ClientDto clientDto : groupSmsMessage.getDistributionList()) {
           Message message = Message.creator(
             new PhoneNumber(twilioConfigurationProperties.isTrial() ? twilioConfigurationProperties.getTrialPhoneNumber() : clientDto.getPersonDto().getPhoneNumber()),
             new PhoneNumber(twilioConfigurationProperties.getPhoneNumber()),
             groupSmsMessage.getMessageBody())
             .create();

           if (message.getErrorCode() == null && StringUtils.isEmpty(message.getErrorMessage())) {
               successCount++;
           } else {
               errorCount++;
               log.error("Error #" + message.getErrorCode() + "(" +  message.getErrorMessage() + ") occurred sending smsMessage=" + groupSmsMessage.getMessageBody());
           }
       }

       log.info("Successfully sent {} SMS messages ({} errors)", successCount, errorCount);
       log.info("sendGroupMessage() completed");
    }
Enter fullscreen mode Exit fullscreen mode

In the code above, after checking for elements which can yield a Fitness exception, the Twilio.init() method is called. Thereafter, a Message is created and the logic has been included to function in production and non-production environments. This basically ties to using the provided trial phone number when not running in production. Everything else is straight forward and pass the data received by the Angular client directly to the target distribution list.

Conclusion

The goal of this article was to outline some client communication needs for the SaaS solution I am building. Thereafter, I provided an example of how easy it is to create a Twilio instance and integrate the solution into my Spring Boot service and Angular client. Using Spring Boot and an Angular client, I was able to quickly and easily get SMS communication configured in use within a matter of hours. In my view, Twilio provides an awesome compliment to the services I am using with Heroku.

From a cost perspective, my sister-in-law has been using the SMS functionality for right at two months now and I just added more money into my production Twilio account two days ago. At this point, the total usage has been right at $10 a month - which is far less than I expected to use.

In the next article, I plan to discuss how Twilio was used to send the session reminder, providing a mechanism where clients can confirm or cancel their attendance to this session. I had initially planned to include everything in one article, but I felt it would be better to create an article dedicated to this functionality. In another future article, I plan to dive into how an end-user's reply to the original (Twilio) SMS can be redirected to the actual trainer for that customer. Exciting articles planned, for sure!

Have a really great day!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .