Skip to content

Licensing

Helix is commercial software. Every Helix binary verifies a signed license file on startup; without one, it refuses to run. This page explains how the license system is wired up, where to put the file, what the failure modes look like, and how to integrate with CI / containers / Kubernetes.

TL;DR

Two equivalent ways to point Helix at your license:

# (a) Env var — set once, applies to every helix call in this shell.
export HELIX_LICENSE=/path/to/your.helix.license
helix run flow.yaml

# (b) Global flag — works on every subcommand. CLI wins over env.
helix --license /path/to/your.helix.license run flow.yaml
helix -L /path/to/your.helix.license run flow.yaml
helix --license=/path/to/your.helix.license run flow.yaml

# Inspect a file without setting the gate:
helix license verify --file /path/to/your.helix.license

The --license / -L flag is parsed before the startup gate, so it works on every subcommand — run, validate, schemas, components, version, etc. — without per-command flag plumbing. It takes precedence over $HELIX_LICENSE when both are set, which is useful in CI where you want a one-shot override.

When the env var is missing or the file is invalid, Helix exits with status 2 and a message pointing to sales@massil.ai.

How it works

The system has three independent pieces:

flowchart LR
  subgraph Massil
    Issuer[helix-license-issuer<br/>Rust binary] -->|signs| File
    Web[helix-license-web<br/>sales console] -->|signs| File
  end
  File[(.helix.license<br/>PEM file)]
  subgraph Customer
    File --> Runtime[helix binary]
    Runtime -->|verify| OK[Banner + run]
  end
Layer Role Source
Issuer Signs license files. Lives in Massil's sales backoffice. Touches the private key. helix-license/ (Rust)
File *.helix.license — PEM-wrapped JSON payload + Ed25519 signature. The thing the customer receives. Generated artifact
Verifier Pure-Go port of the verify logic. Embedded prod public key. Runs every time helix starts. pkg/license/

The customer-side path is entirely offline. There is no callback to Massil at verification time. The only network contact is at issuance (file emailed/portal'd to the customer) and renewal.

File format

A license file is a PEM envelope with two blocks plus a human-readable comment header:

# Helix License
# Customer:    Acme Corp
# Contact:     ops@acme.example
# License ID:  lic_acme_prod_2026
# Type:        Production
# Issued:      2026-06-01
# Expires:     2027-05-29
# Products:    Studio (25 seats, team), Runtime (80 vCPUs, 16 vCPU/node, ...)

-----BEGIN HELIX LICENSE-----
<base64 of canonical JSON payload>
-----END HELIX LICENSE-----

-----BEGIN HELIX SIGNATURE-----
ed25519
<base64 of 64-byte signature>
-----END HELIX SIGNATURE-----

The comment header is informational and can be edited freely; it's not part of the signed payload. The two PEM blocks are the actual contract between Massil and the customer. Editing one byte inside either block invalidates the signature.

Payload schema

The signed JSON payload — same shape on both the Rust signer and Go verifier:

Field Type Notes
v uint32 Schema version. Current: 1. Older parsers reject newer v instead of mis-reading.
license_id string Stable identifier (e.g. lic_acme_prod_2026). Recorded in audit logs.
customer string Company name shown in the banner. Surfacing this is part of the anti-sharing posture (see Security model).
customer_email string? Contact email. Optional.
license_type trial / non_production / production Drives the watermark and per-env behavior. Defaults to production on legacy files that omit the field.
issued_at ISO 8601 timestamp Verified against the local clock.
expires_at ISO 8601 timestamp Strict now < expires_at. No grace.
products.studio { seats, edition, features[] } Read by Helix Studio (the IDE). Optional.
products.runtime { max_cpus, max_cpus_per_node, max_nodes, max_qps_per_node, overage_policy, features[] } Read by the Helix runtime. Optional.
notes string? Free-form. Visible to anyone who base64-decodes the file.

License types

Type Banner prefix Watermark Use case
production none — looks like a normal log line — Real customer workloads.
non_production [NON-PROD] Yes Dev/staging deployments.
trial [TRIAL — NOT FOR PRODUCTION USE] Yes First-touch evaluation. 30-day default expiry.

The startup gate

Every helix subcommand passes through the license gate before dispatching, with two exceptions:

  • helix license … — exempt by design. You can't verify a license if you need one to invoke the verifier.
  • __sandbox-exec — internal re-exec wrapper for the coding agent's shell tool. The parent process already passed the gate.

Every other subcommand — run, start, version, components, help, etc. — fails with exit code 2 if $HELIX_LICENSE is unset or invalid.

This is strict by design. There is no --ignore-license flag, no "warn-only" mode, no fallback to dev caps. Operator intent ("yes, this binary is licensed to run") is captured by setting one env var.

Setting the license

Local development

Export the env var in your shell profile, or use direnv:

# ~/.bashrc or .envrc
export HELIX_LICENSE=$HOME/.config/helix/license.helix.license

Docker

Mount the file and point the env var at it:

# In your Dockerfile or compose file:
ENV HELIX_LICENSE=/etc/helix/license.helix.license
# docker-compose.yml
services:
  helix:
    image: helix:0.3.0
    environment:
      HELIX_LICENSE: /etc/helix/license.helix.license
    volumes:
      - ./license.helix.license:/etc/helix/license.helix.license:ro

Kubernetes

Mount as a Secret. The file is signed — it's not a credential in the usual sense, but treat it as confidential anyway (customer name, license terms, etc. are in plaintext within).

apiVersion: v1
kind: Secret
metadata:
  name: helix-license
type: Opaque
data:
  license.helix.license: <base64 of the file>
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helix
spec:
  template:
    spec:
      containers:
        - name: helix
          env:
            - name: HELIX_LICENSE
              value: /etc/helix/license.helix.license
          volumeMounts:
            - name: license
              mountPath: /etc/helix
              readOnly: true
      volumes:
        - name: license
          secret:
            secretName: helix-license

CI

For tests / validate runs in CI, pass a non-production license via a secret env. The license can be the same file used in dev:

# .github/workflows/test.yml
env:
  HELIX_LICENSE: ${{ secrets.HELIX_NONPROD_LICENSE_PATH }}

Or, if the license file content is stored as a secret:

steps:
  - name: Stage license
    run: echo "$HELIX_LICENSE_BODY" > /tmp/helix.license
    env:
      HELIX_LICENSE_BODY: ${{ secrets.HELIX_LICENSE_BODY }}
  - name: Run helix
    env:
      HELIX_LICENSE: /tmp/helix.license
    run: helix test ./...

The helix license subcommand

Self-exempt from the gate, so you can use it to inspect a license file without needing one to be active.

helix license verify [--file PATH]

Reads the file at --file (or $HELIX_LICENSE if no flag), verifies the signature against the embedded prod public key, enforces the issued/expires window, and prints a human summary:

OK  lic_acme_prod_2026
    file:      /path/to/acme-prod.helix.license
    customer:  Acme Corp
    type:      Production
    issued:    2026-06-01 07:43:58 UTC
    expires:   2027-05-29 00:00:00 UTC
    studio:    25 seats (team), features: [multi-project team-sync]
    runtime:   80 vCPUs total · 16 vCPUs/node · 10 nodes · 1000 QPS/node
    overage:   warn (log + metric, traffic flows)
    features:  [cluster saml audit]

Exit code: 0 valid, 1 any failure.

helix license info [--file PATH]

Same lookup; prints the verified payload as pretty JSON. Useful for scripting (jq '.products.runtime.max_cpus').

helix license show [--file PATH]

Alias for info.

Failure messages

You see What it means Fix
Helix license required. $HELIX_LICENSE is unset. export HELIX_LICENSE=...
Helix license invalid: license: signature mismatch — file has been modified or was signed by the wrong key The PEM was tampered with, OR was signed by a key your binary doesn't trust (staging key in a prod binary, or vice versa). Re-download the file from sales. If still failing, you may have a binary built against a different public key.
Helix license invalid: license: expired at YYYY-MM-DDTHH:MM:SSZ Past expires_at. Renew via sales.
Helix license invalid: license: not valid until YYYY-MM-DDTHH:MM:SSZ System clock is before issued_at. Usually clock skew (drifted NTP, container without a synced clock). Fix NTP, or wait.
Helix license invalid: license: missing HELIX LICENSE block File is truncated, swapped with the wrong artifact, or wasn't a valid .helix.license to begin with. Verify the file content has both -----BEGIN HELIX LICENSE----- and -----BEGIN HELIX SIGNATURE----- blocks.
License: reading … no such file or directory $HELIX_LICENSE (or --file) points at a path that doesn't exist. Double-check the path.
Helix license error: embedded public key corrupt The binary's embedded prod.pub is malformed. This means a broken build — should not happen in shipped binaries. Re-download the binary.

Watermark behavior

For non-production licenses, every log line on the startup banner and the human banner is prefixed with the watermark:

[TRIAL — NOT FOR PRODUCTION USE] Helix licensed to Beta Inc · Trial · expires 2026-07-01
[NON-PROD] Helix licensed to Acme Corp — Dev/Staging · Non-Production · expires 2027-05-29

Production licenses look exactly like an unlicensed binary used to look, because production must look like production to ops engineers.

Future versions will also embed the watermark in HTTP response headers for non-prod runtimes, so a non-prod cluster is detectable by anything downstream.

What's enforced today, what isn't

Behavior Enforced now Planned
Startup verification + expiry ✅ —
Banner + watermark ✅ —
Hard-fail on missing/invalid ✅ —
helix license verify/info ✅ —
CPU/QPS metering ❌ Next release — RuntimeGrant.max_cpus becomes a real cap
Non-prod refusal in prod env ❌ When HELIX_ENVIRONMENT=production is set, non-prod licenses refuse to start
Revocation list ❌ Embed payload_sha256 blocklist in releases
Key rotation ❌ — single embedded key Accept slice of keys; transition window per architecture docs
HTTP watermark headers ❌ Tied to runtime metering

The reference list of RuntimeGrant and StudioGrant fields is in the license code. Field defaults that matter: overage_policy defaults to warn (log + metric, traffic flows). Hard block (503 above cap) is opt-in.

Architecture deep-dive

Cross-links to the Rust issuer / web / keygen workspace and the security model — those live in the sibling repo:

  • Rust workspace: helix-license/ — core/, keygen/, issuer/, cli/, web/.
  • helix-license/docs/ — full architecture doc set including:
  • File format specification
  • Cryptography choice (Ed25519 + canonical JSON via BTreeMap)
  • Key management + rotation procedures
  • Schema evolution
  • The 7-threat security model (defended threats, undefended-but-mitigated, out-of-scope)

The Go port (pkg/license/) handles ISO 8601 "expanded year" notation (+002027-05-29T00:00:00.000000000Z) emitted by the Rust time crate; the Go Time wrapper normalizes this before stdlib parsing.

Source

  • Verifier: pkg/license/license.go
  • Embedded key: pkg/license/prod.pub (copied from helix-license/keys/prod.pub)
  • CLI subcommand: cmd/helix/license.go
  • Startup gate wiring: cmd/helix/main.go (switch command exemption + gateStartup call)
  • Tests: pkg/license/license_test.go (12 tests including cross-impl verification of a Rust-issued file)

See also