by Alexandre, on April 24, 2023  (last updated on September 30, 2023)

The web is moving fast, and things sure have changed since our last tech stack post 2 years ago.

Back then, most of our frontends were static-generated sites, with some pages like the landing page being entirely generated at build times, and other more dynamic pages hydrating on the client with JavaScript, eventually querying our GraphQL and REST backends.

TL;DR: This year, we are rendering on the edge with SvelteKit (+TypeScript) hosted on Fly.io.

Let’s first talk about what changed.

What changed

GraphQL

We love GraphQL, with all its associated tooling, bells and whistle. It helps us keep our backend code clean, with type-safety back-propagated down to the client.

But this tooling comes at a cost. It takes some significant effort to set up a GraphQL server, and to set up the developer workflow. This tooling also needs to be maintained, and kept up-to-date with the versions of the various client libraries.

While the advantages of GraphQL are clear, we found that the cost of maintaining it was seldom worth it for our use cases. We’d still use and recommend GraphQL for large projects with a lot of developers and different client types (web, mobile, desktop, etc…), but it is not our default choice any more.

Google Cloud Run

We liked Cloud Run because it allows us to deploy any container, and make it scale from zero to 1000s of instances without having to worry about the underlying infrastructure.

We encountered some pain points with Cloud Run:

  • Domain names cannot be used in all regions, and in the regions where they can be used, they can only be used for the main (production) app
  • Custom deployments, for instance to handle staging environments with different settings, are cumbersome to set-up
  • Cold starts are slow and hard to predict. Sometimes upwards of 10 seconds can be required when our code includes some heavy 3rd-party libraries.
  • When you deploy an app, it is associated with a single region. If you want to deploy to another region, you need to create a new app (with separate CI conf, separate scaling constraints, and so on…).
  • Google wants you to use their secrets manager, but we didn’t want vendor lock-in, so we didn’t use it, making secrets management unnecessarily laborious.

Netlify

While Google Cloud Run allows you to deploy any container, including NGINX containers with embed Static Websites, it was too burdensome to set-up and use on every project.

That’s where Netlify comes in. You simply give it a folder, and it will serve it. Easy to get started. Then, when you are getting serious, it has 1-click integration with GitHub/GitLab and the likes, and you get to production-grade deployments easily.

We cannot understate how easy it is to get started with Netlify. It is a great product, and it offers a refreshing experience that Google Cloud Run does not provide. It is for this reason that we hosted most of our frontends on Netlify. We had considered similar solutions like Vercel and Cloudflare Pages, but at the time, Netlify was the one that achieved the right balance between price and features for us.

By using Netlify we also get some nice additional features without additional cost:

  • Proxied calls to your backend (complying with CORS and cookie policies even across domains)
  • Handling of redirects
  • A global Content Delivery Network (CDN) for your static assets

Yet, with all this good, there are some places where Netlify fell short for us:

  • The proxying feature that we ended up using a lot turned out to be quite unreliable, sometimes straight up failing with 5XX error responses without even hitting our backend or logging anything. It also times out after 26 seconds, and there is no way to increase this limit. All this meant we did not want to use the proxying feature for our production environments. But as our staging environments were using Cloud-Run-provided domains, we had to use it there, making our staging and production environments diverge slightly in their setup.
  • The CDN is not as good as Cloudflare’s. From our experience it is not as fast, and it does not have as many features. So we used CloudFlare instead, downgrading our Netlify account to the free tier.

Static sites

Our frontend technologies are also evolving. On Netlify, we used SvelteKit with Server-Side Generation (SSG) to generate static sites that Netlify then served globally.

Static sites are fast (you only serve static files), secure (you don’t run any server), and easy to deploy (static hosting has existed since the Web-1 era). They are also cheap to host, and they scale well. SvelteKit makes building such sites very easy, and allows us not to compromise on features and dynamic content. We still use and recommend static sites for blogs and simple websites.

However, as time went on, we found that we would benefit from a more dynamic approach. Some features we were missing with static sites:

  • Access control: We wanted to be able to restrict access to certain pages to some users (through authentication and whatnot).
  • Forms: We wanted to be able to integrate some forms on our website, as well as handle/store the data that comes with them. Netlify can handle this, but we didn’t want to rely on a third-party service for this.
  • Localization: We wanted to serve our website in multiple languages. While a static site can host multiple languages on different domains, we wanted to be able to rely on browser standards like the Accept-Language header to handle the language selection.
  • No-JS: To serve dynamic content on a static sites, you need JavaScript. However, if your users don’t have JavaScript (and this happens more often than you think), then you are out of luck.

Our 2023 stack

Software: Server-Side rendering with SvelteKit

We are still using SvelteKit, and we love it, but we are now using it with server-side rendering (SSR) over static site generation (SSG, see the Static sites section for more details) in projects where it makes sense.

Server-side rendering using SvelteKit renders the page on the server, and then sends the HTML to the client. The client can then take over and render the page with JavaScript, but the page is still usable without JavaScript. This is part of the progressive enhancement philosophy, and SvelteKit brings a lot of other features that make progressive enhancement easy.

Wait… That’s just PHP with extra steps!?

Yes. We have gone full circle.

Servers: Fly.io

Last year we tried Fly.io, a service that basically allows you to deploy any container you provide. We’ve been very happy with it, and this year we are letting it slowly take over both our Cloud Run and Kubernetes workloads.

It has a nice network of server nodes. You decide where you deploy your app, and when a client makes a request, the closest node from the Fly network will proxy the request to your closest running app, wait for the response, and then send it back to the client.

September 2023 update: We have since moved this website from Fly.io to Cloudflare, so the following paragraph and drawing use hypothetical regions and have been rewritten to reflect a hypothetical scenario.

Hypothetically, the Fly edge node that would have handled your request could be the one in Paris, France. It would then proxy your request to our app in Frankfurt, Germany, that would then render this HTML page and send it back to you.

Edge(Paris, France)App Container(Frankfurt, Germany)Wireguard TunnelYour InternetGET /blog/posts/our-tech-stack-2023Forwards RequestFly.ioHTTP/2 200our-tech-stack-2023.htmlForwards ResponseRenders HTML pageHydrates JavaScript (Optional)How this page you are reading could have been rendered by Fly

We are very happy with Fly.io. It solves most of the problems we had with Google Cloud Run, all while being cheaper. It misses some features that we had with Google Cloud Run, but we care more about the features we have that work very well than the ones we don’t have that didn’t even work well in the first place.

Databases: Supabase, PlanetScale, Mongo, and Prisma

It’s 2023 and there are a lot of great database options out there. We are using a mix of them, depending on the project:

Supabase is great for simple to medium-complexity projects. It is a better alternative to Firebase, runs elegantly on top of a PostgreSQL database, handles most of the boilerplate like user accounts, authentication, email/phone verification, or file storage out of the box, and provides a nice set of client libraries to use on both your clients and your servers.

When we need our Database to be globally distributed, we use PlanetScale. It is a MySQL-compatible database that trades some referential integrity guarantees for global distribution. It offers a nice DX, integrates well within Git flows, and feels very safe to iterate with.

When we need the level of global distribution that PlanetScale provides, while staying simple with our data model, we like to stick with MongoDB. Specifically, we use MongoDB Atlas, which hosts MongoDB databases in a distributed fashion. Mongo is very well suited for MVPs and small projects, and reduces the maintenance burden as long as your data model is simple.

With all 3 databases, when we have to query data server-side, we found that Prisma very easily brings type-safety to our database queries. It improves the developer experience by a lot, and it also makes it easier to refactor our database schemas. We highly recommend it for use with any data model that has more than half a dozen tables.

Dev containers

See that Python package you just upgraded? Well, with your code importing it, it has full-disk access to your computer, so if it were malicious, it could do some pretty bad things: install a virus, a botnet, a RAT, or even steal your work/login credentials/crypto/anything stored in your computer.

But it would never do that, because of course you read all the code changes from that package upgrade. Right? :)

This is called a supply chain attack. They can happen in package upgrades, in Git clones, in software installs.

Dev containers are a VSCode feature that help remediate this problem by allowing you to do all your development work in a sandboxed environment like a container. This way, anything malicious in your development environment will only have access to the container’s filesystem and OS.

Dev containers provide additional benefits for free:

  • they force you to “document” the set-up of your development environment in the set-up script
  • they make context switching easier. If a given repo needs a specific Ubuntu package and a specific Node.js runtime version, the dev container can provide it without you having to install it system-wide on your computer.
  • on macOS and Windows, your development work is in a Linux container, which in turn makes setting up the dev container environment in question easier as you don’t have to cater for different OSes
  • local clones of your repo become ephemeral by default, the repository version being the source of truth
  • need to review a PR? Just open a new dev container with the PR branch checked out, and you’re good to go. No need to stash your local changes, and no more conflict woes.

Try it on our prgm-dev/sveltekit-progress-bar repo:
Open in Dev Containers

If you are on macOS, I recommend using Colima (free) or OrbStack (free while in beta) as the container runtime solution. They use less resources than Docker Desktop, and I find them simpler to use.

Looking ahead

We have been very impressed by the progress of the Web ecosystem in the last few years. Project Fugu is progressing rapidly, and soon it might be possible to build Web-apps that are as powerful as native apps on both desktop and mobile platforms.

The Web is permissionless, open, secure-by-default, battle-tested, durable (see the Lindy Effect), only loads required resources (you don’t need to download a 300 Mb app to open Facebook), and is the platform that currently achieves cross-platform compatibility the best.

We have geared our stack toward the Web because we think it is becoming superior, and through the rapid progress of the platform, it will allow us to build a broader range of powerful applications than ever before.

We are very excited about this future ahead of us, and we hope to have shared some of that excitement with you.