Skip to content

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Storm tech: How to code a personalized hurricane status app

How I built a personal situation log app during Hurricane Milton using Node.js, SQLite, and Tailwind.css to monitor power, Wi-Fi, and cell signal in real time.

Nov 27, 2024 • 11 Minute Read

Please set an alt value for this image...
  • Software Development
  • Guides

Having lived in the U.S. state of Florida my entire life, and in Central Florida since college, I’m very familiar with hurricanes. Being directly affected by a hurricane here is a matter of when, not if.

In early October 2024, all of the Central Florida area faced potential damage, destruction, and at the very least, disruption, from a large hurricane named Milton. Friends and family across the country reached out with concern before the storm, and I started to think about a simple way to allow everyone to stay up-to-date with how I was experiencing the storm during and after landfall. 

After a few hours of stress-coding in between hurricane prep, I got the first request log captured by my Milton Tracker app. Now that the hurricane has passed and my family and friends are safe, this post is about how I architected and built that application so you can solve similar problems in the future (although I hope you don’t have to).

Here's a quick preview of the components I used:

  • A back end written with Node.js and Express
  • A front end driven by EJS templates and Tailwind.css
  • Data stored in an SQLite database
  • Hosted on a Digital Ocean VPS Droplet (Virtual Private Server, or a server in the cloud accessed through the Digital Ocean platform)
  • A personal iOS application to quickly update the database

Defining requirements

To define requirements, I started by thinking about how users would experience the application. The only user-facing screen is a table with columns for data that show viewers:

  1. if I have power at home 
  2. if I have a working Wi-Fi connection at home
  3. if I have a cell phone signal

The last column contains a timestamp of when the states of those three data points were last recorded, and the rows are arranged in reverse chronological order. Finally, there's a "last updated" timestamp at the very top of the table for quick reference.

Friends and family could open this URL and see the state of my personal access to power, Wi-Fi, and cell signal at the time I entered my last update. If I hadn't made an update in a few hours, then the expectation would be that I didn't have all three.

This application would need to be hosted on a server that required minimal interaction to stay running—if I lost power and internet, I wouldn't be able to connect to it to keep it running, so I came up with these requirements:

  • A database to store the three states and timestamps
  • An endpoint to update the database
  • A page to display the table of all entered states and timestamps
  • A server that has high availability
  • An interface that would be as accessible as possible in low-power, low-internet speed situations to send data to that endpoint

The database: SQLite

SQLite is a great choice for a fast relational database. Unlike systems with larger feature sets like PostgreSQL, SQL Server, and MySQL, SQLite can be quickly installed nearly anywhere, and the entire database gets stored in a single file.

This was helpful because I didn't spend weeks planning and building this application, and using SQLite got me up and running in just a few minutes.

This is the schema I used:

id INTEGER PRIMARY KEY AUTOINCREMENT,
hasCellSignal BOOLEAN,
hasInternet BOOLEAN,
hasPower BOOLEAN,
timestamp INTEGER

I set the timestamp to an integer so that I could store dates and times in the Unix Time format. In this format, dates and times are measured as the number of seconds since January 1, 1970.  There are 86,400 seconds in a day, so the Unix time for January 1, 1970 at midnight would be 86400. The Unix time for October 9, 2024 at 8 p.m. EDT—around the time when I started feeling the effects of Hurricane Milton—is 1728518400.

Storing times as integers was a fast way to get going, and I could always use date formatting tools later on to display those dates and times in different ways in the front end.

The back end and API: Node.js with Express

Node.js and Express are great tools for getting a back end up quickly, so for this application, I used Node v22 and Express 4. The application is pretty simple, and contains two routes: a GET route named /milton that runs a SELECT query on the database and then passes all that data to a template, and a POST route named /api/save to accept requests with a JSON body and insert new values into the database for hasCellSignal, hasInternet, hasPower, and timestamp.

Since the application was so small and came together quickly, I opted to put everything into a single app.mjs file. If it grew larger, I would have organized my code in a more modular way, but here it wasn't necessary.

The server: Digital Ocean, NGINX, and PM2

I already had a Digital Ocean account, so I spun up one of their cheapest Droplets that gives me a VPS for about $0.14/day. There, I installed Node and SQLite binaries.

I needed high availability because if the application crashed or the server rebooted, I may have been without internet and unable to manually connect and restart the application. I installed the runtime manager PM2. PM2 allowed me to define configuration options like environment variables and rules for automatic application restarts after an application crash or full server crash or reboot.

I could have handled features like rate limiting, 404 error pages, and eventually HTTPS directly in Node, but I decided to install the NGINX server and use it as a reverse proxy. That way, all user requests that came into the Digital Ocean server got intercepted by the NGINX first, and then NGINX could pass those into the Node application. That allowed me to handle server features in NGINX and keep Node focused on database and application logic.

Enhancing application security: Validation, logging, Content-Security-Policy, and HTTPS

Since the POST route accepts user input, I used the express-validator library to validate those boolean and timestamp values before running the INSERT query to avoid malicious or bad data reaching the database.

I wanted to store some request logs just because I was curious how many people were accessing the application, so I chose the library winston. I configured the logs to write to the server console log and to log to files stored on the same server. I also used a library called winston-daily-rotate-file to store the logs for each day in separate files with the date in the name, like this: logs/application-%DATE%.log.

I also decided to serve all traffic over HTTPS instead of HTTP so that any request and response data would be encrypted. Since I already had NGINX running, I used Let's Encrypt's certbot tool to generate an SSL cert and private key and automatically configure NGINX to redirect any HTTP requests to HTTPS. The certbot tool made the process a lot simpler by managing some of the boilerplate configuration for me.

I own a few domains related to my name, so I picked one of them named jonfriskics.net and configured the cert to work there.  This meant that the only two routes that the Node application would accept were GET https://jonfriskics.net/milton and POST https://jonfriskics.net/api/save, and any other requests would return a static 404.html page.

Finally, I installed the helmet library. I set script-src to 'self' and 'jonfriskics.net'outside of the default Content-Security-Policy directives, so that the only JavaScript that would be allowed to run on the server could come from there. Then, I set a font-src directive of 'cdnjs.cloudflare.com' so that the fonts I loaded in my front end would render correctly.

The front end: EJS templates and a personal iOS application

I created two different front ends:

  1. A single web page for end-users loaded from the /milton route. It would display a table of the power, internet, and cell signal status with the timestamp in reverse chronological order.

  2. A simple personal iOS application for admin users (me) to send data about the three states to /api/save. This was not submitted to the Apple App Store, but instead was installed directly on my phone from Xcode.

I chose a web page for end-users because the web is everywhere and everyone knows how to access it. Anyone wanting to check in on me throughout the storm could simply go to https://jonfriskics.net/milton or share that URL with friends and family.

That /milton route returns a single EJS template that contains HTML to display a table. On the server, the route queries the SQLite database and returns all data as an object, which is then passed to the EJS template. I could have sorted the data on the server, but I wanted to try sorting it client-side in the EJS template (thanks for the help, ChatGPT!).

The simple webpage frontend that displayed data from /milton

I also used the Tailwind.css library to style the table. Tailwind takes a declarative approach to using CSS by defining a ton of utility-classes for setting rules for things like layout, text alignment, and colors. Instead of writing all of those rules yourself, you start with Tailwind’s  predefined classes added inline in HTML tags. If you're used to writing CSS, it feels really different at first, but it helped me get a visually pleasing table layout very quickly.

I chose an iOS application for admin-users (me) for three reasons:

  1. It was more likely that I would keep cell signal than power and Wi-Fi, so it would be easier to send periodic updates from my phone than my laptop.

  2. I had experience building iOS apps and already pay Apple's yearly fee for a developer certificate that allows me to install personally built apps on my phone.

  3. I hadn't had much experience working with the newer SwiftUI frameworks for building apps, and this was a simple enough UI that I could figure it out.


The UI for the data entry screen in ContentView.swift is a NavigationView that contains a VStack, and that VStack contains 3 toggle switches, a button that calls the https://jonfriskics.net/api/save endpoint, and a button that loads the /milton HTML page inside a WebView in the application.

The lone iOS app screen used to set the state of cellular signal, internet access, and power availability

When the Save button is pressed, the application shows an Alert view that shows the response from the server to that request—hopefully Data saved successfully. If it didn't save correctly, then an error message shows instead.

How it all worked out

Thankfully, where I live was spared from major damage. I started making updates at 1:34 p.m. on October 9, 2024 (Unix time 1728437699), before the winds started and continued every hour or two until I went to sleep around 12:16 a.m. on October 10, 2024 (Unix time 1728533806). During that time, I maintained power, internet, and cell signal, so the toggles were set to true for these updates.

The strongest parts of the storm passed over me in the middle of the night from around 3 a.m. – 6 a.m., but I slept through it and woke up around 8 a.m. to find that our home internet provider was out. I grabbed my phone, turned off my Wi-Fi connection and confirmed that I still had a cell signal, so I made an update at 8:14 a.m. that power was on, cell signal was up, but Wi-Fi was out. I made a few more updates that morning, and through my 12:29 p.m. update, Wi-Fi was still out. At 1:28 p.m., I noticed that Wi-Fi was back, so I made an update to reflect that.

sqlite console showing the database schema and data entered in the database during the storm - including the times stored as Unix timestamp values

By 2:30 p.m., the storm was out to sea in the Atlantic Ocean, so I made my final update. A few hours later, I updated my NGINX configuration to redirect all requests except for /api/save to a new end.html that mentioned that "the storm had passed, I was fine, and thanks for following along."

Lessons learned and next steps

I was lucky and thankful that this time I wasn't personally impacted as badly as neighbors who lost power in Orlando or neighbors further across the state who lost a lot more than that.

I hope you never have to experience natural disaster events like hurricanes or extreme weather, but once you're safe, keep an open mind for any opportunity to build something useful like this for yourself, because building solutions for real-world problems is one of the best ways to practice and validate your skills. Sure, I could have relied on existing social networks or messaging applications, but by thinking through and building a custom solution I reinforced skills I had and picked up a few new ones along the way.

Learn more about Node.js and Express

If you want to learn how to build web applications with Node.js, Pluralsight's recently updated Node.js path covers many of the techniques I mentioned here like working with Express and EJS templates, thinking about Node.js application security, and deploying Node.js applications to a VPS and running with PM2.

Jon Friskics

Jon F.

Jon is an author, developer, and Pluralsight team member via Code School. Lately, he's been working on content and products that help authors create content efficiently. Prior to that, he worked for several years on interactive learning at Code School, and later helped bring that to Pluralsight as Interactive Content.

More about this author