← All engineering notes

Building the Operating System for Live Music

What "operating system for live music" means once you have to build it: the show as the unit of work, and the architecture that falls out of that.

Gotobeat Fest

Photo: @gianspeaking

Our mission is simple to say. Support emerging artists, help venues fill rooms, and take the repetitive admin out of putting on a show, so the people involved can spend their time on the music.

It is less simple to build. This post is about what that sentence turns into once you have to write it down as a system.

A show is the unit of work, and it is not one entity

The thing our software is about is a gig. A gig is not a row. It is a venue, a date, a set of ticket tiers, a promoter, one or more artists, a deal that splits the money, an inventory count that must never go negative, and the orders that hold that inventory.

Worse, a gig has a long and lumpy lifecycle. Months of planning, then an on-sale that lands in a few seconds, then a quiet period, then a night where a few hundred people scan a QR at a door within twenty minutes of each other, then a settlement weeks later.

  book ----------- announce -- on-sale ------------ door ------ settle
   |                   |          |                   |            |
 months             minutes    seconds             minutes       weeks
 of research        of spike   of spike            of spike      later

 almost no load     the two moments that decide whether the platform works

Almost every architectural decision we have made comes from that shape. The load is not steady. It arrives in two narrow windows, and both of them are the moment the promoter is watching.

Three apps, one set of packages

We run three separate web applications:

App Who it serves
the public site fans buying tickets, and the admin surface behind it
Artist Studio artists reading the deal we have offered them
MyLink the link pages artists put in their social bios

Different audiences, different auth, different deploy surfaces. They share one set of packages, and the sharing is enforced rather than encouraged:

  apps/gotobeat     apps/studio     apps/mylink
        |                |
        +--------+-------+
                 |
          packages/services   <- the only layer that may call the database
                 |
          packages/db         <- one module per table
                 |
          packages/lib        <- utilities, clients, the ElectroDB schema
                 |
          packages/types      <- types only

  ESLint zone: packages/** must never import from apps/**

That last line is a real rule in .eslintrc.cjs, not a convention. A shared package that reaches back into an app is a cycle, and cycles are how a shared layer quietly becomes three copies of itself. If a package needs app state, it takes it as a parameter.

We learned this the expensive way. Artist Studio began as a fork. A pinned deal total showed one number in the artist's PDF and a different number in the admin's, for months, because the fork was older than two bug fixes. Nobody noticed, because both numbers looked plausible. There is now one shared module and two thin render layers.

The slow work is not in the request

The second decision follows from the lifecycle. Most of what the platform does is slow, and none of it belongs in a web request.

  request path                       background path
  -------------                      ----------------
  read a page      milliseconds      crawl a gigography     minutes
  buy a ticket     one write         run a capacity model   minutes
  scan at a door   one CAS write     rebuild a snapshot     minutes
                                     reconcile inventory    scheduled

  the two never share a Lambda, and the request path never waits

So the architecture is event driven. A web Lambda does the small synchronous thing and hands the rest to a worker through an event. Long jobs run on their own functions with their own timeouts, their own concurrency limits and their own retries. A machine learning pipeline that takes four minutes cannot be allowed to hold a page open, and a page that fails cannot be allowed to lose the job.

This has a failure mode worth naming, because it caught us: work started inside a request and left unawaited does not run in the background on Lambda. The runtime freezes the container the moment the response is returned. The promise resolves eventually, or never, and nothing tells you. The fix is not a floating promise. It is an event invoke to a real worker.

Two languages, chosen per job

TypeScript on Node runs the product: three Remix apps, the API, the orchestration, the infrastructure as code. Python runs the data and the models: the capacity ensemble, the scrapers, the enrichment pipelines. Those ship as container Lambdas because they carry XGBoost, numpy and a bundled browser, which do not fit in a zip.

The boundary between them is an event and a JSON contract, not a shared library. Neither side imports the other. That means either can be replaced without touching the other, and it means the Python side can take four minutes without anyone's page hanging.

Why serverless, specifically

The honest reason is the load shape at the top of this post. We are a small team running infrastructure that is idle most of the week and then has to absorb an on-sale. Paying for capacity we use twice a week would be the wrong trade, and being the team that has to scale a cluster manually at 10am on a Friday would be worse.

The cost is real and worth stating. Cold starts. A 250MB unzipped bundle limit that shapes what you may import. Timeouts that become a design constraint rather than a safety net. Local development that only truly matches production once it is deployed. We pay all of that on purpose, because the alternative is paying attention during an on-sale, and attention is the thing a small team has least of.

What this means if you are joining

The mission is the easy part to agree with. The interesting part is that it turns into concrete constraints:

Every hard problem reduces to a question about a room. How many people will come, what will they pay, and what has to be true for the room to fill. That question is why we have a capacity model at all, and why it is a forecast rather than a dashboard.

The load arrives in two narrow windows, so correctness under concurrency is not an advanced topic here. It is the first week. An inventory count that must never go negative and a door scanner that must serialise concurrent writes are ordinary Tuesdays.

The data is small and precious rather than big and cheap. Tens of thousands of artists, hundreds of thousands of shows, and the ticket sales behind them. No Kubernetes, no Spark, no GPU farm. Judgement beats scale at this size, and the interesting work is in the joins, not the volume.

If that sounds like your kind of problem, our open roles are at Gotobeat Hiring.

All engineering notes
region eu-west-2read 6m