Skip to content

Introduction

About dApps

🤔 the term “dApp” is already quite ambiguous, and now we also have “headless dApps”? WTF is a headless dApp?

We should probably start from the begining.

Cardano uses the eUTxO model, an extended version of Bitcoin’s UTxO model that introduces the concept of on-chain validators, custom code that can be attached to UTxOs to enforce rules on how it can be consumed.

Notice that we didn’t use the word “smart contract”, this is because “validators” are just functions that assert that a transaction is valid. They are the absolute minimum code1 needed to make sure all parties in a transaction are playing by the rules. They don’t contain any business logic (no output, no side-effects, nothing).

As a mental exercise, lets take it to the extreme. You could build any Cardano dApp in the world using an on-chain validator that always returns “transaction is valid”. It would be a very insecure app since everything is allowed, but a user wouldn’t be able to tell the difference when executing a valid transaction2.

So where is all the business logic?

In Cardano dApps, the magic happens at the moment of building the transactions. Since transactions in the UTxO model are deterministic, they define the outcome of the interaction. They describe how things should look like after the transaction has been applied on-chain. This is quite different to what happens in blockchains that use the alterantive “account” model, where transactions describes the “action” that the blockchain node needs to execute.

Your job as a developer is to know how to build Cardano transactions that represent the interactions with your system. Your goal is to turn external events (a button in an UI, an API call, some condition on-chain, a cron-job, etc) into transactions that represent the desired change3.

Snowflakes Apps

We now understand that the business logic of our app lives outside of the blockchain, but where exactly? is it in the browser of the user? in a backend in the cloud? as a mobile app?

The fact is, it can live anywhere.

If you’re building a web app that is stateless and only requires some user-input and wallet data, you could probably build your business logic in javascript and bundle it with the rest of your frontend. Something similar applies for simple mobile apps.

If you’re app requires you to query some state from a database and perform complex decision-making processes, you would probably include your business logic as part of a backend component and deploy it in the cloud.

If you’re building a bot that needs to continuously monitor blockchain activity and react by submitting new transactions unattendedly (eg: like a DEX batcher), you could potentially run it in your laptop as a background process.

This flexibility is a double-edged sword. For sure, is nice for developers to choose the tool and environment that is better suited for their use-case. At the same time, the lack of a common interface and framework has some nasty side-effects:

  • isolated apps: each app lives in isolation from the rest of the world, constrained by it’s own interface, because there’s no standard way of interacting with them.
  • re-invent the wheel: each project ends up building their own middleware for interacting with the blockchain (reading on-chain data, submitting transactions, etc).

The end result is “snowflake” apps, each one is unique.

Headless Apps

“Headless dApp” refers to the idea of decoupling the business logic from the external context by forcing all inputs and outputs through a well-defined interface. To run your app, you’ll need a generic runtime that knows how to connect your business logic to the outside world4.

This strict separation of concerns provides several benefits:

  • portability: you can run your app in different contexts depending on your needs. From the terminal, on your browser, on the cloud, etc. As long as you have a compatible runtime to host your app, you should be good.
  • composability: by having a concrete, programatic interface to interact with the business logic of your app, you can “compose” complex apps that orchestrating other apps to fulfill a higher-level objective.
  • multiple frontends: your app can have multiple frontends, maybe even developed by different teams. For example, a DEX could have different web frontends, the user could pick their favorite.
  • less plumbing: the runtime component can be quite generic and reused by many dApps. There’s no need to re-implement how to query on-chain data, how to build transactions, how to submit transaction, etc. You can focus just on your business logic knowing that plumbing is already taken care of.

The Balius Ecosystem

Balius is a comprehensive ecosystem for building headless Cardano dApps with a collection of tools and libraries:

  • balius-sdk: The core SDK for building headless dApps. It provides all the basic building blocks for your business logic.
  • balius-runtime: A library that manages the execution environment for your dApp, handling connections to blockchain services.
  • Tooling:
    • balius: A Cargo plugin for creating, building, and testing Balius projects.
    • baliusd: The Balius daemon that can run your headless dApp in various environments.

These components work together to provide all the plumbing out-of-the-box, so you can focus on your business logic knowing that the infrastructure is already taken care of.

What does it look like?

The introduction sounds good and very fancy but you’re not convinced until you see some code, right?

The following snippet shows how to create an HTTP endpoint from using the framework:

#[on_http_get(path = "/hello")]
fn say_hello() -> Result<JsonValue> {
let output = json!({ "message": "hello world!" });
Ok(output)
}

The following snippet shows how react to an UTxO being locked in a script address relevant for your dApp:

#[derive(Datum)]
struct MyDatum {
foo: PlutusInt,
bar: PlutusInt,
}
#[on_inbound_utxo(to=SCRIPT_ADDRESS)]
fn on_asset_sold(utxo: UTxO) -> Result<()> {
// do something interesting with the new UTxO
let datum = utxo.datum_as::<MyDatum>();
println!(datum.foo);
Ok(())
}

The following snippet shows how react to an UTxO being unlocked in a script address relevant for your dApp:

#[on_outbound_utxo(from=SCRIPT_ADDRESS)]
fn on_asset_sold(utxo: UTxO) -> Result<()> {
// do something interesting with the UTxO data
println!(utxo.address);
println!(utxo.coin);
Ok(())
}

Footnotes

Footnotes

  1. Keeping the on-chain code as small as possible it’s very desirable trait. On-chain code is executed one time by each node of the blockchain. This is required as part of the consensus, but very redundant from the computational perspective.

  2. of course, one could inspect the script on-chain and tell the difference between a real validator and a mock one, but no difference whatsoever from the UX perspective.

  3. everything that lives outside of the blockchain is usually referred to as “off-chain”. It’s a little bit egocentric if you ask me, but serves its purpose. In our experience, there’s usually much more off-chain code than on-chain.

  4. this concept is very similar to “hexagonal architecture” or “clean architecture”. If this idea of strict separation of concerns resonates with you, I encourage you to take a look at those patterns too.