Admin: settings #
Summary #
The last public settings in config.yml move to the admin area: the app name, the web session lifetime, the turn limits and system prompt, and background sync. Each group is a zod schema in the server, stored in the settings store from [[admin]], and edited through a form generated from the schema, the same way plugin options are in [[admin-plugins]]. Changes apply without a restart. Settings the server needs before the database is open move to environment variables. config.yml, config.example.yml, and the ${NAME} references are removed, and nothing reads the file any more.
Motivation #
Once roles, plugins, and models live in the database, the few settings left in config.yml would make it a second place to look. Dropping the file completes the rule that each setting lives in exactly one place: the environment for what the server needs to boot, and the database for everything an admin changes.
Deployment gets simpler too. A new server needs .env and nothing else, and the Docker setup no longer mounts a config file.
Design #
Where each setting goes #
Old config.yml setting |
Now | Why |
|---|---|---|
app.name |
general.appName |
Admin setting |
app.publicUrl |
PUBLIC_URL |
Part of the OAuth client ID, needed before anything else |
app.port |
PORT |
Needed to listen |
app.dataDir |
DATA_DIR |
Needed before the database opens |
app.logLevel |
LOG_LEVEL |
The logger starts before the database |
app.allowPrivateNetworks |
ALLOW_PRIVATE_NETWORKS |
Network policy of the deployment, fixed when the fetchers are built |
auth.scopeMode |
OAUTH_SCOPE_MODE |
The OAuth client is built at startup |
auth.sessionTtlDays |
sessions.ttlDays |
Admin setting |
auth.plcUrl |
PLC_URL |
The identity resolver is built at startup |
turns.ratePerMinute, maxSteps, timeoutSeconds, backfillMinutes, systemPrompt |
turns.* |
Admin settings |
sync.* |
sync.* |
Admin settings |
roles |
Database | [[admin]] |
plugins, models |
Database | [[admin-plugins]] |
The environment variables that used to override admin settings, APP_NAME, SESSION_TTL_DAYS, TURN_RATE_PER_MINUTE, TURN_MAX_STEPS, TURN_TIMEOUT_SECONDS, and TURN_BACKFILL_MINUTES, are removed. So is SCN_CHAT_CONFIG, and the provider key variables in .env.example, since admin keys are now secret plugin options.
Setting groups #
Each group is a key in the settings store, with a zod schema in apps/server/src/settings/schemas.ts. Fields carry .meta({ title, description }) for the generated form, and a number's .meta({ unit }) names what it counts, such as minutes, shown after its field. The defaults match today's.
| Key | Fields |
|---|---|
general |
appName (default SCN Chat), description (default the login page's tagline) |
sessions |
ttlDays (1 or more, default 30) |
turns |
ratePerMinute (default 10), maxSteps (default 8), timeoutSeconds (default 600), backfillMinutes (default 60), systemPrompt (multi-line, default today's DEFAULT_SYSTEM_PROMPT, placeholders unchanged) |
sync |
safetyNet (enabled, intervalMinutes, activeWithinHours), discovery (intervalMinutes, activeWithinDays), allowUserOptOut |
Applying changes #
Consumers read the current value from the settings store when they need it, and never copy it at startup:
- App name. The page title the server renders, the OAuth client metadata's
client_name, the{{appName}}placeholder, andctx.app.nameall read it per use. - Description. The login page's tagline, and the description and link preview tags the server renders, read it per use.
- Session lifetime. Applies to sessions created or extended after the change. Existing expiry times stay as they are.
- Turns. The rate limiter reads the rate on each check. A turn reads the step limit, timeout, and system prompt when it starts. The sync engine reads the backfill window per sync.
- Sync. The scheduler subscribes to
syncand restarts its timers with the new intervals.allowUserOptOutis read per request.
Environment #
After this spec, .env holds everything outside the database:
| Variable | Default | Purpose |
|---|---|---|
NODE_ENV |
development |
|
DATABASE_URL |
sqlite:./data/scn-chat.sqlite |
|
SECRET_KEY, SECRET_KEYS_OLD |
Encryption, as today | |
OAUTH_PRIVATE_KEYS |
Required in production, unless PUBLIC_URL is loopback |
|
ADMIN_DIDS |
Required, from [[admin]] | |
PUBLIC_URL |
http://127.0.0.1:3000 |
Must be HTTPS or loopback in production |
PORT |
3000 |
|
DATA_DIR |
./data |
|
LOG_LEVEL |
info |
|
ALLOW_PRIVATE_NETWORKS |
false |
|
OAUTH_SCOPE_MODE |
permission-set |
Production refuses raw |
PLC_URL |
https://plc.directory |
apps/server/src/config.ts validates only the environment. readConfigFile, interpolate, and the yaml dependency are removed.
Admin API and area #
| Route | Purpose |
|---|---|
GET /api/admin/settings |
Every group, with its JSON schema and current value. |
PUT /api/admin/settings/:key |
Save one group. Failures return 400 with { error: 'InvalidRequest', issues }. |
Three sections join the admin sidebar: General, with the app name and session lifetime, Turns, and Sync. Each renders its groups with SchemaFields and has one Save. Each route picks its cards: a whole group, some of a group's fields under a title of its own, such as Turns' Limits and Instructions, or one nested object, such as Sync's safety net, under its own title and description. The system prompt is a textarea, which SchemaFields renders for string fields marked .meta({ multiline: true }).
Removing config.yml #
- Delete
config.example.yml, and removeconfig.ymlfrom.gitignore,.dockerignore,docker-compose.yml, and the docs. - Update
.env.examplewith the variables above, grouped and commented. - Update
CLAUDE.md: the local setup, the fork notes that nameconfig.yml, and the architecture decisions on admin configuration and the admin UI. - Update the specs that describe
config.yml: foundation, auth, plugins, providers, chat-storage, chat-turns, titles, web-search, and web-ui. Each points at the admin area or the environment instead. - Update
docs/architecture.md,docs/plugins.md,docs/deployment.md, the rootREADME.md, and the web search README, whose example usesconfig.yml.
Upgrading an existing server #
There is no import. An existing server upgrades by:
- Moving the settings in the environment table above from
config.ymlto.env, and addingADMIN_DIDS. - Starting the new version and signing in as an admin.
- Adding the plugin instances and models again, and setting access, roles, and the other settings.
Provider IDs appear in the model references stored in users' records, so each provider must be added back with the same ID it had, such as an OpenAI-compatible instance with id: scn. User settings and tool switches are keyed by plugin ID and are picked up again once the same plugins are added. docs/deployment.md describes these steps.
Scope Boundaries #
- No import of an existing
config.yml. - No settings export or import.
- No environment overrides for admin settings.
- No change to what the settings do, only where they live.
Edge Cases and Decisions #
- Every setting lives in either the environment or the database, never both, so nothing is silently ignored the way Open WebUI's persistent config is.
ALLOW_PRIVATE_NETWORKSstays in the environment. It decides what the server's fetchers may reach, which is a property of the deployment, not something to change from a browser.- Session lifetime changes don't touch existing sessions.
config.ymlis dropped from.gitignore, so a leftover local file shows up ingit statusto be deleted.
Acceptance Criteria #
Files #
apps/server/src/config.ts,apps/server/src/index.ts,apps/server/src/server.tsapps/server/src/settings/schemas.ts,apps/server/src/admin/routes.tsapps/server/src/auth/web-session.ts,apps/server/src/auth/oauth-client.tsapps/server/src/turns/runner.ts,apps/server/src/turns/prompt.ts,apps/server/src/sync/scheduler.ts,apps/server/src/sync/engine.tsapps/server/src/app.ts, which passes the app name to the web app with each requestapps/web/src/routes/_app/admin/general.tsx,turns.tsx,sync.tsx,apps/web/src/features/admin/pages/SettingsAdmin.tsx,apps/web/src/shared/schema-fields/SchemaFields.tsx.env.example,.gitignore,.dockerignore,docker-compose.yml,config.example.yml(deleted)CLAUDE.md,README.md,docs/,specs/,plugins/web-search/README.md