--- title: Configuration description: "enpilink's typed config layer — env > file > DB precedence, editability tiers (runtime / restart / read-only), the Configuration admin tab, reset-to-default, Dev/Prod presets, env-only masked secrets, and the config audit log." --- enpilink has a single, typed configuration layer for the admin / control plane. It resolves every setting through a clear precedence chain, classifies each setting by how it can be edited (**runtime**, **restart**, or **read-only**), keeps **secrets out of the database**, and **audit-logs** every change. The Configuration tab groups settings into friendly functional sections and offers **reset-to-default** and one-click **Dev/Prod presets**. ## Precedence: env > file > DB > default Each setting is resolved from the first source that provides it: 1. **Environment variable** — highest priority. An env value always wins and **locks** the setting (it can't be overridden in the UI). 2. **Config file** — `enpilink.config.json` in the project root. 3. **Database** — the runtime values written from the Configuration admin tab. 4. **Built-in default** — the schema's default, used when nothing else sets it. This means an operator can pin any value via env or file and it becomes read-only ("env-locked") in the admin UI, while everything else stays editable at runtime. The resolver is failure-tolerant: with no storage it returns defaults; if the DB or a file value is bad, it falls back per-key rather than crashing a read. A config read never throws. The default storage engine is **`sqlite`** (a durable local `enpilink.db`) in both dev and production, so changes you make from the Configuration tab during `enpilink dev` — for example enabling analytics — **persist across restarts** and are read back from the database on the next boot. Set `ENPILINK_STORAGE=memory` for an ephemeral, file-less store (and `enpilink dev --mock` always uses an ephemeral in-memory store). The `enpilink.db` file is gitignored by the scaffold. ## Editability tiers Every key is classified by how it can be edited, surfaced as `editable` on each resolved setting: - **`runtime`** — editable live in the Configuration tab; takes effect immediately. These live in the DB (`analytics.*`, `retention.*`, `flags.*`, `display.*`). - **`restart`** — DB-editable, but the change only takes effect after a process **restart**. These are the non-secret end-user-auth keys (`auth.enabled`, `auth.issuer`, `auth.audience`, `auth.jwksUrl`, `auth.upstream.*`, `auth.redirectUris`, `auth.branding.*`). The running process keeps using the value it booted with; the UI shows a **"requires restart"** badge and, once you change one, a "takes effect after restart" note. - **`readonly`** — environment-only, never web-editable. These are the `admin` gate, the secrets (`adminAuthToken`, `auth.signingKey`, `auth.clientSecret`, `agent.ingestToken`), and the **startup-only** keys `port`, `storage`, and `dbPath`. All tiers still honour env > file > DB precedence, so pinning a `runtime` or `restart` key via env/file makes it **env-locked** (read-only in the UI). **`port`, `storage`, and `dbPath` are deploy/env concerns — not shown in the Configuration tab.** They are read at boot from the environment to bind the port and *open* the database, so a value persisted to the DB could never be honoured on the next boot (the storage engine + path must be known before the DB is opened — a chicken-and-egg). Set them via `PORT` / `ENPILINK_STORAGE` / `ENPILINK_DB_PATH` — see [Storage](/guides/storage) and [Deploy](/quickstart/deploy). The config API still returns them (flagged `hidden: true`) for introspection, but the dashboard hides them and the API rejects any write to them (403). | Key | Env var | Default | Tier | Group | Meaning | |---|---|---|---|---|---| | `analytics.enabled` | `ENPILINK_ANALYTICS` | `false` | runtime | Analytics | Capture analytics events | | `analytics.sampleRate` | `ENPILINK_CFG_ANALYTICS_SAMPLE_RATE` | `1` | runtime | Analytics | Fraction of requests sampled `[0,1]` | | `retention.events` | `ENPILINK_CFG_RETENTION_EVENTS` | `5000` | runtime | Retention | Max events retained | | `retention.logs` | `ENPILINK_CFG_RETENTION_LOGS` | `5000` | runtime | Retention | Max logs retained | | `flags.liveLogs` | `ENPILINK_CFG_FLAGS_LIVE_LOGS` | `true` | runtime | Features | Expose the live-log stream | | `display.bucketMs` | `ENPILINK_CFG_DISPLAY_BUCKET_MS` | `60000` | runtime | Display | Dashboard time-bucket width (ms) | | `agent.enabled` | `ENPILINK_AGENT` | `false` | runtime | Agent | Capture agent HTTP requests (fingerprint + detection) | | `agent.sampleRate` | `ENPILINK_CFG_AGENT_SAMPLE_RATE` | `1` | runtime | Agent | Fraction of requests captured `[0,1]` | | `agent.retentionDays` | `ENPILINK_CFG_AGENT_RETENTION_DAYS` | `30` | runtime | Agent | Days to keep captured requests (`0` = forever); enforced by the pruner | | `agent.verifyIpRanges` | `ENPILINK_CFG_AGENT_VERIFY_IP_RANGES` | `false` | runtime | Agent | Optional IP-range verification confidence tier | | `agent.serve` | `ENPILINK_CFG_AGENT_SERVE` | `false` | runtime | Agent | Serve the self-sufficient representation to eligible chat fetchers | | `agent.site.title` | `ENPILINK_CFG_AGENT_SITE_TITLE` | `""` | runtime | Agent | Site title in the representation (overrides `describeForAgents`) | | `agent.site.description` | `ENPILINK_CFG_AGENT_SITE_DESCRIPTION` | `""` | runtime | Agent | Site description in the representation | | `agent.spa` | `ENPILINK_CFG_AGENT_SPA` | `false` | runtime | Agent | Replace a client-rendered SPA's `200` shell with the representation | | `agent.reencode` | `ENPILINK_CFG_AGENT_REENCODE` | `false` | runtime | Agent | Re-encode a real route's HTML response to markdown | | `agent.getTransport` | `ENPILINK_CFG_AGENT_GET_TRANSPORT` | `false` | runtime | Agent | Mount the plain-GET transport (off by default; [unproven-in-use](/api-reference/get-transport)) | | `agent.getRateLimit` | `ENPILINK_CFG_AGENT_GET_RATE_LIMIT` | `60` | runtime | Agent | GET transport default rate (requests/min per IP+tool) | | `agent.getRateBurst` | `ENPILINK_CFG_AGENT_GET_RATE_BURST` | `10` | runtime | Agent | GET transport default burst (per IP+tool) | | `auth.enabled` | `ENPILINK_AUTH` | `false` | restart | Security | Enable end-user OAuth on `/mcp` (restart to apply) | | `auth.issuer` | `ENPILINK_AUTH_ISSUER` | — | restart | Security | Authorization server issuer URL | | `auth.audience` | `ENPILINK_AUTH_AUDIENCE` | — | restart | Security | RFC 8707 audience (`aud`) tokens must carry | | `auth.jwksUrl` | `ENPILINK_AUTH_JWKS_URL` | — | restart | Security | JWKS URL for token-signature verification | | `auth.upstream.clientId` | `ENPILINK_AUTH_UPSTREAM_CLIENT_ID` | — | restart | Security | Upstream IdP OAuth client id (co-hosted AS) | | `auth.upstream.authorizationUrl` | `ENPILINK_AUTH_UPSTREAM_AUTHORIZATION_URL` | — | restart | Security | Upstream authorize endpoint | | `auth.upstream.tokenUrl` | `ENPILINK_AUTH_UPSTREAM_TOKEN_URL` | — | restart | Security | Upstream token endpoint | | `auth.upstream.revocationUrl` | `ENPILINK_AUTH_UPSTREAM_REVOCATION_URL` | — | restart | Security | Optional upstream revocation endpoint | | `auth.upstream.scopes` | `ENPILINK_AUTH_UPSTREAM_SCOPES` | — | restart | Security | Space-delimited scopes requested upstream | | `auth.redirectUris` | `ENPILINK_AUTH_REDIRECT_URIS` | — | restart | Security | Host callback URLs allowed as redirect targets | | `auth.branding.appName` | `ENPILINK_AUTH_BRANDING_APP_NAME` | — | restart | Security | Login-page app name | | `auth.branding.logoUrl` | `ENPILINK_AUTH_BRANDING_LOGO_URL` | — | restart | Security | Login-page logo image URL | | `auth.branding.accentColor` | `ENPILINK_AUTH_BRANDING_ACCENT_COLOR` | — | restart | Security | Login-page accent color (CSS hex) | | `auth.branding.tagline` | `ENPILINK_AUTH_BRANDING_TAGLINE` | — | restart | Security | Login-page tagline | | `admin` | `ENPILINK_ADMIN` | `false` | readonly | Security | Enable the prod admin plane | | `adminAuthToken` | `ENPILINK_ADMIN_TOKEN` | — | readonly | Security | **Secret** — admin bearer token | | `auth.signingKey` | `ENPILINK_AUTH_SIGNING_KEY` | — | readonly | Security | **Secret** — federating AS token signing key (enables guest mode) | | `auth.clientSecret` | `ENPILINK_AUTH_CLIENT_SECRET` | — | readonly | Security | **Secret** — upstream IdP client secret | | `agent.ingestToken` | `ENPILINK_AGENT_INGEST_TOKEN` | — | readonly | Agent | **Secret** — guards the [beacon ingest sink](/api-reference/next-middleware); when unset the endpoint is disabled (404) | Runtime keys use the `ENPILINK_CFG_*` convention so pinning one via env never collides with the bootstrap vars. The `agent.*` runtime keys are documented in full — what each serves, its topology and the honest caveats — in [Agent analytics](/guides/agent-analytics). They live in the DB and toggle live from the Configuration tab, except the secret `agent.ingestToken`, which is env-only like the other secrets below. The startup-only keys below are **env-only and hidden from the Configuration tab** (the DB can never honour them — see the note above). They're configured at deploy time: | Key | Env var | Default | Where to set it | |---|---|---|---| | `port` | `PORT` (or `__PORT`) | `3000` | [Deploy](/quickstart/deploy) — the hosting platform usually assigns it | | `storage` | `ENPILINK_STORAGE` | `sqlite` | [Storage](/guides/storage) — `sqlite` (default, durable `enpilink.db`), `memory`, `postgres`, or custom | | `dbPath` | `ENPILINK_DB_PATH` | `./enpilink.db` | [Storage](/guides/storage) — SQLite database file path | ## Per-key metadata Each resolved setting carries presentation metadata so the UI can render it clearly: a human **`label`**, a plain-language **`description`**, a functional **`group`**, an optional **`unit`** (e.g. `ms`, `events`, `0–1 ratio`), the schema **`default`**, the **`editable`** tier, plus runtime flags: **`source`** (`env`/`file`/`db`/`default`), **`secret`**, **`envLocked`**, **`modified`** (the effective value is a DB override differing from the default), and **`restartRequired`** (a `restart` key whose persisted value differs from the value the process booted with — a pending change awaiting a restart). ## The Configuration admin tab The admin UI's **Configuration** tab groups settings into friendly sections (Analytics, Retention, Features, Display, Security) rather than a raw bootstrap/runtime split. Each row shows the human label, the raw key, the description + unit hint, a source badge, and a **"modified"** badge when changed. The startup-only `port`/`storage`/`dbPath` keys are **not shown** (they're deploy/env concerns — see the note above). - **Runtime keys** — booleans render as switches, numbers/strings as inputs (saved on blur/Enter); each change is written to the DB and takes effect immediately. - **Restart keys** — editable inputs with a **"requires restart"** badge; once changed they show a "takes effect after a restart" note. - **Read-only keys** — secrets and `admin`, plus any env/file-pinned key, render read-only with a source badge and a "set via ``" hint. - **Reset** — each modified key has a reset affordance, and a **"Reset all to defaults"** action clears every DB override (with a confirm). - **Presets** — one-click **Dev** / **Prod** buttons (see below). - **Change history** — the config audit trail (see below). Writes are validated against the schema and rejected with a clear status: unknown key → 404, secret / `admin` / startup-only (`port`/`storage`/`dbPath`) → 403, env-locked → 409, invalid value → 400, no storage → 409. ## Reset to default `DELETE /__enpilink/config/:key` clears a key's DB override and audit-logs the reset. After clearing, resolution falls back to file/env/default. The same guardrails as a write apply (only `runtime` and non-env-locked `restart` keys; secrets, `admin`, the startup-only `port`/`storage`/`dbPath` keys, unknown, and env-locked keys are rejected). The storage layer backs this with `clearConfig(key, actor?)`. ## Presets Two built-in presets bundle runtime-key changes into one action. Presets **only touch runtime keys** — never secrets, never `admin`, never restart-tier or startup-only bootstrap keys — and **skip any key that is env/file-pinned**, reporting which were applied vs skipped. | Preset | What it sets | |---|---| | **Dev** | analytics on, full sampling (`1`), retention `5000`/`5000`, live logs **on**, 1-minute chart buckets — maximum visibility for local development. | | **Prod** | analytics on but sampled to **`0.25`**, retention `20000`/`20000`, live logs **off**, 5-minute chart buckets — lower overhead and more history for busy servers. | - `GET /__enpilink/config/presets` — list presets and the values each would set. - `POST /__enpilink/config/preset/:name` — apply a preset (`dev` / `prod`); returns a summary of `applied` vs `skipped` keys. ## Secrets are env-only and masked Secret keys (`adminAuthToken`, `auth.signingKey`, `auth.clientSecret`, `agent.ingestToken`) are **never persisted to the database and never returned in plaintext** by any API. In the UI they render as masked (`••••••••` when set, "(not set)" otherwise) with a secret badge and are read-only — the only way to set a secret is its environment variable (`ENPILINK_ADMIN_TOKEN`, `ENPILINK_AUTH_SIGNING_KEY`, `ENPILINK_AUTH_CLIENT_SECRET`, or `ENPILINK_AGENT_INGEST_TOKEN`). The config API returns `403` on any attempt to write a secret key. The DB layer never reads or stores them. ## The config audit log Every config write appends a `config_audit` row capturing the timestamp, the key, the **old and new values**, and the **actor** who made the change (from the `x-enpilink-actor` request header, defaulting to `dev`). The Configuration tab's change-history list surfaces these most-recent-first so you can see who changed what and when. ## Related - [Storage](/guides/storage) — the adapters that back config + audit - [Observability](/guides/observability) — what `analytics.*` and `flags.liveLogs` control - [Agent analytics](/guides/agent-analytics) — what the `agent.*` keys control - [Admin](/guides/admin) — the bootstrap admin token and prod auth