| | | | | | |

From Prototype to Production: Turning the Daily Tracker Journal and Mood Tracker App into a Micro SaaS

 

In this blog you will see how I converted a prototype application written in just a few hours to a production SaaS hosted in Microsoft Azure.

 

Before reading the rest of this blog, it will be useful to read these beforehand to get the full context:

 

 

 

I’ve documented this for my own reference.  It contains the key steps and what it took to go from a web application running on my development machine to a SaaS running in the cloud.

 

This will be useful for you if you plan on shipping a similar product or want to know the steps you need to take to take your app from localhost to running on the world wide web using Azure.

 

Ready? Let’s jump in.

~

Authentication and Authorisation Using Microsoft Identity

The original prototype would only run using a single account that was hard coded in a JSON settings file.  Clearly that had to change to run in the cloud.  I didn’t want to code my own authentication and authorisation code.

 

You can scaffold this capability directly within Visual Studio. It creates the required UI and back-end code.

 

In addition to this, I created a database table to store status updates and a service class to interact with the data.  A controller was created to interact the service class.

 

Identity Scaffolding in Visual Studio lets you select from a variety forms. I only needed a few of these:

  • Register an account
  • Login
  • Edit Profile

 

Register an Account

This, as you’d expects, lets you register your account.  Clicking register inserts a record into the identity tables.  All out of the box behaviour.

 

 

Login

The login screen is straight forward.  Again, all out of the box behaviour.

 

One thing to note are the Forgot Your Password and Resend email Confirmation hyperlinks underneath the login button.

 

To use these, Send Grid must be setup.  More on that in a later section.

 

Edit Profile

This screen lets you change basic information related to your profile.  The email address is readonly and set upon signup.  Phone number is optional.

The default options are used.

~

Status Timeline

The timeline form consists of two main parts.  A partial view to post a status and a second partial view that renders the list of statues in a timeline format:

 

You can browse and post for your current year.  An option to delete is also available.

You can get a feel for the underlying HTML here.

~

Migrating from SQL Express to Azure SQL.

A SQL Server express database running locally is currently used for all data.  This must be migrated to an Azure SQL instance.  I used SQL Compare for this:

 

The database is small and uses a single table to stored statuses. The remaining are created by Identity to handle user and account details.

~

Verifying Azure SQL Connectivity

The local development instance of the site is pointed to the Azure SQL connection to verify the schema and data can be accessed successfully:

After verifying this, the web application can be published to an Azure App Service.

~

Deploying Development Web App to Azure App Service.

We deploy the web app from the local development environment to an Azure App Service:

~

Verifying Deployed Site Can Connect to Production Database

With the site deployed, we verify the production site can connect to the production database:

The same data is being returned.  Excellent!

~

Webhosting and Custom Domain Name

Newly deployed sites in Azure App Service have the URL notation {site}.azurewebsites.net.

 

This is no good or easy to remember so I registered the domain http://dailytracker.co for £8.

 

I need to update the DNS from the domain name register to point to the Azure App Service that hosts the web application:

 

A good resource on Microsoft Learn that shows this: https://learn.microsoft.com/en-gb/azure/app-service/app-service-web-tutorial-custom-domain?tabs=root%2Cazurecli

 

After a few minutes, the DNS record has been refreshed and the http://dailytracker.co is pointing to the Azure App Service IP.

~

 

 

Configuring TLS/SSL Bindings

There is no TLS/SSL binding (SLL certificate) though.  This will give the site more credibility.  Luckily, you can set this up directly within Azure:

 

After a few moments we get two green ticks, and the operation has been successful:

 

Nice.

~

Verifying Production App Service, Site, and Database Have Connectivity

At this point, the production database and site can talk to each other.  The DNS has been mapped and the SLL certificate is bound.

 

Let’s try connecting to http://dailytracker.co :

Success.  Almost done now!

~

Activating Twilio Send Grid for Scalable Email Capability

Twilio Send grid needs configured to enable account creation and password recovery services.

But first, I need to reply to their questions:

This took a few days of back of and forth.

~

Location Tagging using Azure Maps

We need the option of tagging the location where the status was posted.  Doing this will provide richer data insights and will let users map their data.

First, an icon is needed so back over to Font Awesome:

 

This one will do.

 

The post status form is updated to contain this new icon. The position of the emoji icons and image upload are moved underneath the textbox:

 

This gives more real estate to type and helpful when browsing via mobile.

 

We need to get the users location somehow.  Luckily modern browsers support this.

 

The navigator.geolocation.getCurrentPosition method will return the lat and long coords.  JavaScript is created to invoke this. For testing purposes, the coords are dumped into a label:

 

This is better but not very human readable.

~

Provisioning Azure Maps

Azure Maps features has an API endpoint that converts lat/long data to human readable addresses:

 

This capability is found in the Reverse Lookup API.  You can learn more about this here.

~

Consuming Azure Maps

C# is written to consume the API which is encapsulated in a service class.  This is invoked using JavaScript which calls a Web API endpoint that returns some JSON with the human readable text:

 

Human readable objects and data are returned.  The human readable description is then bound to the label (instead of lat/long):

 

It’s starting to take shape now.

~

Configuring Twilio Send Grid

When Twilio Send Grid replied. The email services in the app can now be enabled.  First, we create an API Key in Send Grid:

 

Then implement a class that leverages Send Grid:

 

Emails are sent if you:

  • forget your password
  • create a new account (confirm account)
  • need to resend new account confirmation

~

Testing Twilio Send Grid Capabilities

Each of these are tested.  The Send Grid Activity dashboard makes it simple to check the status of each:

 

And verified in the actual target inboxes:

 

These are helpful resources when integrating Send Grid and http://ASP.NET Identity:

~

A Recap

At this point the application can:

  • register new accounts
  • account management
  • post a status with text, image, and location
  • view all statuses for the current year

 

I noticed an issue with time zones in the UI for the date a status is shared.

 

DateTime is stored in UTC format. When bound to the UI it needs to reflect the users actual timezone and not the Azure datetime.

 

This will mean capturing and storing the users timezone when they register their account or update their profile settings.

 

An offset can then be applied.  E.G.

var localTime =

TimeZoneInfo.ConvertTimeFromUtc(model.DateTime, TimeZoneInfo.FindSystemTimeZoneById(timezone))

 

To capture the user timezone, a new class must be created that inherits from the out the box IdentityUser class.

This can be called ApplicationUser.  A new property is added to represent and store the users time zone.  You can see this here:

 

The account registration form is updated to capture this:

..and rendering the time zones:

~

Charting and Reporting

A way to visualise aggregated mood data would be helpful.  Some initial requirements:

  • X axis contains each month of the year
  • months contains three columns to represent each mood
  • Y axis contains the count

 

I used Highcharts on another project so familiar with the JS library.

 

To render this chart a new view model is created to represent the data.  This could and will likely be refactored:

public class MonthlyReportSummaryViewModel
{
  public string Emotion { get; set; }
  public int JanCount { get; set; }
  public int FebCount { get; set; }
  public int MarCount { get; set; }
  public int AprCount { get; set; }
  public int MayCount { get; set; }
  public int JunCount { get; set; }
  public int JulCount { get; set; }
  public int AugCount { get; set; }
  public int SepCount { get; set; }
  public int OctCount { get; set; }
  public int NovCount { get; set; }
  public int DecCount { get; set; }

  public MonthlyReportSummaryViewModel()
  {
  }
}

 

A method is added to the StatusService  class to return aggregated data. The returned object is serialized to JSON and bound to the UI to render the chart.

public MonthlyReportSummaryViewModel GetMonthlyReport(int year, string aspNetUser_Id, string emotion)
{
  List<MonthlyReportViewModel> report = new List<MonthlyReportViewModel>();
  MonthlyReportSummaryViewModel summary = new MonthlyReportSummaryViewModel();

  report = _dbContext.Statuses.Where(u => u.AspNetUser_Id == aspNetUser_Id && u.Year == year && u.Emotion == emotion)
  .GroupBy(status => new { status.DateTime.Year, status.DateTime.Month })
  .Select(group => new MonthlyReportViewModel
  {
    Emotion = emotion,
    Month = group.Key.Month,
    StatusCount = group.Count()
  }).ToList();

  summary.Emotion = emotion;
  summary.JanCount = report.Where(a => a.Month == 1).Select(a => a.StatusCount).FirstOrDefault();
  summary.FebCount = report.Where(a => a.Month == 2).Select(a => a.StatusCount).FirstOrDefault();
  summary.MarCount = report.Where(a => a.Month == 3).Select(a => a.StatusCount).FirstOrDefault();
  summary.AprCount = report.Where(a => a.Month == 4).Select(a => a.StatusCount).FirstOrDefault();
  summary.MayCount = report.Where(a => a.Month == 5).Select(a => a.StatusCount).FirstOrDefault();
  summary.JunCount = report.Where(a => a.Month == 6).Select(a => a.StatusCount).FirstOrDefault();
  summary.JulCount = report.Where(a => a.Month == 7).Select(a => a.StatusCount).FirstOrDefault();
  summary.AugCount = report.Where(a => a.Month == 8).Select(a => a.StatusCount).FirstOrDefault();
  summary.SepCount = report.Where(a => a.Month == 9).Select(a => a.StatusCount).FirstOrDefault();
  summary.OctCount = report.Where(a => a.Month == 10).Select(a => a.StatusCount).FirstOrDefault();
  summary.NovCount = report.Where(a => a.Month == 11).Select(a => a.StatusCount).FirstOrDefault();
  summary.DecCount = report.Where(a => a.Month == 12).Select(a => a.StatusCount).FirstOrDefault();

  return summary;
}

 

This is then called for a given year & mood (smile/meh/frown).

~

Updating the Navigation Menu

Finally, we add a new menu option to the nav bar in the header to let people access their annual report:

 

Clicking the menu option returns data for the current year:

 

The landing / login page should let people know what this is about, benefits, features, and how to use it.

 

It’s back over to Font Awesome for some icons to update the landing page:

And that’s it.  Daily Tracker is live!

Find it here: https://dailytracker.co

~

Summary

Porting the prototype to a micro-SaaS involves a few steps. You can stitch together cloud services and free products then launch in a few days.

 

Consuming cloud APIs is simple.  The most painful parts  were around JavaScript issues and the back and forth with Twilio Send Grid support to get access to email capabilities.

 

The existing application and architecture  can now serve as white-label SaaS template for any other ideas in the future.

~

JOIN MY EXCLUSIVE EMAIL LIST
Get the latest content and code from the blog posts!
I respect your privacy. No spam. Ever.

Similar Posts

Leave a Reply