Web releases

The latest and greatest on the web

State of the web

09 May 2025

With this post, I want to highlight some things that we in the web team take for granted — and hopefully spark some inspiration for other teams. Hopefully, you’ll get something useful out of reading this, and maybe even share back things your team does that we could learn from.

The Proxy

First off, let’s talk about the Web proxy. This server is pretty simple in itself, but it massively shortens our feedback loop and improves the developer experience (DX) significantly.

For example — let’s say you want to make confetti rain down on the login page of our web app 🎉.

You just tweak some code, open a PR, and within seconds we get a link. This link can be used for sandbox, KIAB, and even production. So if you want to see how a bit of confetti would look in production, you can simply go here: https://accounts.kivra.com

This can be used for testing, experiments, or even A/B testing. And with this proxy can we deploy/rollback releases within seconds.

Side note: the proxy also type checks the code, runs static analysis, formats the code, and runs our unit tests — in about 1 second total ⚡. It’s incredibly satisfying, and something we constantly work to maintain, not only for the proxy but in other projects as well.

Side note 2: If you visit https://inbox.kivra.com/__proxy_dashboard and log in with these credentials, you can see some internal state information about the proxy. This is something that is really useful from time to time and something I want to see if we can use in other services as well.

Mono repo

Speaking of builds — lately we’ve been working to gather many of our web-related projects (~30 of them) into a single repository 🗂️. We still have a way to go, but so far it’s been very promising. It has greatly improved the developer experience.

The only real downside is, of course, that the CI builds get a lot more complex.

In the image below can you see the current workflow setup and it will only grow.

The simplest case is when a developer makes changes in one of the web apps. In that case, only the tests for that specific app need to be run — just like it would be in its own repo.

However, if a developer makes changes in the GraphQL schema for the BFF, we need to rerun static linting and type checking in the projects that call the BFF to ensure that the GraphQL types haven’t introduced any linting/type errors. This can happen even if we don’t introduce breaking changes to the schema.

Then we have the case where a developer changes a shared code library — for example, a GUI component or a utility function. Now we have to figure out where the changed code is used and whether it actually results in changes in the built artifact. We do this by analyzing and bundling all the different web apps, using tree shaking to remove unused code. What’s left is an artifact that can tell us if the code change actually affects the production build.

We then take a hash of this artifact and compare it to previous builds. If a build results in the same hash and all tests have passed, we know we don’t need to rerun the tests again. If we don’t have a successful test run for that artifact, we need to run the tests before merging the code.

You might wonder: why don’t we just rerun all tests? That’s because we have thousands of tests — and many of them use KIAB, making them quite slow 🐢 (don’t get me wrong, I love KIAB!). Running all tests would be expensive and time-inefficient. That said, the system isn’t perfect and we’re continuously working to improve it.

On top of that, the monorepo allows us to easily spin up multiple services at once. We’re working on a script that starts a tmux session with selected services — we’re not quite there yet, but getting close!

Test harness

One service that’s easy to overlook is the Test harness. This service lets you quickly create users, content, receipts, etc. in KIAB.

If you have KIAB (with test harness) running on your machine, you can visit http://localhost:8004/docs to get Swagger documentation on what’s available. Or go here to get a simple GUI (using these login credentials).

However, the GUI is quite limited — so for now, I recommend either using kiab-mac-admin or Bruno with this collection.

Complex types

I have two core principles I strive for:

1️⃣ It should be hard to make mistakes

2️⃣ It should be fun to write code

A key part of that is our static types. Here’s an example of how we fetch data in our web applications:

import { useData } from "@inbox/data-layer/react";

function MyUiComponent() {
  const contentList = useData(s => s.contentList());

  return (
    <ul>
      {contentList.map((content) => <li key={content.key}>{content.subject}</li>)}
    </ul>
  );
}

The code above is fully statically typed. For example, if you try to use content.subjuct (note the typo), the build will fail — and your editor (with LSP) will even suggest that you might have misspelled it. We get this for free thanks to the BFF. All types are generated from the BFF, and our UI components stay simple. It is really a DX boost and you can read more about it here if you are instrested: https://github.com/kivra/web_mono/blob/main/web/inbox/src/%40data-layer/readme.md

Another example is our copy/text handling. If a copy in Lokalise includes a dynamic string, e.g. You have {count} unread letters, we automatically generate types for this string and force the developer to provide a number. Furthermore, if different languages have different signatures — for example, if Finnish requires two dynamic parts while Swedish only one — the build will fail to reduce the risk of bugs sneaking into production 🐛.

We do something similar for config values, which are also statically analyzed so we know in advance whether a config value is a boolean or a string. Sometimes, we even know which specific strings are valid values (Literal types).

Of course, these are just a few examples. TypeScript is a very powerful type system (you can even run DOOM using just types 😄). This is a big reason why it’s hard to make mistakes and fun to write web code at Kivra.

Toybox

A few years ago we realized we needed a custom system to develop and present our UI components — so we built Toybox. Here you can set up tests, present components, and experiment with them in a really simple way.

What are you doing that’s great?

In this post, I’ve just scratched the surface. I haven’t even touched on the BFF or how we can easily add or remove new content types in the inbox content list. Hopefully, I’ll get back to that in an upcoming post. And I hope I’ve managed to give you some inspiration with this post!

I’d love to hear about what your team does that works well — and what you’d like to share with others. 🙌