.env at the repo root, then .env.local (.env.local takes precedence). The dev server is responsible for loading these files into the server-side routes; values are accessed through process.env at runtime. Credentials saved from the in-app Configurations panel are written to .env.local.
The canonical example lives in .env.example. This page is the authoritative reference for every variable Fleet Pi reads, grouped by concern.
LLM providers
Fleet Pi has no hard-coded default model. When a new chat session arrives without a model, the server picks one:- The
defaultProvider/defaultModelfrom Pi settings, when that provider has credentials. - Otherwise, the first model with credentials that Fleet Pi registered itself (the OpenAI-Chat-Completions slot or a custom provider) and that you enabled.
- Otherwise, the first enabled model with credentials, then the first model with credentials.
No chat model is available: add an LLM provider in Settings > Providers (or set its env vars) and try again. Fleet Pi never falls back to Pi’s built-in per-provider defaults. The composer also holds your first message until the model picker has loaded, so a turn is never sent without a model.
On deployed authenticated chat, the Neon AI Gateway backs the OpenAI-Chat-Completions (OCC) slot, with qwen35-122b-a10b as the primary model and gpt-oss-120b also enabled. A user’s own OCC BYOK setting always takes precedence when configured.
Pi settings store the active provider and model, and you can change both from the in-app config panel. Locally, the same panel manages provider API keys and writes them to .env.local. Models you explicitly enable for a built-in provider appear in the model picker, and their saved BYOK credentials are used on hosted turns.
OPENAI_API_KEY is always sent to https://api.openai.com/v1. Fleet Pi does not read OPENAI_BASE_URL or OPENAI_MODEL. For a gateway or any other OpenAI-compatible endpoint, use the openai-chat-completions provider instead:
Set all three together. These variables apply to local runs only. Deployed chat never uses the env API key: each signed-in user adds the provider in Settings > Providers, and without it the Neon AI Gateway backs this slot. Locally, signed-in accounts backed by the chat database (
FLEET_PI_CHAT_DATABASE_URL) prefer values saved in Settings and fall back to these variables.
Amazon Bedrock
When using Bedrock, Fleet Pi uses the standard AWS credential chain — environment variables, profile, or IAM role. To pin a default provider and model, setdefaultProvider and defaultModel in .pi/settings.json or from the in-app config panel. The committed .pi/settings.json sets neither, so Fleet Pi resolves the model as described above:
.env (or via the in-app config panel). Every provider supported by Pi is available, so pick whichever credentials you already have.
Amazon Bedrock (opt-in)
WhendefaultProvider is amazon-bedrock, Fleet Pi uses the standard AWS credential chain:
You can also provide
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY directly. Bedrock model IDs use region prefixes such as us.anthropic.claude-sonnet-4-6.
Pi runtime
Agent loop settings
Change these from Settings > Agent loop. They are saved as Pi settings throughGET / PATCH /api/chat/settings. On hosted chat, saved values apply on the next Durable Object turn.
Hosted chat registers extensions, code mode, and MCP servers only when you turn them on.
Logging
The logger lives in
apps/web/src/lib/logger.ts. It includes PII redaction and emits a requestId correlation ID for every chat request, which lines up with provider circuit-breaker events for incident review.
Authentication (Better Auth)
Auth is disabled until you setBETTER_AUTH_SECRET. When the secret is present, Better Auth is mounted at /api/auth/*. The auth store can be local SQLite (default) or Neon Postgres.
The Google login button is hidden in the UI when either Google variable is missing. When
FLEET_PI_AUTH_DATABASE_URL is set, Better Auth uses Neon instead of local SQLite — apply schema once per environment with pnpm --filter web auth:migrate.
Neon Managed Auth (optional)
Fleet Pi runs Better Auth by default. SetNEON_AUTH_BASE_URL (Fleet Pi also accepts NEON_AUTH_URL as a fallback) to proxy sign-in, session, and account routes to Neon Managed Auth instead. Leaving both URLs unset keeps the Better Auth + SQLite fallback, so local anonymous chat still works. Use Managed Auth when you want Neon to own user identity across the app.
Neon AI Gateway (default authenticated chat)
On deployed environments, authenticated chat routes through the Neon AI Gateway as the platform OpenAI-Chat-Completions (OCC) backend. This gives every signed-in user a working model out of the box — no BYOK required — while a user who has saved their own OCC provider settings still takes precedence. Fleet Pi enables two Gateway models by default:qwen35-122b-a10b— primarygpt-oss-120b
gemini-3.5-flash) — the Gateway only activates when the user is signed in and the two env vars below are set.
Both values are captured into process memory once at boot and then deleted from
process.env so agent shell tools cannot read them with printenv. This means you should not rely on NEON_AI_GATEWAY_* being visible to your own code past startup.
URL shape
Fleet Pi enforces a single/v1 suffix on the Gateway base URL. All of these normalize to the same value:
*.neon.tech. Any other host is rejected at boot and the Gateway is skipped rather than serving requests to an untrusted origin.
BYOK precedence
When a user saves an OpenAI-Chat-Completions provider in the config panel, their BYOK settings win over the platform Gateway. Legacy OCC records are only migrated to the platform Gateway shape when the Gateway is active and the user has not brought their own OCC credentials.Named OpenAI-compatible instances
Each user can save multiple OpenAI-compatible Chat Completions endpoints side by side — for example one instance for OpenCode Zen and another for Nebius — instead of overwriting a single BYOK slot. Every named instance keeps its own display name, base URL, model ID, and API key, and each one appears in the model picker as a separate provider row. Named instances work on both deployed chat (signed-in users) and local anonymous chat. Use named instances when you want to:- Route different chats through different OpenAI-compatible backends without editing settings between turns.
- Keep a per-vendor label in the config panel so it’s obvious which endpoint you’re about to use.
- Add a new OpenAI-compatible provider without disturbing your existing default OCC configuration.
Fleet Pi picks the storage backend for named instances based on the surface:
The file store is written atomically (temp file + rename) with a per-process mutation lock, so concurrent creates get distinct slugs. A malformed or future-version store falls back to an empty list with a diagnostic instead of breaking chat session creation. The
.fleet/ directory is gitignored, but treat .fleet/providers.json as sensitive — it contains plaintext API keys.
Named instances must use an
https:// base URL by default. On local dev surfaces, OCC-family instances can also point at http://localhost (useful for pointing at an Ollama or LM Studio process). Deployed chat still requires https:// at both save time and runtime registration, so a legacy http:// value can never sneak through.openai-chat-completions) still exists alongside named instances and continues to take precedence over the platform Neon AI Gateway. Named instances give you additional endpoints without replacing that default.
Fleet Pi also validates each instance every time the runtime registers it. If the stored API key can’t be decrypted or the base URL fails the safety checks, the instance is skipped with a warning diagnostic and shows up in the Settings providers list as Not configured instead of a misleading healthy row — so the model picker never advertises an endpoint that would fail at request time.
Readiness gate
pnpm verify-deployment-readiness validates that NEON_AI_GATEWAY_BASE_URL is a well-formed allowed Gateway URL — not just present. Deploys fail closed when the URL is malformed or points off the *.neon.tech allowlist, so a broken Gateway variable cannot silently ship.
Require chat authentication (optional)
Fleet Pi serves the chat streaming API from the same Cloudflare Worker that hosts settings, providers, and the workspace. On Cloudflare, new hosted Agent sessions stream from the signed-in user’s Pi Durable Object. Chat routes require authentication on Cloudflare and whenever Neon Managed Auth is configured.Sessions and workspace paths
Locally, Pi session files are persisted under.fleet/sessions/ inside the repo. On Cloudflare, hosted Agent transcripts live in the per-user Pi Durable Object instead (see chat session mirror). The session manager rejects paths outside the repo-scoped directory via isUsableSessionFile, so a stale sessionFile in localStorage silently falls back to a fresh session — see runbooks for recovery.
Canonical durable state lives under agent-workspace/. The workspace server reads canonical files directly and uses agent-workspace/indexes/ only as projection storage.
Chat session mirror (Neon Postgres)
Locally, Pi session JSONL files under.fleet/sessions/ are the source of truth. On Cloudflare, each signed-in user’s hosted Agent transcript lives in that user’s Pi Durable Object (SQLite storage, bound as USER_PI_DURABLE). Neon keeps auth, settings, and an optional projection of the conversation.
When FLEET_PI_CHAT_DATABASE_URL is set, Fleet Pi additionally mirrors full Pi session entries, run events, tool executions, and file mutations into Neon Postgres tables prefixed with pi_. Use this when you want SQL search across conversations, cross-surface history, analytics, or long-term debugging. The Cloudflare readiness check requires FLEET_PI_CHAT_DATABASE_URL.
In local development, mirror failures are caught and logged and never break chat streaming. On Cloudflare, mirror errors fail closed.
Use two separate roles in Neon:
The runtime role name defaults to
fleet_pi_app. Override it with FLEET_PI_APP_ROLE.
Apply migrations once per environment before starting the app:
Daytona-backed user sandboxes
Authenticated users can be assigned an isolated Daytona sandbox that runs Pi tool calls in a container instead of on the host. Each user gets one sandbox in their Daytona account via BYOK (bring your own key), keyed by their Better AuthuserId. Sandbox routes require Better Auth — unauthenticated requests return 401.
How Daytona is enabled per user
Fleet Pi enables Daytona for a user only when:- The user is authenticated through Better Auth.
- A Daytona API key is resolved for that user.
DAYTONA_API_KEY environment variable only in local development. On Cloudflare (FLEET_PI_DEPLOYMENT=cloudflare), env DAYTONA_API_KEY alone does not enable Daytona — each logged-in user must save their own Daytona API key as the daytona provider secret.
When Daytona is not enabled for the calling user, GET /api/sandbox/preview returns 503 and tool calls run against the host workspace (unless the request is expected to have a sandbox, in which case the request fails closed).
Environment variables
Persistence and mount paths
Each user’s sandbox has one persistent volume that survives sandbox restarts and archival:
There is no full-repo clone in the sandbox and no sandbox-side Pi session store — Pi sessions stay on the host, the per-user Durable Object, or the Neon mirror. The sandbox mounts only
agent-workspace/.
Fleet Pi seeds each volume from a neutral starter template (templates/agent-workspace/ in FLEET_PI_REPOSITORY_URL): system policy files, empty memory stubs and three starter Pi skills. It does not copy Fleet Pi’s own agent workspace.
- The volume records the applied template version in
.fleet/template-version. Prepare skips the clone when the volume is at that version or newer, and re-seeds when a newer build ships a newer template version. - Fleet Pi copies missing files and never overwrites a file. When a template file changed and your copy differs, Fleet Pi writes the new copy to
.fleet/template-updates/<version>/<path>for you to review. - On volumes seeded before template versions existed, files byte-identical to something the old seed copied move once to
.fleet-pi-quarantine/legacy-seed/. Files you edited stay in place.
Legacy sandbox migration
Sandboxes provisioned before this release mounted the workspace at/home/daytona/fleet-pi. Fleet Pi now expects /home/daytona/agent-workspace. On the next warm-up, legacy sandboxes are recreated automatically — the durable fleet-pi-ws-* volume is preserved and remounted at the new path. No action is required.
Provider credentials in the sandbox
When Daytona is active, Fleet Pi tries to sync each configured LLM provider key into the user’s Daytona organization as a Secret namedfleet_pi_<providerId>. The sandbox then sees only opaque placeholders (dtn_secret_*); Daytona substitutes the real value on egress to the provider’s allowlisted HTTPS host.
Providers eligible for Secrets sync (known HTTPS API hosts):
The following credentials are still injected as plaintext inside the sandbox (they cannot use Secrets-based egress substitution): GitHub Copilot OAuth tokens, Google Vertex ADC (
GOOGLE_APPLICATION_CREDENTIALS), Bedrock signing keys, OLLAMA_BASE_URL, and OCC base URL / model ID.
Daytona sandbox credential sync covers only the reserved default OpenAI Chat Completions slot (
openai-chat-completions). Additional named OpenAI-compatible instances live in the chat runtime’s encrypted store and are never injected into the sandbox — sandbox tool calls that need one of those endpoints must go through the chat runtime, not directly from the container.Access denied on the Secrets endpoint), Fleet Pi falls back to plaintext injection instead of failing sandbox provisioning.
See the API reference for the sandbox preview and webhook contracts.
Cloudflare Workers deployment (trust zones)
Fleet Pi deploys the web app to Cloudflare Workers. Cloudflare is the only supported deploy host. The Worker config lives inapps/web/wrangler.jsonc:
- Worker name
fleet-pi-webwith thenodejs_compatcompatibility flag and observability enabled. - A SQLite-backed Durable Object class
UserPiDurable, bound asUSER_PI_DURABLE. Each signed-in user gets one object that stores their hosted Agent transcript. - Rate limit bindings
FLEET_PI_SIGN_IN(10 requests per 60 seconds) andFLEET_PI_CHAT_START(30 requests per 60 seconds). - The non-secret var
FLEET_PI_DEPLOYMENT=cloudflare, which switches Fleet Pi to its hosted, fail-closed, Postgres-backed code paths. Worker Previews also setFLEET_PI_DEPLOYMENT_TRUST_ZONE=preview.
wrangler secret, not in wrangler.jsonc. Build and deploy from apps/web:
local, cloudflare-production, and cloudflare-preview. On the Worker, assertDeploymentReadyOnBoot() runs the readiness check before Better Auth mounts, so missing secrets or misconfigured previews fail fast instead of accepting cross-zone traffic. Local development is unaffected and stays anonymous.
Set these variables on the Worker (in addition to the auth and chat mirror variables above):
Preview Workers must point at a Neon preview branch that is distinct from production. Previews do not inherit production secrets. The readiness check confirms the preview marker is present in both database URLs and the production marker is absent.
Verify readiness locally before deploying:
Generated configuration files
Related
Quickstart
Apply this configuration end to end.
Runbooks
Troubleshoot provider errors, sessions, and circuit-breaker state.