One month ago, we started working on a new product: Instant 2FA, the easiest way to add two-factor authentication to any site in less than an hour.
Currently, whether you roll your own two-factor with Google Authenticator or use an API service like Authy, a standard integration often takes weeks.
With Instant 2FA, we’re building a solution that reduces the entire integration to 3 API calls and provides everything out of the box: Google Authenticator, SMS, and Yubikey support; remember this computer; rate-limiting and alerting; and with our first few customers, we’ve done the integration in a development environment in less than an hour.
This blog post outlines our thinking behind choosing Ember (a technology none of us had ever used before) over React (which we’ve been running for 2.5 years in production at Clef) and our reflections on the decision after one month of work.
Our experience
Every new project requires a million technology decisions. Luckily, 4 years of company experience building Clef, a two-factor product used by nearly a million websites, made most of our decisions obvious. We knew our backend services would be built with Python, SQL, and Flask, leveraging all the tooling and libraries we’d spent years working on.
The frontend, however, was a different story: we’ve been using React and flux (powered by reflux) in production for two and a half years, but as we began considering how to build Instant 2FA, we wondered whether using a more fully featured framework like Ember or Angular would be a better decision.
Evaluating ember
When we started considering Ember over React, there were two main hypotheses that drove our curiosity:
- Convention over configuration. Since Ember handles many things out of the box that React requires composition of other libraries to do, we would be able to accomplish more, faster, and without suffering from decision fatigue.
- Progressive enhancement & surviving the framework hype cycle. Since Ember has a very strong community that’s very committed to making Ember the best way to build modern web applications, we would be able to use the latest and greatest technologies, even if our use came a little later on the adoption curve (perhaps on the plateau of productivity).
As we dug into each of these hypotheses, we found compelling evidence that ultimately led us to choosing Ember for both our applications.
Convention over configuration
One of the best things about React is that, since it’s only a view layer, it can easily be added to a pre-existing JavaScript application. Three years ago, when the frontend for Clef was built on our own object oriented JavaScript framework and we started exploring whether React could help us, this was one of the best parts. Rather than swap in a new “framework,” we gradually replaced different components with React components — adding additional logic layers on top of React (like redux to handle data flow) only as necessary.
Over time, this gradual composition of libraries cemented into our own little incarnation of the React ecosystem. Different libraries duplicated each other, but things worked. We used:
- coffeescript
- React for the view
- reflux for data flow
- react-router 2 for routing
- jQuery.ajax for network requests
- underscore for utilities
- browserify for module building
- gulp for tasks
- SASS for styles
- no tests 😬
A few months ago, we did a little side project where we had to create a client application from scratch. We went with React for the view layer, but given the dramatic evolution of the surrounding ecosystem over the last few years, we ended up with a very different composition:
- ES6
- React for the view
- redux and redux-saga for data flow
- react-router 3 for routing
- fetch for network requests
- lodash for utilities
- webpack for module building
- post-css& css-modules for styles
- karma, mocha, sinon and chai for tests
It was fun to get to pick and choose, but every step of the way we doubted our choices: were we making the wrong decision with a given library choice? We were “configuring” our own framework from the tools available in the React ecosystem, but had major concerns about whether our configuration would help or hurt us.
Entrusting high-level architectural technology decisions to engineers who have limited experience in a domain often leads to bikeshedding and bad decisions — and this showed in the side project we did. We are a team of 4 engineers building Clef, but since we regularly maintain code across 5 platforms (iOS, Android, backend Python, client JavaScript, and our WordPress plugin), only two of our engineers have really worked on our React application. By the end of working on the side project as a team, we’d saddled ourselves with a set of tools which often proved unstable and got in the way of getting our work done.
With Instant 2FA, every engineer would be working in JavaScript for half their time, so this meant more than half of the people writing code would be relatively inexperienced JavaScript engineers. This makeup — senior overall, but relatively inexperienced with front end engineering — made the issue of convention over configuration feel particularly relevant.
As we started working on Instant 2FA, the thought of Ember making all these decisions for us — enforcing a convention over configuration — was very alluring. Some of the decisions we were most excited about not making were:
Tests
Ember ships with testing built in and tests are auto-generated when new units (models, routes, services, serializers, etc) are created. For the last React application we built, we’d spent days setting up test harnesses, acceptance tests, and all of the other tools necessary to have a fast, easy-to-use test suite, and we still were constantly frustrated with our setup. Ember built acceptance and unit testing into the framework, which we hoped would make it easier for our code to be tested at every level.
Builds & deployment
Ember is powered by ember-cli
, which handles every step of your build: transpiling JavaScript, building modules, productionizing code, and even deploying (with ember-cli-deploy
). This toolchain comes with sane defaults out of the box (ES6 and modules, for instance), but the community built around this standard is the most powerful part.
In our previous setups, every time we needed to change our build to implement a specific goal, we needed to write custom code. One great example of this is uploading sourcemaps to Sentry when our code is deployed. In our React apps, we did this in a shell script after our webpack build was done.
With ember-cli
, almost any new step we need to add is available as an Ember addon (like ember-cli-deploy-sentry). And, because of the way Ember enforces conventions, these addons almost always work out of the box. As a result our build and deploy logic has transformed from a mess of custom JavaScript and shell script logic to a clean composition of different community maintained modules.
Data flow & modeling
The default implementation of data flow and persistence in Ember relies on ember-data
, which is maintained by the core team (along with ember-cli
and Ember core). With our most recent React experience, when we used redux
with redux-saga
, while we felt at the cutting edge, we always seemed moments away from flushing days down the drain trying to figure out how to do something. When evaluating Ember, We were excited that with ember-data
there were established patterns for how to do things — even if some of those patterns felt a little outdated.
Server side rendering.
Ember FastBoot enables server side rendering in any Ember app with one command. Having explored server side rendering in our previous React apps, we knew that for every configuration of libraries around React there was an equally complicated server side rendering setup. Server side rendering wasn’t something we needed out of the box, and we still haven’t enabled it, but knowing that it’s a cli command away, rather than a major project away, is a weight off our shoulders.