Configuration
Ce contenu n’est pas encore disponible dans votre langue.
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:AccessTokenLifetime |
— | Default 15 minutes |
Jwt:RefreshTokenLifetime |
— | Default 30 days |
{ "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 |
Operators:PlaintextRetentionHours |
Operators__PlaintextRetentionHours |
72 |
Age at which a finished assignment’s plaintext is dropped even if no client acknowledged it. 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.
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 | 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.