Skip to content
hekla

Introduction

What hekla is, what you write, and what the runtime does with it.

hekla is a single-app event-sourcing runtime. You point it at a directory of .hk files and it becomes your application’s backend: it serves the HTTP API, keeps the read models, runs the durable side effects, holds the encryption keys, and gives you a console over all of it. There is no build step, because there is nothing to compile. Deploy is restart.

The language is heklang, and it is deliberately small. Five kinds of declaration do the work, with a handful of supporting ones around them: event, command, projector, effect and test carry the weight, and guard, refusal, fn, record, enum, const and secret exist so a rule is written once. Project layout covers where each one goes.

The five declarations

An event is a fact that already happened, named @domain.thing. It is the only durable state: every other declaration either writes one, reads them, or reacts to one, and its fields become the tags a fold can select on.

A command replays the history its decision depends on and appends events. It is the only declaration that writes to the log.

refusal AlreadyJoined "that email is already on the list"

command JoinWaitlist(entry_id: Uuid, email: String) {
  fold joined: Bool = false
    on @waitlist.joined(email) => true

  if joined {
    return reject AlreadyJoined
  }

  emit @waitlist.joined { entry_id, email }
}

A projector consumes events into a read model, which hekla serves over HTTP and rebuilds from position zero whenever its definition changes.

An effect reacts to appended events with durable side effects. It is the only declaration that reaches the network, and every call it makes on the world is journaled, so a crash mid-arm resumes without re-firing what already happened.

A test is a declaration too, so the cases live beside the code they exercise.

Around those five, a guard is a named proposition about the log that several commands can share, and a refusal is a named reason one said no.

What you fold is what you conflict on

The idea the whole runtime rests on is that fold is a read declaration, not a variable. It names a slice of the log, and the slices a command folded are the condition its append is checked against. If another writer lands in one of those slices between the read and the append, the append is rejected and the command re-decides against the new log.

Optimistic concurrency therefore falls out of the code rather than being configured beside it, and the declared boundary cannot drift from the actual reads, because they are the same object. That is the Dynamic Consistency Boundary, and Why DCB covers the problem it removes.

The restrictions are the point

A command cannot reach the network. A projector cannot decrypt, has no failure channel and cannot read a clock. A fold cannot observe anything but the log. None of that is enforced by a check at run time: http.post does not parse inside a command, and reveal does not parse inside a projector.

That is what makes rebuilding a read model and replaying an effect reproduce exactly what they did the first time, rather than usually reproducing it. It is also what lets hekla plan --replay answer a question no diff can: whether your candidate code would still make the same calls, replayed against the journal the original run left behind.

heklang is also total. There is no while, recursion is rejected statically with the cycle named, a for runs once per element of a finite container, and a fn returns on every path. Every program terminates.

What hekla adds around the language

heklang describes what a declaration means. hekla is what makes it real:

  • Routing. command PlaceOrder is served at POST /commands/PlaceOrder. The route is the declared name, never the file name.
  • The read API. Each projector entity gets a key lookup and a filtered scan, with only the key and the leftmost column of each declared index filterable.
  • OpenAPI and a console. Both generated from your declarations, both served from the same port.
  • Storage. tephra for the event log, embedded as a library rather than run as a server, and bundled SQLite for the read models.
  • Encryption. A field marked @subject(customer_id) is encrypted under a key scoped to that customer before it reaches storage, and hekla erase customer_id 42 deletes that key.

What it is not

hekla is one process, one node, one logical writer, and one data directory. Nothing it serves is authenticated: the bind address is the boundary, and it defaults to 127.0.0.1. There is no hot reload. There is no SDK in another language and there is not going to be one, because a single pure sandboxed authoring language is what makes all of the above sound.

It is also early. hekla is 0.4 and heklang is 0.5, both carry breaking changes between minor versions, and neither has run your workload yet.

Where to go next

Getting started builds a project from an empty directory and calls it, How it works follows one request through the runtime, and Editor setup gets highlighting and format on save working.

From there the tree splits three ways: Writing a project is the language, declaration by declaration; Operating it is everything that happens after the code is right, and Effects in production is the page to read before you deploy one; Reference is the closed sets, all derived from the compiler and the runtime rather than retyped.