Skip to content
hekla

Deploying

What the artefact is, what to back up, what to watch, and the limits stated plainly.

The artefact is the source tree

There is no build step, no compiled output and no compile cache. What you deploy is the .hk files, plus hekla.toml if you have one, plus the master key if the project declares a subject.

Deploy is copy and restart. There is no hot reload: the project is read at startup, and a running process never re-reads it.

The data directory

data/
  events/                tephra segments, the immutable source of truth
  projectors/{Name}.db   one read model and its checkpoint
  hekla.db               effect journals, subject keys, deployed declarations
  hekla.lock.db          the exclusive one-process lock

Two of those are irreplaceable: events/ and hekla.db. Projector databases are rebuildable from the log, so they are the part you do not need to keep.

The lock lives in its own file rather than inside hekla.db, because the operational database is written continuously and an exclusive transaction held for the process lifetime would deadlock against it.

Opening any of these files directly is not a supported surface, and the table layout is private.

Backup

Copy the directory of a stopped process.

The good pattern is to stop, copy, start, and then run hekla verify against the copy. That checks the invariants and the backup in one pass, on a machine that is not serving traffic.

Migrations

The operational database migrates itself on open, one schema version per transaction, and refuses a database newer than the build:

operational database is at schema version 12, newer than this build (11)

So a downgrade is a refusal rather than a corruption. hekla plan refuses a directory this build has not migrated yet; run hekla serve against it once first.

A key namespace written before subjects were declared

One migration needs an answer from you, and only for a directory that already holds sealed data written by hekla 0.6.0 or earlier. A key row is filed under a subject’s declared name, and a directory written before the declaration existed files it under the field an @subject annotation named. The wrapped secret carries across untouched; the label does not, and nothing in the database records which field became which subject. A row under a label the program never looks up is unreachable, and every value sealed under it reads back absent, which no caller can tell from an erasure.

So the upgrade refuses until it is told the mapping, rather than succeeding and leaving data that looks deleted:

HEKLA_V10_SUBJECTS="customer_id=Customer,shop_id=Shop" hekla serve ./shop --data-dir ./data

The refusal names every namespace it found, which is the list to answer. A namespace that should keep the spelling it has is written as itself, legacy_ref=legacy_ref, which is also what to write for one you have decided to abandon. Two namespaces may merge onto one subject, and that is refused only if both hold a key for the same id. An entry naming a namespace this directory has nothing under is a warning, so one variable can cover several directories. The right-hand side is taken on trust, because the migration runs before any program is loaded: check it against your .hk files.

A refused upgrade changes nothing, the schema version included, so the previous build still opens the directory while you work the mapping out. Unset the variable once the directory is upgraded; it is read on that one step and does nothing afterwards.

Giving a subject a parent

A deploy that adds under to a subject whose keys already exist moves every one of them under that parent before it serves, and refuses to serve if it cannot. That is a scan of the log, holding the data-directory lock with the listener down, so on a large log it is the part of the boot that takes time.

hekla adopt does the same work ahead of the deploy, against the live directory and without the lock, so the boot that follows finds nothing left to do. hekla plan counts what is waiting before either.

The event log is not migrated, it is read as it stands. An event type carries no version and there is no upcast hook. A field added to an event that already has history says what it reads as with @absent(<value>), or becomes optional. See Events.

That is checked before the process serves anything. serve compares the program’s declarations against every shape ever recorded for those types, then decodes the oldest stored event of each declared type. A field no recorded shape can answer for, or a stored payload this program can no longer read, refuses the start:

this program cannot read events that are already in the log, so it cannot serve:
`@booking.confirmed` (no stored event can carry `channel`), so nothing has been
recorded and correcting the declaration and deploying again is the whole of the repair.
A field was added to an event that already has instances, and no payload written before
it can carry one: say what it reads as with `@absent(<value>)`, or make it optional so
the type itself says absence is possible.

The message carries its own repair, one sentence per distinct fault. verify runs the same check and refuses the sweep with the same findings, saying it cannot verify rather than cannot serve, and noting that nothing was changed. Both hold the data-directory lock, so neither is something to point at a directory a server has open.

The first half of the check is complete: it is settled from the declaration table without opening an event. The second samples one stored event per declared type, so it is a floor rather than a survey, and it stays silent about a type the first half already answered for. hekla plan reports both without refusing, whenever the diff touches an event, record or enum declaration, and it is the one that takes no lock. See Change safety.

Observability

Logs

tracing, filtered by RUST_LOG, default info. Installed by serve and verify only, so every other subcommand stays plain and machine-parseable: hekla openapi . > openapi.json and hekla plan --json are safe to pipe.

Colour is on only when stdout is a terminal and nobody opted out. NO_COLOR with any non-empty value, or --no-color, turns it off.

Lines worth alerting on: a failed ctrl-c handler registration (shutdown must then be forced), a completed projector rebuild with its duration, and an in-flight invocation whose recorded source hash differs from the code on disk, logged by name at startup.

/status

The whole runtime in one document: log head, uptime, every projector’s position, lag, readiness and last error, and every effect’s state, lag, wedged lanes, pinning key and pinning position. This is what a console or a health dashboard should read.

/metrics

Prometheus text exposition. Gauges are read at scrape time, so there is no interval and no [metrics] section.

Gauges

hekla_build_info, hekla_uptime_seconds, hekla_log_head_position, hekla_module_info, hekla_projector_up, hekla_projector_position, hekla_projector_lag, hekla_projector_readiness, hekla_projector_last_progress_timestamp_seconds, hekla_effect_up, hekla_effect_position, hekla_effect_lag, hekla_effect_state, hekla_effect_wedged_lanes, hekla_effect_consecutive_failures, hekla_effect_retry_backoff_seconds, hekla_effect_last_progress_timestamp_seconds

Counters

hekla_events_appended_total, hekla_commands_total, hekla_command_refusals_total, hekla_command_conflict_retries_total, hekla_projector_events_total, hekla_projector_rebuilds_total, hekla_effect_invocations_total, hekla_effect_restarts_total, hekla_effect_terminal_skips_total, hekla_effect_live_suppressed_total, hekla_effect_collapsed_total, hekla_effect_http_requests_total, hekla_reads_total, hekla_read_waits_total

Two labelling rules are design commitments rather than conventions.

A lane key is never a label. Every label is a declaration: a command, projector, entity, effect or event name, a refusal code, or a fixed outcome word. The reason is erasure, not cardinality. A lane key is a partition key, and /status may name it because it is a live view an erasure passes through; a scrape may not, because it is a copy taken somewhere hekla erase cannot reach. So the metric counts wedged lanes and /admin/effects/{name} names them.

A refusal series appears on first use. heklang inlines a refusal, so hekla cannot enumerate the codes to prime them at boot. Any expression over hekla_command_refusals_total needs or vector(0). Every other counter reads 0 from the first scrape.

Alerts

The repository ships a rules file at docs/monitoring/hekla-alerts.yml with sixteen alerts, each carrying the reasoning for its threshold inline: HeklaDown, HeklaProjectorStalled, HeklaEffectWedged, HeklaEffectQuarantined, HeklaEffectBlocked, HeklaCommandConflictsExhausted, HeklaLogNotAdvancing, HeklaReadYourWritesTimingOut and others. Start from it rather than from scratch.

/metrics is unauthenticated like everything else, so the scrape target is reached the same way /admin is.

The limits, stated plainly

Saying these first is the point, so here they are without hedging.

Nothing is authenticated. The bind address is the boundary and it defaults to loopback. A caller who reaches the port can append events, browse the console, read metrics and skip an effect’s work.

One process per data directory, enforced by an exclusive lock. There is no clustering, no replication and no horizontal scaling.

Deploy is restart. No hot reload, no upload API, no live editing.

No version on an event type and no upcast hook. A field added later reads through @absent, but renaming one or changing its type means a new field or a new type.

A scan filters on one index. Equality across any prefix of a declared index, plus a range on the column after it, and nothing wider: a filter spanning two indexes is a 400 rather than a table scan.

No automatic dead-lettering. A wedged effect is never advanced automatically; the manual skip endpoint is the only escape hatch.

No hekla fmt and no language server. Both arrive when heklang has a language server of its own. hek fmt exists today and is what to point an editor at; see Editor setup.

/docs needs outbound network, because it loads its reference UI from a CDN. /openapi.json does not.

invoke is exactly-once only when the target command is idempotent under replay. Raw http.* is at-least-once. That is an inherent property of the design rather than future work: the journal can guarantee a call is not repeated after a crash, but it cannot make a remote endpoint idempotent for you.

Pre-1.0. hekla is 0.7 and heklang is 0.9. Both carry breaking changes between minor versions, and neither has run your workload yet.

And one that will not change

heklang is the only authoring surface, permanently. There is no Rust, TypeScript or WASM SDK, and there is not going to be one. A single pure sandboxed authoring language is what makes the rebuild, the replay and plan --replay sound, and an escape hatch into a general purpose language would take all three away.