Configuration
Boot configuration, platform settings, and model providers.
Configuration follows one discipline: the instance should be governable from
the admin UI at runtime, with as little reason to touch a server as possible.
Neuralis configuration is split into four tiers with a clear ownership rule:
.env holds only what the process needs to boot, the platform config store
holds runtime settings an admin edits from the UI, long-lived secrets live
exclusively in the encrypted credential store, and the Docker topology lives
in a setup-generated docker-compose.yml (see
deployment). For runtime values the
precedence is: stored platform value, then environment fallback, then the
built-in default.
Tier 1: .env — boot-critical only
The environment file is written by pnpm neuralis:setup and read at process
start. It contains nothing a user would reasonably edit at runtime; changing
it requires a restart. The important variables:
| Variable | Purpose |
|---|---|
NEXTAUTH_SECRET | Session encryption secret. Required; generated by setup. |
NEXTAUTH_URL | Public app URL (defaults to http://localhost:3100). |
NEURALIS_APP_PORT / MCP_HTTP_PORT | App and external-MCP ports (3100 / 3101). |
NEURALIS_HOME | Data root (in-container: /.neuralis). |
NEURALIS_HOST_HOME | Host-side pair of the data root, required in Docker so file metadata carries host paths instead of container paths. |
QDRANT_URL | Vector database endpoint. |
QDRANT_API_KEY | The vector-database API key. Setup writes it: minted automatically for the compose-managed and binary modes (and preserved across re-runs), asked for when you point at an external instance — leave it empty only for an external, unauthenticated one. The compose-managed Qdrant ENFORCES the key on every request; the compose file hands it over by interpolation, so the key itself lives only in .env. It lives there rather than in the credential store because the vector client is constructed synchronously at boot, before any credential can be decrypted. The running platform sends it on every vector-database call — memory and sync, the admin panel's health probe, vector status page and vector reset — pnpm reset:vector, which lists and drops collections, and pnpm neuralis:qdrant-upgrade, which hands it to each upgrade step through a private file, never on a command line. The setup-time reachability probe deliberately does not send it: it shares its HTTP helper with the Ollama probe, so an unconditional key there would leak the key to a different service. It calls only the health and root endpoints, which Qdrant's own auth whitelist leaves unauthenticated. A key mismatch is not a boot failure: the platform falls back to in-memory vectors and logs a structured unauthorized line until the key matches. |
NEURALIS_CODEX_LOOPBACK | Docker only: on publishes the ChatGPT sign-in callback on the host's 127.0.0.1:1455, so a browser on the Docker host can finish the sign-in directly (off by default). Setup asks it on a Docker install; to change it later, re-run setup or edit .env and regenerate the compose. While the stack runs, Docker then holds that host port. |
QDRANT_DASHBOARD | Switch for Qdrant's static dashboard UI on the loopback port (on / off, default on). Setting off and regenerating the compose disables the static UI entirely; the API is unaffected. |
NEURALIS_BUILTINS | Optional comma-separated list of full package ids to load instead of the discovered builtin set (slim deployments). |
NEURALIS_BUILD_NPMRC | Optional path to an .npmrc for a private package registry, on an install that builds its own image: scope lines only (@yourco:registry=<url> and its token line, never a default registry=). The image build mounts it as a build secret, never a layer; regenerating the compose refuses a missing file or a default registry line. See your own packages. |
Tier 2: platform config store
Runtime platform settings live in a single JSON file in the platform zone
(~/.neuralis/app/config/platform.json) behind a schema-validated store, and
are edited from the admin widget's Config tab. Every key has a type, a
default, and an editable flag; read-only keys surface in the UI as
informational rows. Examples of what lives here:
- the target embedding model id and vector dimension (the model the vectors are actually built with stays the active one until an owner rebuilds — see memory and sync),
- custom LLM endpoints (your own hardware, or a keyed remote gateway) (Ollama-native or OpenAI-compatible),
- custom embedding endpoints — any OpenAI-compatible
/v1/embeddingsserver, edited in the admin Vector section; like the LLM endpoints, their keys live in the credential store, never in this file, - how many data checkpoints to keep (
dataCheckpointKeep, see deployment), - runtime knobs such as the per-stream agent step cap and default temperature,
- the chat composer clipboard caps — the maximum size of a pasted/dropped
image and of pasted text (client-side paste guards only; they do not affect
how much data a tool like
fs_readreturns), - platform limits such as the maximum number of projects.
Every key is declared by the package that consumes it (the manifest
configSettings[] array — the host hardcodes no schema of its own), and the
Config tab shows each key's declaring package, whether its current value comes
from the default, an environment fallback, or an admin-set override, and any
number bounds. The list can be filtered by that declaring package — one entry
per package that declares keys, plus the host's own settings and the read-only
environment rows. Only first-party packages can contribute keys: the host honors
a configSettings[] declaration from a package it trusts by source (one of its
own dependencies, whatever the npm scope), and skips one from a project-dropped
or synced package, because an environment fallback would otherwise project an
environment variable's value into the dashboard. Boot-critical environment values (ports, auth URLs, the vector
database endpoint) appear in the same list as read-only rows served from an
explicit host allowlist — secret values are never listed. Where a
boot-critical variable is itself a secret, only its presence is reported: the
vector database API key renders as (set) or (unset), never as the key.
Edits are validated against the declared schema (type + bounds), written atomically, and most take effect at runtime — no restart; keys that apply on restart or at client rebuild say so in their description. Many keys keep an environment fallback for first boot, but once a value is stored the stored value wins.
Tier 3: credential store
Anything secret — provider API keys, OAuth tokens, MCP API keys,
skill-declared and custom secrets — belongs in the encrypted credential
store, never in .env or platform config. The store is scope-aware
(platform, user, project, agent) and managed from the admin Credentials tab.
The full model is on the credentials page.
Model provider setup
LLM provider keys are credentials, not environment variables:
- During first-run setup, the wizard asks for provider keys interactively
and writes them straight into the encrypted store. The runtime reads
provider keys from the store only — there is no environment fallback, and
the compose file carries no key entries (anything in a container's
environment is readable via
docker inspect). - After that, manage keys from the admin Credentials tab. A key set at any scope is honored at stream time through most-specific-wins resolution (agent → project → user → global), so a project can carry its own provider key that overrides the platform default.
- Custom endpoints carry their own authentication: register an Ollama or OpenAI-compatible server in the admin Credentials tab and say whether it needs a key. One on your own hardware usually does not, and its models appear in the selector alongside the hosted providers. One that DOES — a remote gateway, say — stores its key like any other credential, resolved with the same scope cascade, and its models are offered only to callers for whom that key resolves. A row nobody has a key for is greyed out rather than failing mid-conversation, and the endpoints beside it are unaffected.
ChatGPT/Codex OAuth sign-in is the one special case: there is no key to paste, so it is connected through a sign-in flow from the Credentials tab (or a member's own Connect button in chat). The resulting login is stored like any other credential, at agent, project, user or global scope, and resolves through the same cascade. See providers and models for the provider catalog itself.
Tier 4: the generated docker-compose.yml
Docker deployments have one more setup-owned artifact: the compose file
itself. pnpm neuralis:setup generates it next to .env with concrete
machine-specific values (user/group IDs, ports, the Qdrant service or a
direct QDRANT_URL depending on the chosen mode, an optional local Ollama
service); pnpm neuralis:setup --compose-only regenerates it without
re-asking anything. It is never hand-edited, and it never carries secrets —
the single .env interpolation is NEXTAUTH_SECRET. Machine-local extra
mounts live in a separate docker-compose.override.yml that survives
regeneration. Details on the deployment page.
Logs: two planes, and the keys that level them
Neuralis writes logs to two places that are easy to confuse, and one level key
(logLevel) behaves differently from all the others.
| Docker / console | Files on disk | |
|---|---|---|
| Where | container stdout (docker logs) | first-party package logs app/data/<package>/logs/*.jsonl, project-installed package logs projects/<id>/data/_installed/<slug>/logs/, per-agent run logs, plus the host's app/logs/audit.jsonl and app/logs/routes.jsonl |
| Lifetime | until the container restarts — nothing persists | rotated by size and age, survives restarts |
| Level key | logLevel (plus routeConsoleLogLevel for the route lines) | packageLogLevel (+ packageLogLevelOverrides) and routeLogLevel |
| Takes effect | on restart | live, on the next line written |
| Readable in-product | no | yes — the admin Logs tab, and agent skills for the per-package files |
packageLogLevel(defaultinfo) governs every package's own file log.packageLogLevelOverridessets exceptions per package as<package>=<level>pairs (brain-core=debug,agent-core=warn), keyed on thepkgfield of the log line; a malformed pair is ignored and that package follows the global.routeLogLevel(defaultwarn) governs the route log — one structured line per package-route dispatch: matched pattern, method, status, duration, caller, and which gate refused a call. It defaults towarnbecause several App surfaces poll every few seconds, so atinfoa single open shell tab writes tens of thousands of lines a day and the file rotates away within hours. Raise it for a debugging window, then put it back. Atwarnthe file holds refusals, faults and every dispatch slower thanrouteSlowMs(default 1000 ms, markedslow: truewhatever its status). The same lines are also written to container stdout through their own gate,routeConsoleLogLevel: empty (the default) followsrouteLogLevel, sodocker logsshows the same refusals, faults and slow requests; set it towarnwithrouteLogLevel=infoto keep every request in the file and only the problems on stdout.logLevelgoverns only container stdout. It changes nothing on disk.- The process monitor writes one
perf.slowwarning to container stdout when, over a sample window (perfSampleIntervalMs, default 30 s, applies on restart), the Neuralis process's CPU exceedsperfProcessCpuWarnPercent(default 80; 100 = one core, the Neuralis process only — not the container) or its longest single event-loop stall reachesperfEventLoopLagWarnMs(default 200 ms). Below the thresholds it writes nothing.
Because the file levels apply live, the working loop is: raise the level, reproduce the problem, read, put it back. Raising a level never recovers lines that were not written at the time.
Retention is what the rotation keys buy, not a time promise. Each plane has its
OWN trio so neither is rotated on a budget set for the other: routeLogMaxBytes
x routeLogMaxFiles x routeLogMaxAgeDays (25 MB x 5 x 90 days) for the route
log, and packageLogMaxBytes / MaxFiles / MaxAgeDays for package logs. The
audit log is the exception: auditLogMaxBytes (25 MB) only decides when
audit.jsonl is renamed to a new generation — no generation is ever deleted.
What is hot and what needs a restart
| Change | Takes effect |
|---|---|
| Platform config edits (Config tab) | At runtime, on the next read — except the keys whose own description says they apply on restart (or at client rebuild), which are read once as the service starts |
packageLogLevel / packageLogLevelOverrides / routeLogLevel / routeConsoleLogLevel / routeSlowMs / perfSlowToolMs | Live, on the next log line — no restart |
logLevel (Docker/console) | Process restart |
| Credential writes and deletions | Immediately — caches are invalidated on every store mutation, so a rotated key applies to the next stream |
| Role and feature grant changes | Immediately, server-side on the next request |
.env changes (ports, URLs, secrets, data root) | Process restart |
| New or upgraded packages | Container/image update for builtin-class packages; rescan for project packages |
The configuration UI and the routes behind it are feature-gated
(platform.config — a platform-tier feature with no default role grant); see
administration for who can edit what.