Hi! We get a lot of questions about what we use for Luma’s tech stack. I’ll share the major technologies we chose along with some of the reasons why we chose them.
And if you want to work with our tech stack, we are hiring. 😍
I started programming with React in 2015 and it felt like a revelation. It still does. I can’t imagine building with another view library today.
Next.js provides a great framework for code splitting, server rendering, and a ton of other small things. We’re really happy with where Next.js is today and the direction they are heading in.
Next.js also handles most of the Babel / Webpack config for us so that we haven’t had to spent time optimizing our bundle.
styled-jsx + SCSS
We started with CSS Modules but it was frustrating to have a different CSS file for every JS file. Whenever you wanted to move a component to a different file, you would need to figure out how to move the associated CSS file.
I looked at the various css-in-js options and chose styled-jsx because it seemed relatively well adopted and was made by the makers of Next.js.
I am not confident in this decision. styled-jsx has a hard time styling nested components and emotion seems like it’s getting more popular.
A Note on Design
We care tremendously about delivering a delightful experience. That means spending time iterating on the UX and actually working through edge cases.
We use some Bootstrap utility classes but most of our CSS and design is done in house. In order to deliver a warm emotional response, we’ve built our own components and design system.
We chose Typescript for the frontend and backend for a few advantages:
Developers code in one language
We share code between the frontend / backend
Koa web server
Express is the most popular Node.js web server but it hasn’t had a major release in 6 years which means it still has slightly awkward support for
Koa was made by the maker of Express and is built with
await as native primitives. Unfortunately Koa is not evolving or improving anymore. But it’s a minimalistic web server that gets the job done.
I have been surprised by the lack of progress in the Node.js web server space. Today most of my hope rests on Deno which will rewrite the way that Node.js handles HTTP requests.
I love Postgres. While we briefly considered MySQL since my co-founder used it at Uber, we settled on Postgres since it’s the gold standard in DBs.
We don’t use too many advanced Postgres features but we like JSONB support and have created a function to generate prefixed IDs.
Knex.js + Raw SQL
I’ve played with a lot of different JS ORMs. But none of them are at the maturity of ActiveRecord or SQLAlchemy. Once your project gets to a certain size the JS ORM starts hurting you more than helping you.
We use Knex.js as a query builder but we often resort to just writing Raw SQL. This has made our API routes obviously performant and has not slowed down development.
I think there is promise in Prisma but at this point it feels too magical and heavy for us to adopt it.
We try to avoid having too many services unless the services have very different resource needs or performance characteristics.
Here are the different perf characteristics of our services:
Web — serve HTML / JS / CSS, serve it fast
API — serve JSON in < 1s, longer responses should be an async job
Async Job System — we use a queuing + worker system to process jobs asynchronously
Screenshots — we use Puppeteer to generate screenshots, this can crash and take longer than a second so we don’t group it with the API
Realtime — we use Centrifugo as a service to manage websockets and send realtime updates to clients
We launched Luma on Vercel and Render. While I love both of those services and they allowed us to move insanely quickly, we ended up switching over our production infrastructure entirely to AWS.
We were attracted to AWS’s reliability and the ability to have all of our services in one place. Here is how we use AWS:
ECS + Fargate
Our services all use ECS to spin up / down containers. There was a steep learning curve when I had to understand Services, Tasks, Task Definitions, IAM, Security Group and a million other terms. But now it seems pretty easy.
Aurora for the DB
Aurora is amazing. It separates compute and storage so that we haven’t had to worry about scaling our database up. It also gives us peace of mind with point in time backups.
Github Actions for Deploys and Tests
We run a test suite with every Pull Request that must pass before the PR is merged into master.
For every commit on master, a Github Action updates our services. This means we might have 10+ deploys to production every day. Each deploy updates our services in a waterfall — DB is migrate, then the new API rolls out, then the web service.
Vercel for Preview Deploys
Vercel deploys each pull request to a preview branch. This makes it really easy to test frontend changes on production.
Sentry for Errors
Sentry feels like a React-level decision. Widely accepted and generally reliable.
Honeycomb for Instrumentation
We have set up a Koa middleware to wrap all of our API routes. This lets us see how long each route is taking or if we are seeing a spike in traffic.