My tool-kit for tiny responsive web apps

Gareth Cronin
8 min readJun 25, 2021

In my first story in this series, I explained that I spend about a day a week solving problems that interest me with tiny software applications. I’ve been assembling a tool-kit to quickly build against architectural patterns that I find keep coming up in tiny apps. The technology I choose has to solve these problems:

  • I only want to pay for what I use (scale-to-zero)
  • I don’t have a lot of time available for learning or building (no steep learning curves without substantial time-savings)
  • I don’t have time for maintenance activities (no patching servers, automated scale-up)
  • I’m not a good UI designer or front end engineer (design systems are great)

I’ll talk about mobile apps and app store distribution in a later post, but I want to start with the pattern that I find comes up most often — the responsive web app.

TL;DR

  • React, Material-UI, Create React App, Jest
  • Google Firebase
  • AWS: S3, CloudFront, Route 53, ACM
  • Github Actions

The business problem

I’m a fan of Calm, the mindfulness app. It’s still my go-to for guided meditations. Much of time I find I don’t need a guided meditation, but I still want background sound and a periodic bell to prompt me to check that my thoughts haven’t wandered. Calm has some unguided meditations with a bell, but only with a few fixed lengths and intervals. It also has some “soundscapes” that have background sounds along with intermittent passing sounds (e.g. in the “Central Park” soundscape people chat as they walk by, a plane flies over). I decided that what I really wanted was a combination of all of these plus more:

  • A timed, unguided meditation
  • Ability to continue when the timer has finished if wanted
  • Ability to pause and resume a session
  • A calming, ambient, background sound loop
  • A periodic bell to check I’m still in the moment
  • Random intermittent, but relevant, sounds
  • An automatic diary of sessions

I want all of this in a web app that will run well on a mobile device, since I usually use Airpods with my Samsung phone for meditation sessions.

There’s a working version of “Mindful Mix” running here. It’s not beautiful, but it works!

The front end

Despite my limited front-end engineering skills, I’m pretty comfortable with the React/JSX model, particularly with React Hooks. After experiencing the pain of promulgating state all over the place, I have built a CRUD app using Redux before, but I found it didn’t really help me that much. I did spot SimpleR State somewhere recently, and next time I need some more sophisticated state management, I’m planning to give that a go. In this post though, given the limited role state plays, I’m exercising the YAGNI principle and using State Hooks in my React components.

I’ve used Bootstrap bindings for React in the past, but these days I tend to use Google’s Material Design. I find it’s the most complete and consistent set of widgets for the dilettante UI designer. It’s a bit more constraining that using Bootstrap, but that is a good thing. I’ve always found that working within a set of constraints forces me to think harder about how to solve problems with what I’ve got at hand, and avoids wasting time fiddling around and experimenting with different approaches. There’s actually some solid backing to this theory in a classic HBR article called “Breakthrough Thinking from Inside the Box”. The React bindings for Material Design are called Material UI, and importantly there is a wealth of help available in StackOverflow questions and answers for the project.

Material UI has an excellent fluid layout model (I mostly use the basic grid) that makes it straightforward to create responsive layouts. I tend to leave my Chrome dev tools in iPhone 6/7/8 emulator mode while I’m working so I can spot any problems I accidentally introduce as I go.

Ever since discovering Create React App a couple of years ago, that is all I tend to use to scaffold a new app. Messing around in the bowels of webpack and other bundlers and transpilers is no fun. The react-scripts project magically takes that all away. The Material UI project has a handy template for scaffolding with the boilerplate all in place and a minimal app ready to run.

For this specific project, there are some special requirements. I might go into the details of those in a later article, but I went with Howler for the audio, a hook-friendly approach to running a timer, and a clever little library for keeping a mobile device awake while the mindfulness session is going.

The middle

What middle?

The lovely thing about building tiny SaaS apps is I’m not thinking about how a team might work on this, I’m not going to build a public API for it, so all I really need is a way for an authenticated user to write the diary entries to a data store in the cloud somewhere and read them back again.

“But, what about server-side logic?”, I hear you say. This example project doesn’t need it, but I often do need it, and I will cover building APIs and running batch jobs in later posts.

The back end

When it comes to data storage and retrieval, I want to exercise scale-to-zero. Given I will be the only user until I get lucky (if I ever get lucky) there’s no way I want to be paying monthly charges. However, the database does need to be secured, which means I need to authenticate my users, and if ever there is a time suck, it’s messing around with authentication flows and authorisation logic. If I do find myself with users, I want to be confident it will scale up to the load without manual intervention.

Luckily for us, Google Firebase comes to the rescue. Firebase has two database options, and a beautiful piece of documentation to explain which one to choose. They are both basically document databases — JSON trees. For this project I’m using Realtime Database. Firebase provides integration with all the popular social media single-sign-on systems, email registration with password authentication, and the cherry on top: a full user interface with the authentication flow baked in for React! It also works in Mobile Safari and Mobile Chrome.

Because of the cunning way Firebase works, we can integrate the front end directly with the Firebase APIs. The user will authenticate via Firebase, which will leave a secure token in their browser, and Firebase rules will mean they can only read and write their own data. In my project, I read and write the diary entries for each user under a node with their Firebase-generated user ID as the key, so the access rules in Firebase (these get plugged into a web console) are really simple:

{
"rules": {
"diary": {
"$uid": {
".read": "auth != null && auth.uid == $uid",
".write": "auth != null && auth.uid == $uid"
}
}
}
}

Deployment and hosting

Three of my principles are important here: scale-to-zero, no learning curves, and no maintenance.

Learning curves are of course heavily dependent on what you’ve messed with in the past, but in my case I am most comfortable with AWS when it comes to cloud infrastructure. CloudFront CDN in front of S3 with DNS in Route53 provides a simple, maintenance-free, scale-to-zero solution for hosting static resources. For a tiny web app that really just means the “build” folder that the react-scripts process generates: bundled and chunked JavaScript files, an HTML wrapper, a little bit of other boilerplate and then whatever additional assets the app needs. For my mindfulness app, that includes some audio files.

AWS’s integration between their certificate manager and Route53 makes it easy to stand up new domains with http to https redirects for any domain or subdomain. I use a wildcard cert on *.apps.cronin.nz (one of my domains) to make it trivial to stand up new app ideas quickly.

For the deployment, the last thing I want is a continuous integration server sitting running somewhere (that wouldn’t be scale-to-zero or no maintenance) and that’s where Github Actions comes in. I host all my project source code in Github, and Actions provides a ready-to-go CI/CD system with no server required. The community builds extensions that can take care of whatever additional behaviour is required. In my case, that means actions for an NPM build, running the Jest tests, deploying to S3, and invalidating the Cloudfront cache. Actions integrates with Github secret management, so the AWS credentials live happily in there. That means a complete CI/CD pipeline that automatically deploys from a push to the main branch is just a few dozen lines of YAML in a directory at the root of my Git repository (.github/workflows):

name: Production build
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: NPM install
run: |
npm install
working-directory: web
- name: Jest tests
run: |
npm test
working-directory: web
- name: Production build
run: |
npm run build
working-directory: web
env:
CI: ""
- name: Deploy to S3
uses: jakejarvis/s3-sync-action@master
with:
args: --acl public-read --delete
env:
AWS_S3_BUCKET: ${{ secrets.AWS_BUCKET }}
DEST_DIR: apps/mm
SOURCE_DIR: web/build
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: us-west-2
- name: Invalidate Cloudfront cache
uses: muratiger/invalidate-cloudfront-and-wait-for-completion-action@master
env:
DISTRIBUTION_ID: ${{ secrets.AWS_DISTRO_ID }}
PATHS: '/*'
AWS_REGION: us-west-2
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Summary

Let’s revisit those principles and check I have it covered.

I only want to pay for what I use (scale-to-zero)

At the front end, the CloudFront CDN with S3 scales to zero(the cost of idle S3 storage is virtually zero). At the back end, so does Firebase.

I don’t have a lot of time available for learning or building (no steep learning curves without substantial time-savings)

I’ve used React with Material-UI for a while — nothing new here.

I don’t have time for maintenance activities (no patching servers, automated scale-up)

The solution is serverless, Github Actions deploys automatically when I push to the main branch, and scale-up is automatic.

I’m not a good UI designer or front end engineer (design systems are great)

Material-UI does all the hard work.

Bonza!

Next up is my tool-kit for tiny APIs.

--

--

Gareth Cronin

Technology leader in Auckland, New Zealand: start-up founder, father of two, maker of t-shirts and small software products