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:
- Initial Prototype (blog) – How To: Creating a Private Journal and Mood Tracking App Using .NET 6, Chat GPT, C# and Visual Studio 2022 – Jamie Maguire
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:
- https://twilio.com/blog/how-to-send-asp-net-core-identity-emails-with-twilio-sendgrid
- https://learn.microsoft.com/en-us/aspnet/core/security/authentication/accconfirm?view=aspnetcore-7.0&tabs=visual-studio
~
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.
~