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.
| Platform | Default 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 bundleEnvironment variables
| Variable | Default | What it does |
|---|---|---|
SEALR_HOME | platform default above | Recorder home directory. --home overrides it. |
SEALR_PRINCIPAL | unset | Human principal attached to records from sealr run and sealr proxy mcp, producing an attributed subject rather than an inferred one. |
SEALR_LOG | warn | Tracing filter. Logs go to stderr so stdout stays usable for JSON-RPC and hook output. |
SEALR_KEYSTORE_PASSPHRASE | unset | Protects 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.
| Field | Type | Default | What it does |
|---|---|---|---|
console_endpoint | string | "" | Console gRPC endpoint. Empty in offline mode. |
tenant_id | string | "" | Tenant assigned at enrollment. |
recorder_id | string | "" | This recorder's id. Generated by init, assigned by enroll. |
stream_id | string | "" | The stream records are written to. A recorder with an empty stream_id refuses to run and tells you to init or enroll. |
environment_labels | string[] | [] | Labels from enroll --label, attached to records and matchable by policy rules. |
sync_enabled | bool | false | Whether to talk to the Console at all. Set by init --endpoint and by enroll. |
default_fail | open or closed | open | Intended fail behaviour when a guard cannot decide. |
spool_max_bytes | number | 2147483648 (2 GiB) | Intended spool size cap. |
spool_max_hours | number | 24 | Intended 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_PASSPHRASE | Derivation | What it actually gives you |
|---|---|---|
| set, non-empty | Argon2id over the passphrase and the stored salt (19 MiB, t=2, p=1), then a BLAKE3 keystream | An attacker holding both files still needs the passphrase. |
| unset | BLAKE3 over the host fingerprint and the stored salt | Obfuscation, 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 initIt 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.
| Surface | Durability | Loss window on a crash |
|---|---|---|
run --enforce, proxy mcp --enforce, hook pre-tool-use | fsync on every write | none — no committed record is lost |
run, proxy mcp in observe mode | buffered | at 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
| Variable | Default | Required | What it does |
|---|---|---|---|
NODE_ENV | production | no | development, production or test. Defaults to production on purpose: an unset value must not unlock development conveniences. |
PORT | 4000 | no | HTTP port. |
GRPC_PORT | 50051 | no | gRPC port the recorders connect to. |
DATABASE_URL | postgres://sealr:sealr@localhost:5432/sealr | in production | Postgres connection string. The default is a local development database. |
URLs and origins
| Variable | Default | Required | What it does |
|---|---|---|---|
PUBLIC_URL | http://localhost:4000 | in production | The API's own public base URL. Used to build magic-link callbacks. |
WEB_URL | http://localhost:3000 | in production | Console web origin. Allowed by CORS with credentials, and where the GitHub install callback redirects. |
LANDING_URL | http://localhost:3001 | in production | Marketing site origin. Allowed by CORS explicitly rather than by wildcard, because this API is cookie-authenticated. |
Signing and trust
| Variable | Default | Required | What it does |
|---|---|---|---|
CONSOLE_SIGNING_KEY_HEX | none | in production | Ed25519 seed (hex) for the Console signing key: checkpoint countersignatures and policy bundle signatures. |
CONSOLE_KEY_ID | console-dev-1 | in production | Key id recorded alongside those signatures. Set a real one so key rotation is expressible. |
DEV_ROOT_KEY_HEX | none | no | Root 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
| Variable | Default | Required | What it does |
|---|---|---|---|
SEALR_DEV_AUTH | unset (off) | no | Development 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_SECRET | dev-session-secret-change-me | no | Declared and validated. No code path reads it today — sessions are random tokens stored hashed in the database. |
OIDC_ISSUER | none | no | Declared. OIDC is not wired up in V1; magic-link is the sign-in path that exists. |
OIDC_CLIENT_ID | none | no | Declared, unused. |
OIDC_CLIENT_SECRET | none | no | Declared, 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.
| Variable | Default | Required | What it does |
|---|---|---|---|
GITHUB_APP_WEBHOOK_SECRET | none | for Model A | HMAC secret for verifying deliveries from the shared App. Also the key used to sign the install-callback state. Secret material. |
GITHUB_APP_SLUG | none | for Model A | The App's URL slug, used to build the one-click install link. |
RECONCILE_GRACE_SECONDS | 300 (5 min) | no | How 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
| Variable | Default | Required | What it does |
|---|---|---|---|
TSA_URL | "" | no | RFC 3161 timestamp authority endpoint for anchoring. Declared and validated; no code path reads it yet. |
TSA_QUALIFIED | false | no | Whether that TSA is eIDAS-qualified. Declared, unused. |
STRIPE_SECRET_KEY | none | no | Stripe 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:
| Variable | Default | What it does |
|---|---|---|
NEXT_PUBLIC_API_URL | http://localhost:4000 | Console API base URL. Baked in at build time, so a production build needs it set at build, not just at runtime. |