How to ship a highly dynamic webapp as a static website

Imagining a leaner way

Théophile Villard
Dec 11, 2018 · 9 min read

In early April I was given three months to ship a rough prototype of a crypto wallet for companies. Building decentralized web applications (dApp) — or web3 apps — was still a new domain, and could get quite tricky because of the paradigm shift introduced by blockchains.

I needed to focus as much as possible on these new challenges to build this complex webapp, and scrap as many mundane web development tasks as I could. From a security standpoint, given the stakes (cryptocurrencies) the attack surface had to be reduced. Finally, fast iteration was a must.

Being lean starts with removing things. I decided to challenge every piece of the standard architecture:

  • What if I could use a more powerful language allowing for more expressivity and a better interactive development experience?
  • What if I could do without a backend?
  • What if I could do without a server?
  • What if I could reduce the release process to under 30 seconds?
  • What if I could pay for no more than what I’m really using?

My answer: the Multis tech stack.

Here’s a quick tour to give you an idea of how we’re trying to get lean.

Multis is a static website

Yep, you heard correctly.

Is it 1999 all over again?? Not so fast, web1.0 is not back yet!

Multis is just a folder containing an html file and the usual static assets (js, css, images and the json files under contracts)

Image for post
Image for post
the folder

Simple right?

We then make app.multis.co point to that html. It’s simple, it’s static: all end users get the same version of the html, hence statically served.

Want to hear more? Good.

Deploying a folder

Well we could just put the folder in Dropbox and make app.multis.co point to the public url of that folder thanks to a service like Duet.to — That would work, but feels a bit amateur. What do we do instead? Check out the extract of our Cloudflare DNS records:

Image for post
Image for post
app.multis.co

Where do we get these IP address from? Firebase.

Image for post
Image for post

That’s the first important point: we’re using Firebase Hosting.

Image for post
Image for post

Firebase Hosting is similar to services like GitHub Pages, Netlify or Surge. It’s basically like our Dropbox with some added benefits:

  • everything served over https (allowing Full+Strict SSL on Cloudflare) “so content is always delivered securely”
Image for post
Image for post
Firebase Hosting proxied thru Cloudflare
  • “Each file that you upload is cached on SSDs at CDN edges around the world. No matter where your users are, the content is delivered fast.”
  • Deploying is as simple as doing firebase deploy --only hosting
  • “Firebase Hosting provides full versioning and release management with one-click rollbacks.”

The last point is actually a lie: it takes TWO steps to rollback!

Image for post
Image for post
new versions immediately available worldwide!

Html, css, and images don’t change often so rolling back is about changing the JavaScript file.

I hear you. You’re impatient to know how this js file representing the whole app code is written. Brilliant question! Let’s talk about it.

Writing code

Let’s get this clear: I’m definitely not writing this js file directly — I told you it’s not 1999 again 🤣

Nothing too fancy here, I’m writing code in different files that’s then compiled into one file. You know the drill.

Image for post
Image for post

Notice the .cljs ?
These are ClojureScript files. You might have heard of this language but here’s an elegant definition:

ClojureScript is a modern, functional & immutable data-oriented language with a great standard library that compiles down to self-contained & compact JavaScript bundles. Based on Clojure, it brings Lisp’s elegance and meta-programming to the JavaScript ecosystem.

Why would anyone want to choose this language?

ClojureScript combines the reach of the JavaScript platform, the flexibility and interactive development of Clojure, and the whole-program optimization of Google Closure to provide the most powerful language for programming the web.

This would warrant another article, but writing ClojureScript abstracting React makes developing dynamic web applications truly enjoyable. And by truly I mean really f*cking awesome.

Of all these files, only one function gets exported to the global window object:

Image for post
Image for post
notice the ^:export — that’s metadata in Clojure

The humble init function triggers the “data flow” with rf/dispatch-sync (see re-frame, think Redux on steroids) and the mount! function is mounting the main view component on the #app DOM element (see Reagent, think React on steroids).

What’s next?

We just need to call this init function in the html to trigger the rendering and the data flow:

Image for post
Image for post

That body part is not extremely complex 😉

The shrewd readers might think the tool used to build that final JavaScript file is Webpack. But they’re wrong.

Compiling ClojureScript

There’s a tool in ClojureScript land called shadow-cljs, and it basically handles dependencies (npm) and builds (webpack) with the distinct advantage of being concise — see for yourself its config file:

Image for post
Image for post
shadow-cljs.edn

That’s all you need.

The command shadow-cljs watch app will trigger an initial build followed by incremental compilations (taking less than 1 second) on every code change. You also get the interactive development experience unique to Lisp languages — think React hot reload but on mega steroids!

The command shadow-cljs release app will compile the production build (taking less than 30 seconds) using optimisations. You'll get a JavaScript file of the form core.723222C8EF.js thanks to :module-hash-names 10 — we'll touch on why this is important in a second.

For every release we use a custom bash script to insert that file name in the final folder’s html (see beginning of article):

Image for post
Image for post

THEN WE SHIP IT, cache gets invalidated automatically because of the hash in the file name (html files are not cached) — a new version of the webapp is live!

All done?

Not quite yet. You might be thinking “ok, they’re shipping a static website, They have the frontend, but where’s the backend??”

Glad you asked.

Backend as a service (BaaS)

Firebase comes with other services providing us with solutions for authentication, user-generated storage, and databases.

On the client side, their JavaScript library is polling these services through web-sockets allowing us to go realtime in the blink of an eye.

Their first version of their database “Realtime Database” was great but was missing some important features regarding scalability. They’ve now release their new database “FireStore” and it’s fair to say that the force is strong with this one 😉

Firebase plays almost too well with the single application state enforced by re-frame. It boils down to one big unified data flow:

  1. An event — like a click — triggers an event handler (dispatch)
  2. This handler triggers a side effect writing to the remote db (Firestore write effect)
  3. Changes in the remote db are pushed on the client and triggers a side effect writing to the local db (Firestore read effect)
  4. Views → Virtual DOM → DOM : displays materialized views of the local db (subscriptions)
  5. Go to 1.

After recovering from the initial dizziness of this infinite looping, you might still be wondering:

oh so you’re doing everything on the frontend. What happens when you need to use a service with a private key or if you want to run a compute-intensive task? You can’t just do that in the browser!

Indeed, in the first example that would expose my key for everyone (and as we know we can’t trust everyone) and in the second example that would put a lot of stress on the user’s browser: What if it’s a mobile browser? What if the tasks take some time to complete?

Hummmmm, you’re gonna like this 👇

Functions as a service (FaaS)

There’s a service called Firebase Cloud Functions (think AWS lambda) where you write and store functions that will be triggered upon certain events:

  • Any http request
  • Firebase authentication (ex: new signup)
  • Firebase database (ex: write under a specific path)

These functions live “in the backend” — their code is never exposed to the end user, you can make use of any private key or run compute-intensive tasks. The simplest use case for us is a tiny integration with Slack to receive a notification in a private channel when a user signs up:

Image for post
Image for post
make sure the hook posts in a private channel

Sweet huh? 😀

Cloud Functions can be much more complicated as well, for instance we have a big one for our “import wallet” feature.

By the way, you’re not hallucinating (yet): the snippet above is pure JavaScript. That’s the compelling feature of shadow-cljs in its ability to use JavaScript within ClojureScript (and vice versa) — in that particular case we create another build with {:target :node} instead of :browser !

Deploying these functions is also a one-liner:

firebase deploy --only functions

😉

Soooooooo, what’s the icing on the cake?

Image for post
Image for post

Connecting to the Ethereum blockchain

How can one feel complete without a connection to this always available and uncensored world computer?!

It’s got similarities with a BaaS:

  • individual wallets or contract accounts (user authentication)
  • contract state (database)
  • access from a JavaScript library on the client side

But also key differences:

  • decentralized (anyone can join participate and secure the network)
  • money protocol (value transfer as a native feature)

Funnily enough, there’s a growing list of companies in the blockchain industry using ClojureScript. The obvious reason? Immutability 🤓

Image for post
Image for post
thanks!

How does this Ethereum connection work? Here’s an example:

Image for post
Image for post

Providing an instance of web3 — boils down to connecting it to an Ethereum provider (could be one injected by Metamask, one running on your machine or a remote one like the nodes provided by Infura).

With re-frame-web3-fx library you watch the balance of an address so that ::coinbase-balance is always up to date in your global state (we then merge that data source into the unified data flow — see BaaS).

Maybe you’re feeling a bit overwhelmed, let’s try to get an overall picture:

10000-foot view of Multis’ architecture

Monies

Not that I’m not willing to pay for services, but it feels more elegant to pay only for the service you’re using — and the freemium model works well to get you started and then grow (and pay) with the tools you first used.

As of today, we’re under the free tier for Firebase and Cloudflare. Meaning that our only expense so far has been for buying the different domain names. Not too ashamed of that 😋

What about you?

That’s it for now, hope you got intrigued by this static website architecture.

If that sounds too exciting for you, please hit me up on Twitter, we’re looking to hire Engineer Numero Uno for Multis.

💪 👩‍💻 👨‍💻 💪 #remoteok

Bisous

Multis

Stories from the humans who buidl Multis

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store