Sealrdocs
Reference

Configuration

The recorder home layout and config.json fields, and every Console API environment variable.

Two things are configured separately: the recorder on each machine, and the Console if you run one. The recorder works with neither an account nor a Console — offline mode is the default.

Recorder

Where its state lives

Resolution order: the --home flag, then $SEALR_HOME, then the platform default.

PlatformDefault home
Windows%LOCALAPPDATA%\sealr
Linux$XDG_DATA_HOME/sealr, or $HOME/.local/share/sealr when XDG_DATA_HOME is unset
macOS$HOME/.local/share/sealr

sealr init and sealr enroll create the skeleton:

<home>/
  config.json                     non-secret configuration; safe to share when debugging
  identity/
    recorder.key                  identity signing key, encrypted-file keystore (0600 on Unix)
    recorder.salt                 per-install keystore salt
    recorder.pem                  this recorder's certificate
    recorder.token                Console bearer credential — secret, never in config.json
    console.pub.pem               Console verifying key, used to check policy bundle signatures
    root.pem                      trusted root, bundled into exports so they verify offline
  spool/
    <stream-id>/
      <segment-id>.ndjson         append-only records, one JCS line each
      checkpoints.log             signed Merkle checkpoints
    sync-state.json               how far the Console has acknowledged
  policy/
    bundle.json                   the cached, Console-signed policy bundle

Environment variables

VariableDefaultWhat it does
SEALR_HOMEplatform default aboveRecorder home directory. --home overrides it.
SEALR_PRINCIPALunsetHuman principal attached to records from sealr run and sealr proxy mcp, producing an attributed subject rather than an inferred one.
SEALR_LOGwarnTracing filter. Logs go to stderr so stdout stays usable for JSON-RPC and hook output.
SEALR_KEYSTORE_PASSPHRASEunsetProtects the identity key at rest with Argon2id over this passphrase. See Keystore.

config.json fields

Written by init and enroll; edit it by hand only when you know why. Unknown fields are rejected — a typo makes the recorder fail to start rather than silently ignore your setting.

FieldTypeDefaultWhat it does
console_endpointstring""Console gRPC endpoint. Empty in offline mode.
tenant_idstring""Tenant assigned at enrollment.
recorder_idstring""This recorder's id. Generated by init, assigned by enroll.
stream_idstring""The stream records are written to. A recorder with an empty stream_id refuses to run and tells you to init or enroll.
environment_labelsstring[][]Labels from enroll --label, attached to records and matchable by policy rules.
sync_enabledboolfalseWhether to talk to the Console at all. Set by init --endpoint and by enroll.
default_failopen or closedopenIntended fail behaviour when a guard cannot decide.
spool_max_bytesnumber2147483648 (2 GiB)Intended spool size cap.
spool_max_hoursnumber24Intended spool age cap.

default_fail, spool_max_bytes and spool_max_hours are parsed and validated, but no code path reads them today. The spool is not capped or aged out by the recorder, and fail behaviour on the enforcement path comes from the policy binding's stale_action and approval_unavailable settings, not from this field. Watch spool growth yourself until these are wired.

Keystore

V1 ships one backend: an encrypted file. The Backend enum names tpm2, secure-enclave and keychain, and the trait boundary exists so they can arrive without a breaking change — but none of them is implemented. sealr doctor and every enrollment record report the active backend and mark the file backend as a downgrade, so the protection level is never misrepresented in evidence.

What protects the key depends on one variable:

SEALR_KEYSTORE_PASSPHRASEDerivationWhat it actually gives you
set, non-emptyArgon2id over the passphrase and the stored salt (19 MiB, t=2, p=1), then a BLAKE3 keystreamAn attacker holding both files still needs the passphrase.
unsetBLAKE3 over the host fingerprint and the stored saltObfuscation, not encryption. The salt sits next to the key file and the host fingerprint comes from public machine attributes, so anyone who can read the key file can recompute the keystream.

The host fingerprint is a BLAKE3 hash of hostname, OS and architecture. On Unix the key and salt are written 0600; on Windows the per-user LOCALAPPDATA ACLs are the baseline and a full ACL hardening pass is still outstanding.

Set the passphrase if the key file might ever be read by someone who is not you:

export SEALR_KEYSTORE_PASSPHRASE='…'
sealr init

It must be present for every later sealr invocation on that home — without it the key will not decode.

Spool durability

The spool is the evidence stream: append-only NDJSON, one canonical (JCS) record per line, one file per boot segment. Recovery reads the last valid line and resumes from its sequence and hash; a torn tail is truncated.

SurfaceDurabilityLoss window on a crash
run --enforce, proxy mcp --enforce, hook pre-tool-usefsync on every writenone — no committed record is lost
run, proxy mcp in observe modebufferedat most the last unflushed batch

Policy cache

policy/bundle.json holds the Console-signed bundle. Before evaluating anything, a connected recorder refreshes it — best-effort: an unreachable Console logs a warning and falls back to the cached bundle rather than stopping the recorder. The bundle's Console signature is verified before installation, and a version older than the cached one is rejected, so a replayed bundle cannot relax your rules.

The cache goes stale after 24 hours without a refresh. In enforce mode a stale cache hands the decision to the binding's stale_action: observe downgrades enforcement to record-only, fail_closed raises it to a block. sealr status reports policy_stale.

Console API

Every variable is validated by Zod at boot; an invalid value fails startup rather than degrading quietly. Nothing is strictly required — every field has a default or is optional — which means a Console will start with development defaults. Setting the production values is on you.

Runtime

VariableDefaultRequiredWhat it does
NODE_ENVproductionnodevelopment, production or test. Defaults to production on purpose: an unset value must not unlock development conveniences.
PORT4000noHTTP port.
GRPC_PORT50051nogRPC port the recorders connect to.
DATABASE_URLpostgres://sealr:sealr@localhost:5432/sealrin productionPostgres connection string. The default is a local development database.

URLs and origins

VariableDefaultRequiredWhat it does
PUBLIC_URLhttp://localhost:4000in productionThe API's own public base URL. Used to build magic-link callbacks.
WEB_URLhttp://localhost:3000in productionConsole web origin. Allowed by CORS with credentials, and where the GitHub install callback redirects.
LANDING_URLhttp://localhost:3001in productionMarketing site origin. Allowed by CORS explicitly rather than by wildcard, because this API is cookie-authenticated.

Signing and trust

VariableDefaultRequiredWhat it does
CONSOLE_SIGNING_KEY_HEXnonein productionEd25519 seed (hex) for the Console signing key: checkpoint countersignatures and policy bundle signatures.
CONSOLE_KEY_IDconsole-dev-1in productionKey id recorded alongside those signatures. Set a real one so key rotation is expressible.
DEV_ROOT_KEY_HEXnonenoRoot key material for the local development CA. Development only — a production root is offline and never in an environment variable.

With no CONSOLE_SIGNING_KEY_HEX, the Console generates an ephemeral key at boot and logs a warning containing the generated seed. Everything it signs becomes unverifiable after a restart, and the seed is now in your logs. Set this before anyone depends on a countersignature.

Authentication

VariableDefaultRequiredWhat it does
SEALR_DEV_AUTHunset (off)noDevelopment only. 1 or true enables the passwordless dev sign-in path: unknown emails self-provision an owner, and the magic link is returned in the HTTP response instead of being emailed.
SESSION_SECRETdev-session-secret-change-menoDeclared and validated. No code path reads it today — sessions are random tokens stored hashed in the database.
OIDC_ISSUERnonenoDeclared. OIDC is not wired up in V1; magic-link is the sign-in path that exists.
OIDC_CLIENT_IDnonenoDeclared, unused.
OIDC_CLIENT_SECRETnonenoDeclared, unused.

SEALR_DEV_AUTH bypasses account verification. It is gated twice — it needs both a non-production NODE_ENV and an explicit SEALR_DEV_AUTH=1 — so neither a missing variable nor a stray NODE_ENV=development unlocks owner self-provisioning on its own. Never set it on a deployment that anyone else can reach.

GitHub App

Two models, and the shared one is what these variables enable. In Model A one public Sealr App serves every tenant: the webhook secret lives here and is never shown to customers, and the install callback is signed so an attacker cannot bind their installation to someone else's tenant. In Model B a customer runs their own App and registers its own per-installation webhook secret — that path works whether or not these are set.

VariableDefaultRequiredWhat it does
GITHUB_APP_WEBHOOK_SECRETnonefor Model AHMAC secret for verifying deliveries from the shared App. Also the key used to sign the install-callback state. Secret material.
GITHUB_APP_SLUGnonefor Model AThe App's URL slug, used to build the one-click install link.
RECONCILE_GRACE_SECONDS300 (5 min)noHow long an observed push waits before it can be reconciled, so a push the recorder is still syncing is not wrongly flagged as a coverage gap. Lower it only in demos and tests where there is no sync lag.

Both GITHUB_APP_WEBHOOK_SECRET and GITHUB_APP_SLUG must be set for the shared App. With either missing, the Console reports that the shared App is not configured and the "Connect GitHub" button is unavailable; Model B still works.

Timestamping and billing

VariableDefaultRequiredWhat it does
TSA_URL""noRFC 3161 timestamp authority endpoint for anchoring. Declared and validated; no code path reads it yet.
TSA_QUALIFIEDfalsenoWhether that TSA is eIDAS-qualified. Declared, unused.
STRIPE_SECRET_KEYnonenoStripe key. Billing degrades gracefully without it.

TSA_URL and TSA_QUALIFIED are configuration surface ahead of the feature. Bundles verify their own hash chain, Merkle roots and signatures regardless; a qualified timestamp is what turns "in this order" into "before this wall-clock time, provably". Do not promise an auditor an anchored timestamp on the strength of these variables alone.

Console web

The Next.js Console reads one variable of its own:

VariableDefaultWhat it does
NEXT_PUBLIC_API_URLhttp://localhost:4000Console API base URL. Baked in at build time, so a production build needs it set at build, not just at runtime.

Next

On this page