Configuration
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
Pia Server uses standard ASP.NET Core configuration: layered sources with environment variables overriding appsettings.json. Sensitive values (secrets, master key) must come from environment variables in production.
Source order (lowest precedence first)
Section titled “Source order (lowest precedence first)”appsettings.jsonappsettings.{Environment}.json(e.g.appsettings.Production.json)- Environment variables
- Command-line arguments
Sections
Section titled “Sections”Database
Section titled “Database”| Key | Env | Purpose |
|---|---|---|
Database:Provider |
Database__Provider |
sqlite (default in dev) or postgresql |
Database:ConnectionString |
Database__ConnectionString |
EF Core connection string for the chosen provider — the role every request uses |
Database:MigrationConnectionString |
Database__MigrationConnectionString |
Owner role. Read only by the --migrate-only entry point and by local development; production leaves it unset on the server so the application process holds no owner credential |
Database:MaintenanceConnectionString |
Database__MaintenanceConnectionString |
Bypass role for the admin roll-up and the retention sweep, which must read every user’s rows |
PostgreSQL example:
Host=postgres;Database=pia;Username=pia;Password=<db-password>Migrations are applied on startup only when the process has a credential that can run DDL: always under SQLite, and under PostgreSQL only when Database:MigrationConnectionString is set. Production leaves it unset and runs dotnet Pia.Server.dll --migrate-only from a one-shot container instead; the server then asserts at boot that no migration is pending and refuses to start with PIA-DB-001 if one is.
Database:MaintenanceConnectionString enables the PostgreSQL database role split, which puts row-level security on assignments and assignment_events. Leave it unset and the split is not adopted: everything runs on Database:ConnectionString exactly as before.
| Key | Env | Purpose |
|---|---|---|
Jwt:SecretKey |
JWT_SECRET_KEY |
HMAC signing key, ≥ 32 chars |
Jwt:Issuer |
— | Default pia-server |
Jwt:Audience |
— | Default pia-client |
Jwt:AccessTokenExpirationMinutes |
— | Default 15 minutes |
Jwt:RefreshTokenExpirationDays |
— | Default 30 days, also the maximum: a longer value is capped and 0 or less gets 30 |
{ "OAuth": { "Google": { "ClientId": "...", "ClientSecret": "..." }, "Microsoft": { "ClientId": "...", "ClientSecret": "..." } }}In production, prefer environment variables: OAUTH_GOOGLE_CLIENT_ID / _SECRET, OAUTH_MICROSOFT_CLIENT_ID / _SECRET. See Microsoft Entra ID OAuth for the full Entra setup.
Encryption
Section titled “Encryption”| Key | Env | Purpose |
|---|---|---|
Encryption:MasterKey |
ENCRYPTION_MASTER_KEY |
64 hex chars (32 bytes) — used to derive per-user keys via HKDF |
The default upstream AI provider, plus optional per-mode overrides selected by the X-Pia-Mode header.
| Key | Env | Purpose |
|---|---|---|
Ai:ProviderType |
Ai__ProviderType |
One of the seven chat types: OpenAI, AzureOpenAI, Ollama, OpenRouter, OpenAICompatible, Mistral, VLlm (default OpenAI). Linkup is a web search provider, not a chat model — naming it here or under Ai:Modes stops the server at startup |
Ai:ApiKey |
Ai__ApiKey |
Upstream API key |
Ai:Model |
Ai__Model |
e.g. gpt-4o-mini |
Ai:Endpoint |
Ai__Endpoint |
Upstream base URL |
Ai:CreditRates:InputPerMillion |
Ai__CreditRates__InputPerMillion |
Credits charged per million input tokens against a member’s group credit ceiling. Default 1000 — the anchor, where one credit is 1,000 tokens. Also valid under Ai:Modes:<Mode>: and per catalog entry below. Omit it or give it a number — never an empty value, which fails the bind at startup the same way an empty Ai:EnableWebSearch does |
Ai:CreditRates:OutputPerMillion |
Ai__CreditRates__OutputPerMillion |
The same, per million output tokens. Default 1000 |
Ai:CreditRates:CachedInputPerMillion |
Ai__CreditRates__CachedInputPerMillion |
The same, for the input tokens the provider served from its cache. Default 1000 (no discount); must not exceed the input rate |
Ai:CreditRates:ConnectorPerMillion |
Ai__CreditRates__ConnectorPerMillion |
The same, for provider-side connector tokens such as Mistral web search. Default 0, which keeps them out of the budget |
TokenUsage:WeekStartTimeZone |
TokenUsage__WeekStartTimeZone |
IANA zone the weekly credit window is anchored in — the bucket starts Monday 00:00 there. Default Europe/Berlin. An id the host does not know is logged and falls back to UTC rather than failing a request |
Per-mode overrides take the same keys under a mode name:
| Key | Mode |
|---|---|
Ai:Modes:Assistant:* (Ai__Modes__Assistant__*) |
Assistant |
Ai:Modes:Research:* (Ai__Modes__Research__*) |
Research |
(default Ai:*) |
Optimize |
If a mode override isn’t set, the default provider is used.
Ai:Providers[]
Section titled “Ai:Providers[]”The provider catalog: a named list of upstreams that a group’s provider override, a guardrail’s classifier and answer models, Embeddings:ProviderId and Chunking:Context:ProviderId all reference by id. Entries defined here belong to the deploy: read-only in the admin console, and read once at startup. The same catalog also holds rows added at runtime on AI Providers, which is where the merge rules live.
| Key (per entry) | Purpose |
|---|---|
Id |
Required. The stable reference group settings store |
Name |
The label shown wherever a provider is picked |
ProviderType |
As above, plus Linkup for a web search provider — an entry a chat model delegates searching to, never one that answers a chat. Defaults to OpenAI |
Endpoint / ApiKey / Model |
The upstream, its credential, and the one model this entry serves. On a Linkup entry Model is the search depth — fast, standard or deep — and anything else stops the server at startup |
AzureDeploymentName |
Required for AzureOpenAI |
MistralAgentId |
Mistral only, optional |
ReasoningEffort |
None, Minimal, Low, Medium, High or XHigh. Omit to send no preference at all — None instead asks the upstream not to think, and only reaches types that own an off-switch. VLlm is the exception: it always pins enable_thinking, so omitted and None behave alike there |
EnableWebSearch / EnableVision |
Capability switches. EnableWebSearch is read as off on AzureOpenAI, Ollama, OpenAICompatible, VLlm and Linkup, whose handlers have no native search — set it there and it is ignored rather than rejected, which is also what lets those entries use WebSearchToolProviderId |
WebSearchToolProviderId |
Another catalog id — a Linkup entry or database row — this entry delegates web search to when it cannot search itself. The model’s query text is sent to that provider; see AI Providers |
VisionToolProviderId |
The same shape for image reading: a vision-capable catalog id a text-only entry delegates to |
SupportsReasoningEffort |
OpenAICompatible only: send the flat reasoning_effort field, none included. Off by default, since strict servers reject unknown fields — and not every server that takes low/medium/high takes none |
CreditRates:* |
The four rates above, per entry: InputPerMillion, OutputPerMillion, CachedInputPerMillion, ConnectorPerMillion. The card is stamped onto each usage row at spend time, so changing it re-prices future traffic only. A rate outside 0–10000000, or a cached rate above the input rate, stops the server |
An array takes a numeric segment in environment form, so Ai:Providers[0].Id is Ai__Providers__0__Id. Indices must start at 0 and stay contiguous — the binder silently ignores every entry past a gap.
Knowledge, Embeddings, Chunking (knowledge base)
Section titled “Knowledge, Embeddings, Chunking (knowledge base)”The knowledge base requires both the Knowledge license feature and
Knowledge:ConnectionString set — miss either and it stays off. See
Admin → Knowledge bases for the full setup flow and
Architecture → Knowledge base for how it works.
| Key | Env | Purpose |
|---|---|---|
Knowledge:ConnectionString |
Knowledge__ConnectionString |
pgvector DB connection — enables the KB when set |
Knowledge:IngestApiKey |
Knowledge__IngestApiKey |
Instance-wide REST ingest key (X-Pia-Service-Key); empty disables the REST API |
Embeddings:ProviderId |
Embeddings__ProviderId |
A provider catalog id — Ai:Providers[].Id or a database row — resolving the embedding endpoint/key |
Embeddings:Model |
Embeddings__Model |
Embedding model (default bge-m3) |
Embeddings:Dimension |
Embeddings__Dimension |
Must equal the compiled schema (1024); mismatch disables the KB |
Embeddings:TimeoutSeconds |
Embeddings__TimeoutSeconds |
Ingestion upstream timeout (default 100) |
Chunking:Context:Enabled |
Chunking__Context__Enabled |
Contextual retrieval, on by default (auto-disabled if no provider) |
Chunking:Context:ProviderId |
Chunking__Context__ProviderId |
A provider catalog id — Ai:Providers[].Id or a database row — of a chat model for per-chunk context |
Chunking:* |
Chunking__* |
Semantic double-pass splitter knobs (buffer size, thresholds, chunk-size bounds) |
Per-group quotas (KnowledgeBases, KnowledgeDocuments, KnowledgeStorageBytes,
MonthlyEmbeddingTokens) live in GroupSettings.Quotas and are edited in the
group editor, not in config.
Operators and Temporal
Section titled “Operators and Temporal”The Pia Mesh operator runtime. Off by default. Enabling it needs the
AiProxy license feature and a reachable Temporal service — the .NET SDK ships
only a worker, so the service is a separate container (both shipped compose files wire one).
| Key | Env | Default | Purpose |
|---|---|---|---|
Operators:Enabled |
Operators__Enabled |
false |
Master switch — restart-only |
Operators:MaxSteps |
Operators__MaxSteps |
8 |
Steps in one assignment — must be at least the longest reachable skill (brief takes 3) |
Operators:WallClockTimeoutSeconds |
Operators__WallClockTimeoutSeconds |
900 |
Total run time |
Operators:MaxConcurrentAssignments |
Operators__MaxConcurrentAssignments |
4 |
Worker-wide concurrency (not per user) |
Operators:MaxConcurrentAssignmentsPerUser |
Operators__MaxConcurrentAssignmentsPerUser |
2 |
One user’s Queued + Running rows; a further POST gets 429. Clamped to MaxConcurrentAssignments |
Operators:MaxActivityAttempts |
Operators__MaxActivityAttempts |
3 |
Retries of a single step |
Operators:PerAssignmentTokenCeiling |
Operators__PerAssignmentTokenCeiling |
200000 |
LLM spend per assignment |
Operators:QueuedGraceMinutes |
Operators__QueuedGraceMinutes |
5 |
How long a Queued row waits before the reconcile pass owns it |
Operators:MaxReconcileAttempts |
Operators__MaxReconcileAttempts |
5 |
Reconcile tries before a row is force-failed |
Operators:RetentionDays |
Operators__RetentionDays |
30 |
Age at which a finished assignment and its events are deleted; also the maximum, see Retention |
Operators:PlaintextRetentionHours |
Operators__PlaintextRetentionHours |
72 |
Age at which a finished assignment’s plaintext is dropped even if no client acknowledged it. Also the maximum; zero or negative falls back to 72 — see Retention for why, why the Temporal namespace retention is the other half of the number, and why moving it makes the desktop client’s consent copy (which names 72 hours as a constant) wrong |
Temporal:Address |
Temporal__Address |
temporal:7233 |
Temporal service — restart-only |
Temporal:Namespace |
Temporal__Namespace |
pia |
Restart-only |
Temporal:TaskQueue |
Temporal__TaskQueue |
pia-operators |
Restart-only |
Operators:Enabled and all three Temporal:* keys are read once at startup, before the configuration
overlay the admin UI writes to is layered in. That is deliberate: a running worker must not have the
address changed underneath it. Every other Operators:* number hot-reloads and takes effect on the next
tick or the next assignment.
Connector (pod) settings are not configuration keys — they live on catalog rows and are edited from Admin → Connectors.
Retention
Section titled “Retention”Every sweep that deletes personal data reads its period from one of these keys. Each value is the default
and the maximum: a shorter period is honoured, a longer one is capped, and 0, a negative number or text
gets the default. None of them switches a sweep off. A replaced value is logged once as a warning, which the
first sweep does at startup. The one key with a maximum above its default is Gdpr:DeletionGraceDays.
| Key | Env | Days | What goes |
|---|---|---|---|
Audit:RetentionDays |
Audit__RetentionDays |
180 |
Audit log entries |
Audit:IpRetentionDays |
Audit__IpRetentionDays |
14 |
The IP address on older audit entries; the entry stays |
LoginHistory:RetentionDays |
LoginHistory__RetentionDays |
90 |
Sign-in events and the login anomalies derived from them |
LoginHistory:IpRetentionDays |
LoginHistory__IpRetentionDays |
14 |
The IP address, user agent and coordinates on older sign-in events; city and country stay |
Guardrails:RetentionDays |
Guardrails__RetentionDays |
30 |
Guardrail decisions |
TokenUsage:RetentionDays |
TokenUsage__RetentionDays |
90 |
Token usage rows and the frozen weekly credit pools |
EmbeddingUsage:RetentionDays |
EmbeddingUsage__RetentionDays |
90 |
The knowledge base’s embedding ledger |
Abuse:RetentionDays |
Abuse__RetentionDays |
30 |
Account-creation events and registration and invitation bursts |
AccountTombstones:RetentionDays |
AccountTombstones__RetentionDays |
14 |
Credit tombstones of deleted accounts |
GroupInvitations:RetentionDays |
GroupInvitations__RetentionDays |
30 |
Accepted, revoked and expired group invitations |
Operators:RetentionDays |
Operators__RetentionDays |
30 |
Finished operator assignments and their events |
Sync:TombstoneRetentionDays |
Sync__TombstoneRetentionDays |
30 |
Deleted synced items (templates, personas, providers, sessions, memories, to-dos, board columns, scheduled jobs, research sessions, reminders, assistant chats) and deleted knowledge-base documents, counted from the delete |
SyncEvents:RetentionDays |
SyncEvents__RetentionDays |
90 |
The sync event log, one row per push |
SyncCursors:RetentionDays |
SyncCursors__RetentionDays |
180 |
A device’s sync cursor, counted from its last push; a revoked device’s cursor goes at the next sweep |
LicenseEvents:RetentionDays |
LicenseEvents__RetentionDays |
180 |
License events (limits reached, features blocked, expiry) |
Gdpr:DeletionGraceDays |
Gdpr__DeletionGraceDays |
14, at most 30 |
A deleted account, before it is purged for good |
Feedback:ClosedContentRetentionDays |
Feedback__ClosedContentRetentionDays |
30 |
An AI feedback report’s text and account link, counted from closing |
Feedback:ContentRetentionDays |
Feedback__ContentRetentionDays |
90 |
The same, counted from arrival, whatever the report’s status |
Feedback:RetentionDays |
Feedback__RetentionDays |
365 |
The minimised trend record left of a report, counted from arrival |
Feedback (AI feedback)
Section titled “Feedback (AI feedback)”| Key | Env | Purpose |
|---|---|---|
Feedback:NotifyAddress |
Feedback__NotifyAddress |
Where a thumbs-down report is mailed. Empty means nobody is notified and the client says so. Ignored on a licence with the PiaCloud feature, which notifies ki@pia-ai.de |
Feedback:PrivacyNotifyAddress |
Feedback__PrivacyNotifyAddress |
Where a report the user marked as a privacy concern is mailed; falls back to Feedback:NotifyAddress. Ignored on PiaCloud, which notifies datenschutz@pia-ai.de |
Both need an SMTP relay (Smtp:*) to reach anyone. They are environment-only on purpose: an address that
receives user content should not be a field in the console. The three retention keys are listed under
Retention; AI feedback describes the channel.
| Key | Env | Purpose |
|---|---|---|
Legal:ImprintUrl |
Legal__ImprintUrl |
Linked as Imprint from the public account pages (registration, password reset, e-mail confirmation) |
Legal:PrivacyUrl |
Legal__PrivacyUrl |
Linked as Privacy from the same pages |
An unset key shows no link. Only an absolute http or https URL is used; anything else is dropped with a
warning.
License
Section titled “License”| Key | Purpose |
|---|---|
License:Path |
Path to the signed .lic file (default ./license.lic next to the binary; both Compose files set /app/data/license.lic, on the mounted data volume) |
Setup:TokenPath |
Where the setup token is written while the server is unlicensed (default ./.setup-token) |
There is no License:PublicKey key — the verifying key is compiled into the binary.
See Licensing for how a licence is obtained and replaced, and Admin overview → Activating a new install for the setup token.
OpenApi
Section titled “OpenApi”| Key | Purpose |
|---|---|
OpenApi:Enabled |
Serve the generated API description at GET /openapi/v1.json. Default false |
Off by default because the route is anonymous once on: anything that can reach the server can read the
shape of your surface without an account. That is deliberate — an integrator has no token yet — but whether to
publish it is the operator’s call, so it has to be made explicitly. The admin plane is never described either
way, and an unlicensed server answers 403 no_license here like everywhere else outside the setup allowlist.
A licence carrying the PiaCloud feature refuses to serve the document whatever this key says — on the managed
instance the API is the desktop client’s transport rather than an advertised contract.
One-shot command-line entry points
Section titled “One-shot command-line entry points”Two switches make the server do one job and exit instead of starting the web host. Both run ahead of the
host builder, so each can be given strictly less than the web container holds — neither needs
Jwt:SecretKey or Encryption:MasterKey. They are refused together rather than running only the first:
they are meant to carry different credentials, so asking for both is a mistake about which container is
being run.
| Switch | Reads | Does |
|---|---|---|
--migrate-only |
Database:Provider, Database:MigrationConnectionString (falling back to Database:ConnectionString), Knowledge:ConnectionString |
Applies pending migrations and exits. The deploy-time path for a server whose runtime role holds no DDL rights |
--provision-pod |
Database:Provider, Database:ConnectionString |
Provisions a Pia Mesh pod: creates the connector row, mints its token, optionally adds the operator row, and grants both to groups |
Provisioning a pod headlessly
Section titled “Provisioning a pod headlessly”--provision-pod create --name <name> [--grant-group <guid>]... [--operator-skill <skill>]--provision-pod issue-token --connector <guid>docker compose -f docker-compose.prod.yml run --rm pia-server \ --provision-pod create --name "Invoices" --operator-skill invoices --grant-group <group-id>Every prerequisite for a running pod was otherwise reachable only by clicking through
Admin → Connectors, which is why this exists. stdout carries exactly one JSON
object — connectorId, operatorId, token, grantedGroupIds, resolvedMemberCount — and every log
line, warning and refusal goes to stderr, so a script’s parse cannot be corrupted by an interleaved log.
What it refuses, and why each refusal beats the alternative:
- An unknown
--grant-groupid. A typo would otherwise produce a pod that is online, granted on paper, and routable to nobody. - Granting without the
GroupManagementlicence feature — the same gate every other writer of a group’s plugin allowlist honours. - A skill name that the Operators page would also reject, using the same rules, so the two writers cannot drift apart.
It also reports what it cannot refuse. resolvedMemberCount is the number of users in the granted groups:
zero means the grant is written and no chat request can reach the pod yet. With no --grant-group at all it
warns that the pod will connect and serve nothing. And whether the pod actually advertises the skill named
by --operator-skill is not checked here — that needs the pod card, which does not exist until the pod
connects — so a mismatch surfaces as a run failing operator_pod_plane_mismatch.
Writing the pod itself is Writing a pod.
Environment variable summary
Section titled “Environment variable summary”The keys most commonly set in production:
| Required | Variable |
|---|---|
| ✓ | ASPNETCORE_ENVIRONMENT (Production) |
| ✓ | Database__Provider and Database__ConnectionString |
| ✓ | JWT_SECRET_KEY |
| ✓ | ENCRYPTION_MASTER_KEY |
| ✓ (Postgres) | POSTGRES_PASSWORD (the owner role’s password — matches the one in Database__ConnectionString until the role split is adopted) |
| optional | AI_* |
| optional | OAUTH_*_CLIENT_ID / _SECRET |
| optional | Knowledge__ConnectionString + Embeddings__ProviderId (enable the knowledge base) |
| optional | Operators__Enabled + Temporal__Address (enable the Mesh operator runtime) |
| optional | OpenApi__Enabled (false withdraws the generated API description) |
| optional | Feedback__NotifyAddress, Feedback__PrivacyNotifyAddress and Smtp__* (who is told about AI feedback) |
| optional | Legal__ImprintUrl, Legal__PrivacyUrl (legal links on the public account pages) |
| optional | FreeTier__Suspended (true freezes new account creation and free-group AI spend). Never set it before the deployment’s first admin has signed in: account creation is the only way an admin comes into being, so the console becomes unreachable — see Limits & Policies |
appsettings.json is committed; .env.prod is not (and should be chmod 600).
Restart vs hot-reload
Section titled “Restart vs hot-reload”Most operator-tunable settings are read via IOptionsMonitor<T> and re-bind on file change — for example, rate limit and quota policies on the /admin/limits page apply immediately.
These settings only take effect after a restart:
Database:Providerand all threeDatabase:*ConnectionStringkeysJwt:SecretKey(changing it invalidates all outstanding tokens)Encryption:MasterKey(changing it strands all existing ciphertext — see Encryption at rest)Operators:Enabledand everyTemporal:*key (captured before the admin override overlay, so a running worker cannot have its target changed underneath it)
The admin UI surfaces a banner when a change requires a restart, with a Restart button that delegates to RestartCapability. In environments where a process restart isn’t safe (e.g. a single-replica deployment without a supervising orchestrator), the capability is no-op and the operator must restart the container manually.