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 PlaceOrderis served atPOST /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, andhekla erase customer_id 42deletes 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.