Skip to content
hekla

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.