hekla, the runtime
All twelve subcommands, what each one needs, and what each one refuses.
hekla [OPTIONS] <COMMAND>
Twelve subcommands. --no-color is global and is accepted before or after the subcommand;
colour is off anyway when output is not a terminal or NO_COLOR is set to a non-empty
value.
Every DIR argument defaults to ., and every --data-dir defaults to <dir>/data.
What each one needs
| project | data dir | takes the lock | master key | exit 1 when | |
|---|---|---|---|---|---|
hekla check |
yes | no | no | no | any error finding |
hekla test |
yes | no | no | no | a failing test or an error |
hekla secrets |
yes | no | no | no | a required credential is unset |
hekla openapi |
yes | no | no | no | errors, or nothing to describe |
hekla plan |
yes | yes | no | with --replay |
errors, or an unmigrated directory |
hekla project |
yes | yes | no | if the projection seals a column | errors, no projector, or no log |
hekla serve |
yes | yes | yes | if it declares a subject |
it could not start |
hekla verify |
yes | yes | yes | if it declares a subject |
the report is not clean |
hekla rewind |
yes | yes | yes | no | no such effect, or the lock is held |
hekla erase |
no | yes | no | no | no operational database |
hekla rotate |
no | yes | no | yes | the key is unset |
hekla adopt |
yes | yes | no | yes | the key is unset, the schema version is not this build’s, or a key no event accounts for |
The lock column decides what you can run against a live server: plan, project, erase,
rotate and adopt are fine; verify and rewind need it stopped.
check
hekla check .
checked 2 module(s): 1 command(s), 1 projector(s), 0 effect(s), 1 event(s)
ok: no errors, 0 warning(s)
Runs the compiler and adds the runtime’s own rules: that a declaration sits in the directory its kind requires, and that a read model can be keyed and indexed the way the read API needs.
It runs nothing, opens no data directory and reads no environment, which is what keeps it usable as a CI gate.
Findings print as severity: file:line:col: message, with an optional = hint under them.
What it adds over the compiler
Errors: a command outside commands/, a projector outside projectors/, an effect
outside effects/, an event field beginning with the reserved _hekla_, a sealed entity
column that is not optional, an entity holding a sealed column with no column of that
subject’s type beside it, an index covering a subject-encrypted column, a key column whose
name collides with a generated index name, and a filterable field colliding with a reserved
read query parameter (limit, cursor, after, timeout_ms, order_by).
Warnings: a fold slice with no high-cardinality filter, an over-constrained slice, a
declared secret nothing reads, a [secrets] entry naming no declaration, and a slice that
filters on a field carrying @absent, which can only ever match events appended since that
field was declared.
test
hekla test .
ok: "a new address joins the list"
ok: "the same address twice is refused"
2 passed, 0 failed
Runs every test declaration in the program, wherever it is declared, synthesising the
event envelope the runtime would produce. A project with no tests prints 0 passed, 0 failed and exits 0.
serve
hekla serve . --addr 0.0.0.0:8080
| Flag | Default |
|---|---|
--addr |
127.0.0.1:8080 |
--data-dir |
<dir>/data |
--verify |
off |
Boot fails before binding when the project declares a subject and HEKLA_MASTER_KEY is
unset, and when another process holds the data directory. It also moves every subject key
under the parent its subject declares before it serves, and refuses to serve without it:
hekla adopt is the same work run ahead of the deploy.
See Running a project for startup order, shutdown and every configuration key.
plan
hekla plan --replay --json .
| Flag | |
|---|---|
--json |
the whole plan on stdout, for a deploy gate |
--replay |
re-run recorded effect invocations against the candidate code |
--replay-limit N |
how many of each effect’s most recent invocations, default 1000, minimum 1 |
--data-dir |
--replay-limit requires --replay, because a cap on a replay that is not happening is a
request the command would otherwise accept and drop.
Exits 0 whether or not anything would change: a change is the answer, not a fault. Full treatment in Change safety.
project
Ask the log a question you have not deployed. FILE is a standalone .hk file declaring
one projector; it is folded over the log into a read model in a temporary directory, the
rows are printed, and the directory is deleted.
hekla project ~/by-customer.hk . --data-dir /srv/hekla/data
folded 38214 event(s) of @order.placed into `projector ByCustomer` in 1.70s
entity PerCustomer, key customer_id, 3 row(s)
sealed: last_email under Customer
customer_id orders last_email
1 21044 ada@example.test
2 14903 -
7 2267 grace@example.test
a snapshot at position 412903, the tip pinned when this opened; anything appended since is not in it
ok: 3 row(s) from 38214 event(s)
| Flag | |
|---|---|
--projector NAME |
which one, when the file declares more than one |
--entity NAME |
which entity’s rows to print |
--from N / --upto N |
bound the window folded |
--max-events N |
stop after that many events |
--rows N |
how many rows to print |
--no-decrypt |
withhold the rendering, the way ?decrypt=false does |
--json |
the whole projection on stdout |
Nothing is deployed and nothing is recorded: no declaration row, no database under
data/projectors/, no checkpoint, no route, no metric. A later hekla plan cannot tell it
ran. It reads through the same read-only follower plan --replay uses, so it takes no lock
and runs against a directory a server has open.
The scratch file is compiled with the project, which is what lets it name events it does
not declare, call the project’s fn helpers and read its consts. The placement rule is
relaxed for that file alone, so the projector need not sit under projectors/; a command
or an effect written there is still an error, because neither folds over anything.
A bounded answer says so. --max-events ends the report on partial: rather than
ok:, --from and --upto name the positions they did not fold, and --rows says how
many more rows there were.
--upto bounds the answer rather than the work: the read is planned over the whole log and
the window is applied as the fold walks it, so --max-events is the one that reaches the
planner.
A projection that seals a column needs HEKLA_MASTER_KEY, because a projector re-seals under
the column’s own field name rather than carrying the log’s ciphertext through. That is
refused up front rather than part-way through the scan.
Exits 0 whenever the projection ran, whatever it found. Findings go to stderr, so
hekla project q.hk . --json > out.json writes only JSON.
verify
hekla verify .
Checks that a projector rebuilt from position 0 matches the live one, that every recorded effect invocation still replays without performing anything, and that no subject key sits under the master while its subject declares a parent.
Takes the lock, so it refuses against a directory a server has open. Verify a copy, which checks the backup at the same time. Exit 0 only when the report is clean.
secrets
hekla secrets .
DISCORD_WEBHOOK set 2633c771 env DISCORD_WEBHOOK_URL
SENTRY_DSN unset (optional) env HEKLA_SECRET_SENTRY_DSN
STRIPE_KEY MISSING file /run/secrets/stripe
failed: 1 of 3 credential(s) unset; serving would refuse to start
Never prints a value. Reads the environment and [secrets] and nothing else: no data
directory, no log, no lock. Exits non-zero when a required credential is unset, so it stands
alone as a pre-deploy gate.
erase
hekla erase Customer 7 .
subject `Customer` = `7`
key present, and about to be deleted
beneath it no other keys
This is irreversible. Every value scoped to these keys becomes unreadable
across the log and every read model at once.
Continue? [y/N] y
erased subject `Customer` = `7`
Deletes that subject’s encryption key, making every value scoped to it unreadable across the log and every read model at once. Irreversible. Needs no master key and takes no lock.
The first argument is the subject’s declared name, which is what a key row is filed
under: subject Customer(Int) plus buyer: Customer erases as hekla erase Customer 7
whatever the field is called. It is the same pair a test writes as erased Customer "7".
| Flag | |
|---|---|
--yes |
answer the prompt without silencing the summary |
--data-dir |
A subject with children takes them with it, so the summary counts them first:
beneath it 2 key(s), which become permanently unreadable. The count walks parent
pointers and unwraps nothing, which is why this still needs no master key.
Without a TTY and without --yes the command refuses rather than exiting 0, so a script is
never told an erasure happened that did not. Declining prints nothing was erased. A
subject with no key answers no key for subject ... (already erased or never created). Both
exit 0.
rotate
hekla rotate .
rewrapped 412 subject key(s) under the primary master
Rewraps every root subject key under HEKLA_MASTER_KEY, unwrapping with
HEKLA_MASTER_KEY_PREVIOUS as needed. Ciphertext is unchanged, so reads keep working. A
subject with a parent is never counted: its key is derived from that parent’s secret, which a
rotation does not change. The ordered procedure is in
Keys and secrets.
adopt
hekla adopt .
adopted 3 subject key(s) under their declared parent, reading 3 event(s)
Moves every subject key under the parent its subject declares, for rows minted while that
subject declared none. hekla serve does this at boot and refuses to serve without it, so
this is how the work happens ahead of a deploy rather than during one.
Point it at the new project and the live data directory. It reads the log through a follower and takes no lock, so the server still serving the old declaration is unaffected, and the boot that follows finds nothing left to do. It changes no ciphertext and destroys nothing, because the wrapping moves and the key does not, so there is no prompt.
| Flag | |
|---|---|
--no-progress |
do not draw the progress line |
--data-dir |
A second run reports every subject key is already under its declared parent. A project
where no subject declares a parent reports no subject declares a parent, so there is nothing to adopt, and a data directory nothing has been deployed to reports nothing is deployed at <path>. All exit 0 and none needs a project to have subjects at all, so a deploy script can
run this over every project it has without special-casing the flat ones.
A subject whose events name two different parents is a warning on stderr and the run still succeeds: the key is filed under the first one, which is the rule, and only that one’s erasure reaches it. That report is what the pass noticed on its way rather than an audit, so it names each subject once and stops collecting after twenty.
Contention exits 0. A live deployment is serving the old declaration the whole time and keeps minting keys this run will never catch, so the command says what it could not move and leaves those to the boot. A key no event in the log accounts for is the opposite and exits 1.
Like plan and project, it refuses a data directory whose schema version is not this
build’s and never migrates one, because it is pointed at a directory a server may still be
serving from.
rewind
hekla rewind NotifyCustomer 412 .
Moves an effect back to a position so it reprocesses everything after it and performs those side effects again. Irreversible.
| Flag | |
|---|---|
--live |
also lower an on live boundary. Permanent, and off by default |
--yes |
answer the prompt without silencing the summary |
--data-dir |
Position 0 reprocesses the whole log. Refuses while a server holds the directory, and
without a TTY refuses unless --yes is given.
Deliberately not an HTTP endpoint. See Effects in production.
openapi
hekla openapi . > openapi.json
The generated OpenAPI 3.1 document on stdout, findings on stderr, so the redirect is safe.
The same document /openapi.json serves.
Refuses a path that is not a directory, a project with errors, and a directory that declares nothing at all.
There is no hekla fmt or hekla lsp
Neither exists. They arrive when heklang has a language server of its own. Formatting is
hek fmt’s job, and it rewrites every .hk file a project holds today.