Skip to content

Environment Variables ​

Most runtime variables use the OXICLOUD_ prefix. A few build-time or allocator variables do not.

Every variable below can also be written in a TOML config file — the key is the name with the OXICLOUD_ prefix dropped, lower-cased. See toml.md.

Server ​

VariableDefaultDescription
OXICLOUD_STORAGE_PATH./storageRoot storage directory
OXICLOUD_STATIC_PATH./staticStatic files directory
OXICLOUD_TEMP_DIRstd::env::temp_dir() ($TMPDIR)Directory for tier-1 temporary data — pure scratch, safe to lose at reboot. Backend services stream blobs here when extractors need a &Path (id3, mp3_duration, ffprobe, nom-exif video). Files are auto-removed after use. On Linux /tmp is often tmpfs (RAM-backed); point at a disk-backed dir under RAM-constrained deployments.
OXICLOUD_SERVER_PORT8086Server port
OXICLOUD_SERVER_HOST127.0.0.1Server bind address (IPv4 or IPv6 allowed)
OXICLOUD_BASE_URL(auto)Public base URL for share links; defaults to http://{host}:{port}
OXICLOUD_BASE_PATH(empty)URL path prefix the whole app is served under (e.g. /oxicloud) for subpath deployments behind a reverse proxy. Runtime-only — the same frontend build serves any prefix; the server injects it into the SPA shell's <base href>. The proxy must forward the prefix unstripped. See Deployment.
OXICLOUD_MAX_UPLOAD_SIZE10737418240Whole-file size ceiling, in bytes (10 GB on 64-bit, 1 GB on 32-bit). Applies to BOTH direct PUTs (per-request body) and chunked uploads (declared total_size, checked upfront at session creation).
OXICLOUD_DIRECT_PUT_MAX_BYTES1073741824Per-request cap for non-chunked PUT bodies, in bytes (1 GiB). Set below OXICLOUD_MAX_UPLOAD_SIZE so larger files are pushed onto the chunked protocol (resumable on failure). See Storage Fine Tuning.
OXICLOUD_CHUNK_MAX_BYTES104857600Maximum size of a single chunked-upload PUT in bytes (100 MB). Per-chunk cap, independent of OXICLOUD_MAX_UPLOAD_SIZE (whole-file cap). See Storage Fine Tuning.
OXICLOUD_CHUNK_DIR{STORAGE_PATH}/.uploadsRoot directory for chunked-upload sessions (REST + NextCloud). Direct (non-chunked) uploads stream straight into the blob store and need no spool directory. Placement guidance: see Storage Fine Tuning.
OXICLOUD_REUSE_PORTfalseEnable SO_REUSEPORT so multiple processes can share the same port. Disabled by default — a second accidental instance will fail with "address already in use". Enable only for deliberate multi-worker setups (process supervisor, rolling restart). Not supported on Windows.
OXICLOUD_WORKER_THREADSCPU budget (affinity capped by the cgroup CFS quota)Tokio worker threads. Tokio's own TOKIO_WORKER_THREADS is honoured as a fallback when this is unset. The default folds in docker --cpus / cgroup cpu.max, which available_parallelism() alone ignores — on a quota-limited container it would otherwise spawn one worker per host core.
OXICLOUD_MAX_BLOCKING_THREADSmax(32, 8 × workers)Cap on the spawn_blocking pool (thumbnails, transcode, zip, PDF/text extraction, Argon2 at ~19 MB per hash). Deliberately below tokio's flat default of 512, which bounds worst-case RSS at multiple GB.
OXICLOUD_METRICS_LISTEN(unset)Prometheus /metrics listener address (e.g. 127.0.0.1:9090, IPv6 allowed as [::1]:9090). Unset = disabled: no /metrics endpoint is bound and no metrics recorder is installed (zero runtime cost). When set, a separate HTTP listener on this address serves the text-format scrape. Deliberately NOT merged into the main API — no auth, CSRF, or DPoP layer in front. Bind to loopback or a private interface unless you intend to expose metrics publicly. Starter counters: oxicloud_dpop_verify_failed_total{reason}, oxicloud_dpop_proof_missing_total, oxicloud_dpop_header_missing_on_bound_session_total, oxicloud_dpop_replay_detected_total, oxicloud_dpop_nonce_challenges_issued_total.
OXICLOUD_STARTUP_JOBS—DEPRECATED spelling of OXICLOUD_JOBS_STARTUP, still read so an existing deployment keeps working; a deprecation warning is logged. Renamed when scheduling and alerting arrived, so everything about jobs shares one OXICLOUD_JOBS_* prefix and an operator can find the whole surface by grepping for it. Setting both is a fatal error rather than a precedence rule: this list can delete files, and someone who left the old name in a compose file while adding the new one must not have to guess which list ran.
OXICLOUD_JOBS_STARTUPthumb_derived_import?repair=true,thumb_attached_import?repair=true,transcode_import?repair=true,backend_rechunkBackground jobs dispatched once at boot, comma-separated, each name or name?flag=true using the same syntax as POST /api/admin/jobs/{name}/trigger. Flags: force, deep, repair, storage. The three import jobs migrate thumbnails and cached transcodes out of the legacy .thumbnails/ and .transcoded/ directories and delete the originals, so the migration completes without anyone triggering it from the admin panel; each sidecar is read back through the normal stack before it is unlinked, and every deletion is audited. backend_rechunk converts pre-CDC whole-file blobs into CDC chunks so a Range read stops reading (and, with encryption, decrypting) the whole blob; it has no repair flag because converting is all it does, and it frees the whole-file copy only after the chunks are written and hash-verified. It replaces a background task previously spawned at boot behind OXICLOUD_LEGACY_RECHUNK, so omitting it from an explicit list stops a migration that used to run on every start — its cost is one full read of every legacy blob, which on a metered backend may want deferring (see OXICLOUD_LEGACY_RECHUNK). An explicit value replaces the default; set it empty (OXICLOUD_JOBS_STARTUP=) to disable startup jobs, or to thumb_derived_import,thumb_attached_import to import without deleting. Non-blocking — readiness never waits on a job; entries run sequentially in the background. Fail-fast — an unknown job name or flag panics at boot, because a silently-dropped entry means a migration that never runs. A run interrupted by a restart resumes from its cursor on the next boot, so a long migration finishes across restarts. Safe to leave at the default: the jobs are idempotent, and once drained a run does nothing. See Thumbnail Migration for the upgrade runbook.
OXICLOUD_JOBS_SCHEDULEDconsistency_batch=168hWhich jobs run on a timer, and how often. Sibling of OXICLOUD_JOBS_STARTUP: that one dispatches a job once at boot, this one gives it a cadence. Comma-separated name=interval entries, where the interval is a number plus s/m/h/d, or off. Overrides whatever the code registers, applied at boot before the supervisor starts. The default schedules consistency_batch weekly, which runs every *_consistency detector in sequence — picking up new detectors automatically as they are added, so one entry covers them all on a database-friendly serial walk instead of seven jobs ticking at once. Weekly rather than daily because the batch includes backend_consistency, which enumerates the storage backend and costs real money on S3. A scheduled run is discovery-only: deep and repair both default to false, so it reads metadata and records findings — it does not re-hash content and it does not delete. off un-schedules without disabling — the job keeps its admin-panel row and its Run button; "not on a timer" and "not available" are different things. To keep only the cheap database checks, daily: OXICLOUD_JOBS_SCHEDULED=consistency_batch=off,blobs_consistency=24h,satellites_consistency=24h. Read-only parameters may be attached with the same query syntax as the trigger endpoint (backend_consistency=720h?deep=true for a monthly bit-rot scan). repair cannot be scheduled and is refused at boot: a tick that repairs deletes on a cadence with nobody consenting after the first time — name the job in OXICLOUD_JOBS_STARTUP instead, which runs it once as a deliberate act. Fail-fast — an unknown job name, an undeclared parameter, a duplicate job name, or a zero interval panics at boot, because a silently-ignored entry means a sweep the operator believes is running. A job's paused run is also never auto-resumed by a tick if it was started with repair or deep; it waits for an explicit Resume, and the decline is audited. repair is gated for consequence (it deletes), deep for cost: re-reading and re-hashing every object is metered on S3, and the case that settles it is a flapping backend — the run pauses on a transient failure, a tick resumes it, it fails and pauses again, spending real money on a loop with nobody watching. Finishing work an operator asked for is fine; re-attempting it indefinitely a week later is not the same thing. This variable only needs to name jobs whose cadence differs from what the code registers — consistency_batch registers on-demand, so the default above is what schedules it, while the cleanup jobs already carry their own daily interval and appear here only to change it (job_runs_cleanup=24h?retention_days=90, or job_runs_cleanup=off).
OXICLOUD_JOBS_NOTIFY_MIN_SEVERITYdata_lossWhich job findings reach the configured channels (OXICLOUD_WEBHOOK_URL, OXICLOUD_JOBS_NOTIFY_EMAIL_TO) — the policy half, scoped to jobs because the severities are a findings concept. What is sent is a transition, not a run summary: a finding kind absent from the previous completed run and present now, or present then and absent now. A persisting problem is therefore announced once rather than every scheduled run — fifty-two weekly alerts about the same 29 orphaned blobs is an alarm that gets disconnected. Resolutions are sent too, gated by the severity the finding had, so an all-clear only arrives for something that was actually reported. Only completed runs are compared: a paused or failed run holds partial findings, and diffing one would announce everything it had not reached yet as resolved. Delivery is best-effort — the finding row is the truth, a failing webhook never fails the run, and the failure is audited plus counted on the run as notify_failures so a broken channel is visible without reading logs. Also governs run-health alerts, which are a second class of message: a run that stopped rather than one that found something. When a run gives up — the backend unreachable after its retries, or a terminal failure — it records error_reason (a stable key: backend_unavailable, backend_timeout, job_failed, server_restart) alongside error_message (the full error chain), and that sends one alert with the reason as its kind and the chain as its detail; completing later sends the all-clear. These are graded anomaly, so the default floor does not deliver them — widen this to hear about a sweep that could not run. The grade is deliberate rather than convenient: the scale measures how bad the data is, and a stopped run makes no claim about data, so labelling "I could not check" as data_loss would corrupt the one signal that has to stay trustworthy. A pause an operator asked for records neither column and never alerts — "stopped with a reason" is the entire discriminator, so a human clicking Pause pages nobody. De-duplicated per run against a marker on the run row, so a backend down all week is auto-resumed repeatedly and still produces one message per run rather than one per attempt. Verify the whole path with the notify_selftest job (?stall=2 for this class, a bare trigger for findings) — see Admin Settings. Lowest severity worth sending: data_loss, inconsistent, anomaly, or none to send nothing while keeping the destination configured (none silences both classes). The names are the ones jobs.run_findings.severity and the findings drawer already use, deliberately — a second vocabulary would mean translating between an alert and a config file. The default is the strictest useful setting because the failure mode of an alert channel is being muted, and the fastest route there is firing on something the operator considers routine. A severity this build does not know is below every threshold, so a new one added by a future detector cannot start paging people before someone decides it should. Fail-fast — an unrecognised value panics at boot rather than resolving to "everything" (pages for an anomaly) or "nothing" (indistinguishable from a healthy instance).
OXICLOUD_JOBS_NOTIFY_EMAIL_TO(unset)Who receives job findings by mail, comma-separated. Unset = no mail alerting. Used in addition to OXICLOUD_WEBHOOK_URL when both are set, which is the usual pairing — a push channel to be told now, mail for the record — and there is no routing setting to express that, because the channel set is whatever is configured. Needs OXICLOUD_SMTP_*; set without it the server refuses to start, for the same reason an unusable webhook URL does. An explicit recipient rather than treating "SMTP is configured" as consent, deliberately: SMTP is already set on nearly every instance for magic links and password resets, and quietly converting that into data-loss mail is a subscription nobody asked for — and there is no good implicit address, since every admin account mails people who never opted in while the operator this exists for may have no account at all. One message per finding and per recipient: no digest, because alerts are per finding kind and per run so the volume is one or two with the diff suppressing repeats, and a subject naming one severity and one kind can be filtered and threaded where "3 findings changed" cannot; no shared To: line, because the port is single-recipient and a shared operations list and a personal address do not need to learn about each other. The subject leads with severity and kind — [OxiCloud] data_loss: blob_unreadable (3) in blobs_consistency, or [OxiCloud] resolved: orphan_blob in blobs_consistency — and the body carries the same fields the generic webhook payload does, because mail is read where the panel is not open. Plain text, no HTML part. Delivery retries 3 times with 1s doubling backoff and a 20s timeout per attempt (SmtpEmailSender sets none of its own, and a sink runs on a job's terminal path), and a partial success across recipients is reported as a failure — "two of three operators were told" deserves the same attention as none of them being. A malformed address panics at boot rather than being dropped from the list: a channel delivering to one of two people is one an operator believes in more than it deserves. A dotless domain is accepted (root@localhost), since that is an ordinary address on a box relaying locally. Note that POST /api/admin/smtp/test tests the transport to an address you type in, not this list; GET /api/admin/notify/info shows the parsed recipients, the severity floor and the channels actually wired.
OXICLOUD_WEBHOOK_URL(unset)Outbound webhook endpoint — a destination, in the same sense OXICLOUD_SMTP_* is one, not a jobs feature. Job findings are its first consumer; whatever wants to post somewhere next should reuse this rather than introduce a second URL. Unset = no webhook. Verify it from the admin panel or POST /api/admin/webhook/test, which reports the receiver's HTTP status and response body and does not retry, so a dead host answers immediately. Real delivery does retry — 5 attempts with 1s doubling backoff — on a transport failure, a 429 or a 5xx; a 4xx is permanent and is not retried, because a wrong URL or a revoked token will not fix itself. If the URL is set but unusable (e.g. telegram with no target), the server refuses to start: an operator who configured alerting and got a running server without it would believe they are covered.
OXICLOUD_WEBHOOK_FORMATgenericPayload shape: generic (the alert's fields, flat, for your own receiver), slack, discord, teams, telegram, ntfy. Slack, Discord and Teams are not separate integrations — the same POST with the text under a different key — so none of them adds a vendor SDK or a proprietary dependency. ntfy also sets a title, tags, and a 1–5 priority (5 for appearing data loss, 4 for other appearances, 3 for a resolution, so an all-clear does not buzz a phone at 3am). An unknown value panics at boot: a typo'd slak would POST a body Slack ignores, which looks exactly like a webhook that was never called.
OXICLOUD_WEBHOOK_TARGET(unset)Recipient for the formats that carry it in the body rather than the URL: a Telegram chat id (URL is https://api.telegram.org/bot<TOKEN>/sendMessage) — a supergroup or channel id such as -1001234567890, where the leading - is real and supergroup ids start with -100; or @channelusername; or a positive id for a private chat with one user — or an ntfy topic (URL is the ntfy base, e.g. https://ntfy.sh/; the topic goes in the body because ntfy's POST /<topic> form treats the whole body as the message text, so JSON publishing goes to the root). Required for telegram and ntfy — the server refuses to start without it rather than discovering the gap at the first alert. Ignored by the other formats.
OXICLOUD_LEGACY_RECHUNKtrueDEPRECATED — removed in the next major release. The pre-CDC re-chunk migration is now the backend_rechunk job, present in the default OXICLOUD_JOBS_STARTUP above, which gives it an admin trigger, run history, findings and a resumable cursor — so "has this finished on my instance?" is answerable from the admin panel rather than from boot logs. Setting this to false still has its original effect: backend_rechunk is dropped from the default startup list, so no egress is spent re-reading every legacy blob at boot, and a deprecation warning is logged. It has no effect when OXICLOUD_JOBS_STARTUP is set explicitly, because that list is a direct statement about what runs at boot and is not second-guessed by another variable. To keep the deferral after the variable is gone, set OXICLOUD_JOBS_STARTUP explicitly without backend_rechunk and trigger the job by hand when the egress is convenient.

Database ​

VariableDefaultDescription
OXICLOUD_DB_CONNECTION_STRINGpostgres://postgres:postgres@localhost:5432/oxicloudPostgreSQL connection string
OXICLOUD_DB_MAX_CONNECTIONS20Max pool connections
OXICLOUD_DB_MIN_CONNECTIONS5Min pool connections
OXICLOUD_DB_MAINTENANCE_MAX_CONNECTIONS5Max connections in the isolated maintenance pool
OXICLOUD_DB_MAINTENANCE_MIN_CONNECTIONS1Min connections in the isolated maintenance pool

Build-Time SQLx ​

VariableDefaultDescription
DATABASE_URL—Build-time database URL for SQLx compile-time checks

Authentication ​

VariableDefaultDescription
OXICLOUD_JWT_SECRET(auto-generated)JWT signing secret; auto-persisted to <STORAGE_PATH>/.jwt_secret if unset
OXICLOUD_ACCESS_TOKEN_EXPIRY_SECS3600Access token lifetime (1 hour)
OXICLOUD_REFRESH_TOKEN_EXPIRY_SECS604800Refresh token lifetime (7 days); active sessions auto-renew on use
OXICLOUD_HASH_MEMORY_COST65536Argon2id memory cost in KiB (64 MiB). Server-side — used by the legacy password path (POST /api/auth/login) and the app-password Basic-Auth verifier. Distinct from OXICLOUD_AUTH_OPAQUE_KSF_* (client-side).
OXICLOUD_HASH_TIME_COST3Argon2id iteration count for the server-side legacy path.
OXICLOUD_HASH_PARALLELISM2Argon2id parallelism lanes for the server-side legacy path.
OXICLOUD_DISABLE_REGISTRATIONfalseDisable registration of new user accounts
OXICLOUD_REGISTRATION_ALLOWED_EMAIL_DOMAINS—Comma-separated allowlist of email domains accepted on POST /api/auth/register (case-insensitive, exact match on the post-@ part). Empty = any domain is allowed. Distinct from OXICLOUD_EXTERNAL_EMAIL_DOMAINS: this one gates SELF-registration (public sign-up), the external list gates INVITATIONS (grants + magic-link to third parties). An operator can lock sign-up to their company domain while leaving invitations open. Subdomains must be listed explicitly. Rejected registrations return 403 RegistrationDomainNotAllowed and emit an audit line. Example: mycompany.com,mycompany-eu.com.
OXICLOUD_AUTH_METHODSpassword,magic_linkComma-separated allowlist of auth methods (password, magic_link, oidc). Fail-fast: unknown token → boot panic; empty allowlist → boot panic; oidc in list without OXICLOUD_OIDC_ENABLED=true → boot panic. Removing password disables POST /api/auth/login (returns 403 PasswordLoginDisabled) and password-based register (returns 403 PasswordRegistrationDisabled). Removing magic_link disables POST /api/auth/magic-link/send (returns 403 MagicLinkLoginDisabled) and the redemption path for login-purpose tokens. Setting OXICLOUD_AUTH_METHODS=oidc is the cleanest "SSO-only" posture. Loose semantic (deprecation warning): if this list is explicitly set WITHOUT oidc but OXICLOUD_OIDC_ENABLED=true, OIDC is served regardless — a boot warning is emitted and this will become a fail-fast panic in the next major release. Startup gate: if magic_link is the only working method (no password, no oidc) AND no SMTP transport is configured (OXICLOUD_SMTP_HOST empty), the server refuses to start. OIDC master rule: when OIDC is enabled, magic-link login is hard-disabled regardless of this list (would otherwise bypass IdP-enforced MFA / step-up). Legacy alias (DEPRECATED): OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN=true still removes password from the list but emits a boot warning; removal in next major release.
OXICLOUD_AUTH_POLICIES—Comma-separated additive policy switches. Each token grants an exception or restriction to the default auth behaviour; empty (unset) = pure defaults. Recognised tokens: permit_magic_link_for_password_users (allow magic-link login for accounts that also have a password — off by default because magic-link would weaken the password to mailbox-strength; OIDC-linked users are still refused regardless); auto_redirect_if_standalone_oidc (when OIDC is the only working login method, auto-redirect the login page to the IdP instead of showing a click-to-continue button — off by default to avoid redirect loops on IdP failure and preserve logout UX).
OXICLOUD_REQUIRE_VERIFIED_EMAILfalseWhen true, POST /api/auth/login returns 403 EmailNotVerified for any account whose email_verified_at is NULL. Users can prove control by requesting a magic-link (whose redemption stamps email_verified_at), so this composes with magic_link in OXICLOUD_AUTH_METHODS to give users a self-service verification path. Admin-created (POST /api/admin/users) and setup-admin (POST /api/setup) users are auto-verified. OIDC-JIT users are also stamped verified at creation.
OXICLOUD_AUTH_MIN_PASSWORD_LENGTH8Minimum password length (UTF-8 bytes) enforced by every password-bearing endpoint: POST /api/setup, POST /api/auth/register, POST /api/admin/users, admin password reset, and PATCH /api/auth/me/password. Advertised on GET /api/config as auth.min_password_length so the SPA gates submit on the same value and users see the failure instantly instead of after a round-trip (fix for #677). Values below 8 are accepted but emit a boot warning — OWASP interactive-auth guidance is 8+ bytes.

OPAQUE aPAKE (zero-knowledge password login) ​

OPAQUE (RFC 9807) is a zero-knowledge password-authenticated key exchange: the passphrase never leaves the client, not on registration and not on login. It's shipped in stages (see docs/plan/opaque.md); this build carries the substrate only — endpoints are inert until OXICLOUD_AUTH_OPAQUE_MODE is set. See docs/config/authentication.md for the phase rollout, the migration plan, and admin-facing guidance.

VariableDefaultDescription
OXICLOUD_AUTH_OPAQUE_MODEoffRuntime mode. off = endpoints 404 (default). migrate = endpoints live, legacy POST /api/auth/login still accepted. opaque_only = endpoints live, legacy refused for users with an envelope. Effective-mode cross-check: when password is not in OXICLOUD_AUTH_METHODS, the mode is auto-downgraded to off with an audit-channel INFO line (OPAQUE only replaces the password path — nothing to shadow in an OIDC-only or magic-link-only deployment). So OIDC / magic-link-only operators can safely ignore every OXICLOUD_AUTH_OPAQUE_* variable.
OXICLOUD_AUTH_OPAQUE_SERVER_SETUP—Base64-encoded ServerSetup blob. Required when OXICLOUD_AUTH_OPAQUE_MODE != off AND password is enabled — the server refuses to start with a helpful error otherwise. Generate once with the oxicloud opaque setup subcommand and persist the value like your JWT secret. Never rotate — rotating invalidates every user's envelope (they'd all need to reset their passphrase).
OXICLOUD_AUTH_OPAQUE_KSF_MEMORY_KIB47104Client-side Argon2id memory cost in KiB (46 MiB — matches OWASP interactive-auth recommendation). Runs on the user's device during OPAQUE login/registration, TWICE per login. Distinct from OXICLOUD_HASH_MEMORY_COST (server-side legacy path). Bumping raises brute-force cost after a hypothetical envelope leak but also raises login latency and risks WASM heap OOM on low-memory devices — see authentication.md § OPAQUE — KSF parameters for the full rationale + per-device latency table.
OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS1Client-side Argon2id iteration count (OWASP interactive-auth recommendation).
OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM1Client-side Argon2id parallelism lanes (OWASP recommendation). Higher only helps on multi-core hardware and can hurt single-core / older mobile devices.

DPoP cryptographically binds a session cookie to a browser-held ECDSA keypair (P-256, non-extractable via SubtleCrypto). Every request carries a signed proof the middleware verifies against the session's binding. Closes the info-stealer replay threat: a cookie copied to another machine is useless without the private key. Non-browser clients (Nextcloud sync via app passwords, CLI via device-authorization) never bind and are exempted at the middleware regardless of mode. See docs/plan/dpop.md for the full rollout plan.

VariableDefaultDescription
OXICLOUD_DPOP_MODEoffEnforcement mode. off = middleware pass-through (default, ship-safe). opportunistic = verify when a proof is present, log dpop.header_missing_but_session_bound audit when absent on a bound session, but allow the request through (rollout mode — catches client bugs). required = bound sessions MUST present a valid proof or 401. Unbound sessions always exempt. Recommended rollout: off → opportunistic for 2-4 weeks → required. For DPoP to be meaningful, cookies must be Secure (OXICLOUD_COOKIE_SECURE=true in production over HTTPS) and reverse-proxy X-Forwarded-Proto / X-Forwarded-Host must reach the app so the htu claim canonicalises correctly.

Rate Limiting & Account Lockout ​

VariableDefaultDescription
OXICLOUD_RATE_LIMIT_LOGIN_MAX10Max login attempts per IP per window
OXICLOUD_RATE_LIMIT_LOGIN_WINDOW_SECS60Login rate-limit window (seconds)
OXICLOUD_RATE_LIMIT_REGISTER_MAX5Max registration attempts per IP per window
OXICLOUD_RATE_LIMIT_REGISTER_WINDOW_SECS3600Registration rate-limit window (seconds)
OXICLOUD_RATE_LIMIT_REFRESH_MAX20Max token refresh attempts per IP per window
OXICLOUD_RATE_LIMIT_REFRESH_WINDOW_SECS60Refresh rate-limit window (seconds)
OXICLOUD_RATE_LIMIT_USER_PROFILE_MAX60Max user-profile lookups per caller per window
OXICLOUD_RATE_LIMIT_USER_PROFILE_WINDOW_SECS60User-profile lookup window (seconds)
OXICLOUD_RATE_LIMIT_DELTA_UPLOAD_MAX240Max delta-upload requests per caller per window
OXICLOUD_RATE_LIMIT_DELTA_UPLOAD_WINDOW_SECS60Delta-upload window (seconds)
OXICLOUD_LOCKOUT_MAX_FAILURES5Consecutive failed logins before account lockout
OXICLOUD_LOCKOUT_DURATION_SECS900Account lockout duration (15 minutes)

Login, register and refresh are keyed on the client IP and guard the unauthenticated front door. User-profile and delta-upload are keyed on the caller id and guard an authenticated user against exhausting a shared resource.

That distinction decides which knob to reach for. When several actors share one identity — a CI suite, an integration bot, a kiosk account — they share one caller bucket, and a ceiling sized for a single human is easily exceeded while the IP-keyed limits sit untouched. Raising the login/register/refresh limits does nothing in that case.

Exceeding the user-profile limit returns 429, which in a browser usually shows up as owner names failing to resolve in file listings rather than as a visible error.

Feature Flags ​

VariableDefaultDescription
OXICLOUD_ENABLE_AUTHtrueEnable authentication
OXICLOUD_ENABLE_FILE_SHARINGtrueFile/folder sharing
OXICLOUD_ENABLE_TRASHtrueTrash / recycle bin
OXICLOUD_ENABLE_SEARCHtrueFull-text and metadata search
OXICLOUD_ENABLE_MUSICtrueMusic playlists and audio metadata
OXICLOUD_ENABLE_MARKDOWN_COLLABtrueCollaborative editing of .md / .txt / code / config files (Yjs CRDT) over the message-bus WebSocket. Requires OXICLOUD_MESSAGEBUS_ENABLE=true — the CRDT data plane rides /api/rt/ws, so the boot refuses the contradictory combination (enable_markdown_collab=true with the bus off) rather than silently doing nothing. When true, the server wires CollabSessionService and the WS binary-frame branch routes Yjs sync/awareness/update frames to per-file actors backed by the collab.doc_sessions table; the FE mounts CollabEditor (CodeMirror 6 + y-codemirror.next) for text-shaped files, with peer cursors, per-file can_write gating from the subscribe ack, read-only-for-Viewers, graceful rt.write_denied on Update denial, and explicit flush on tab hide / unmount. Eviction surface covers resource_deleted, grant_revoked, external_write (WebDAV / WOPI / REST replace) and doc_too_large — all fire rt.revoked / rt.write_denied on the collab topic so attached editors transition to read-only within one tick. Reader seeds the CRDT from the file's current blob text on first attach; writer flushes back via a debouncer (defaults 15 s idle / 60 s max) so WebDAV / download / search see CRDT-current content within a bounded window. Per-frame AuthZ (Read on SYNC, Update on UPDATE) is enforced. Set to false only if you specifically don't want live Yjs routing — e.g. a read-only-clients-only deployment or one whose users write text files exclusively through external editors and expect last-write-wins semantics. Debouncer + idle-GC + doc-size-cap tuning knobs listed below. See docs/plan/markdown-collab.md.
OXICLOUD_ENABLE_VIDEO_THUMBNAILStrueServer-side single-frame thumbnail extraction from uploaded videos (one frame → WebP). Requires ffmpeg on PATH (override with OXICLOUD_FFMPEG_PATH). When true and ffmpeg is missing at boot, a WARN log is emitted and videos fall back to a placeholder icon. Set to false to skip the ffmpeg lookup entirely — useful on hosts where ffmpeg can't be installed, or when the client uploads video previews itself (some desktop/mobile clients generate thumbnails locally and POST them alongside the video).
OXICLOUD_FFMPEG_PATHffmpeg (on PATH)Absolute path to the ffmpeg binary. Ignored when OXICLOUD_ENABLE_VIDEO_THUMBNAILS=false. Useful for pinning a specific static build or when ffmpeg lives outside the default PATH.
OXICLOUD_VIDEO_THUMBNAIL_CONCURRENCYhalf the CPU cores (min 1)Maximum number of ffmpeg extractions running in parallel.
OXICLOUD_VIDEO_THUMBNAIL_TIMEOUT_SECS30Per-video ffmpeg timeout; a slower extraction is abandoned and the video gets a placeholder icon.
OXICLOUD_VIDEO_THUMBNAIL_MAX_MB2048Largest video (in MB) streamed to a temp file for frame extraction; bigger files skip thumbnailing.
OXICLOUD_EXPOSE_SYSTEM_USERStrueExpose other OxiCloud users as a read-only address book at GET /api/address-books
OXICLOUD_ENABLE_EXTERNAL_MOUNTSfalseExternal file mounts — surface host-filesystem paths (or a future S3/WebDAV/SFTP backend) as folders inside a user's drive. Admins configure mount rows via the "External Mounts" admin panel; each row picks a target drive, mount-root name, provider kind, and provider config. When false: admin CRUD (/api/admin/external-mounts) is not registered (404); the "External Mounts" tab is hidden from the admin sidebar (FE gates on serverConfig.features.external_mounts, same discovery path as message_bus); MountRegistry stays empty at boot even if mount rows exist in the DB, so mount-root folders resolve as empty native folders. When true: CRUD exposed, existing DB rows load at boot via MountRegistry::reload, users can browse mount contents. Opt-in per deployment because external mounts expose host filesystems (or credentialed remote backends) inside user drives — an admin misconfiguration can leak state that isn't part of OxiCloud's normal storage substrate. Keep off unless you have a concrete need.
OXICLOUD_GRANT_CLEANUP_ENABLEDtrueBackground daemon that deletes expired rows from storage.role_grants. The authorization engine already filters expired grants out of every permission check at read time (expires_at IS NULL OR expires_at > NOW()), so leaving expired rows in place is a hygiene issue — not a security one. This daemon garbage-collects them daily. Set to false to keep every expired grant row forever (uncommon; a fresh install rarely wants this).
OXICLOUD_GRANT_CLEANUP_GRACE_DAYS15Days past a grant's expires_at before the row is eligible for deletion. The grace window preserves the audit / support answer to "what happened to my access?" for a couple of weeks past expiration. Values below 1 are legal but discouraged — the recommendation is ≥ 15 days. Values above the actual grant TTL used by clients waste index space; a few weeks is the sweet spot.
OXICLOUD_GRANT_CLEANUP_INTERVAL_HOURS24How often the grant-cleanup daemon fires. Clamped to a minimum of 1 hour. Adjusting this doesn't change what gets deleted — only how promptly. Daily is fine for any realistic grant volume.
OXICLOUD_NOTIFICATIONS_RETENTION_DAYS30Retention window for read notification rows (notif.notifications). The notifications_cleanup scheduled job runs daily and deletes rows where read_at IS NOT NULL and read_at < now() - retention_days. Unread rows are preserved unconditionally — the whole point of the durable table is that a user offline for a month still sees the share-granted notice on next login. Clamped to a minimum of 1 (0 would purge every read row on every tick). Adjust down for compliance-sensitive deployments where "cleared once seen" matters; adjust up when operators expect users to reference old notifications for support.
OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX@driveNative WebDAV URL segment that renders the caller's drive list. Sanitized by trimming leading/trailing /. Three shapes: (1) default @drive — /webdav/… addresses the caller's default personal drive (back-compat), /webdav/@drive/ returns the drive listing, /webdav/@drive/<uuid|name>/… targets a specific drive. (2) empty string "" — /webdav/ IS the drive listing, /webdav/<uuid|name>/… targets a specific drive, no default-drive shortcut. (3) any other string (e.g. drives) — same shape as @drive with that segment substituted. Only drives the caller has Read on via role_grants resolve.

Collab tuning (only relevant when OXICLOUD_ENABLE_MARKDOWN_COLLAB=true) ​

The debouncer flushes CRDT text back to the file's blob on two thresholds: idle (no update for IDLE_MS) OR max age (continuously edited for MAX_MS since the first dirty edit). Idle bounds "how quiet before we flush"; max bounds worst-case staleness for WebDAV/download/search on a doc that never quiets down. TICK_MS is the actor's wake-up cadence — smaller = faster response to thresholds, more overhead. Production defaults are Y-Sweet-shaped (15 s / 60 s / 1 s); test suites shrink them to sub-second scales.

The idle-GC scheduled job reaps stale collab.doc_sessions rows: force-flush → actor shutdown → row delete. TTL_MINUTES is the "how long can a session lie idle" contract; SCAN_INTERVAL_SECS is how tightly the contract is enforced — scanning less often than the TTL effectively extends the TTL. BATCH_LIMIT caps per-tick work so a large backlog can't monopolise the maintenance pool.

VariableDefaultDescription
OXICLOUD_COLLAB_DEBOUNCE_IDLE_MS15000 (15 s)Debouncer idle threshold: no UPDATE for this long → flush fires. Lower = fresher blob content, more writes.
OXICLOUD_COLLAB_DEBOUNCE_MAX_MS60000 (60 s)Debouncer max age: continuously edited for this long → force-fire regardless of idle. Bounds worst-case staleness for non-collab consumers.
OXICLOUD_COLLAB_DEBOUNCE_TICK_MS1000 (1 s)Actor wake-up cadence for the flush-due check. Setting this larger than the idle threshold makes the idle threshold effectively meaningless.
OXICLOUD_COLLAB_IDLE_TTL_MINUTES30Idle-GC TTL: a session with no apply_update for this many minutes is eligible for reaping on the next scan. Clamped to a minimum of 1.
OXICLOUD_COLLAB_IDLE_SCAN_INTERVAL_SECS300 (5 min)How often the idle-GC job fires. Scanning much less often than the TTL turns the TTL into a fiction — the effective TTL becomes ~TTL + SCAN_INTERVAL.
OXICLOUD_COLLAB_IDLE_BATCH_LIMIT200Maximum sessions reaped in one tick. A backlog carries to the next tick — the scan walks last_activity_at ASC so the coldest rows are evicted first. Clamped to a minimum of 1.
OXICLOUD_COLLAB_MAX_DOC_BYTES10485760 (10 MB)Ceiling on the actor's encoded Yjs state. An UPDATE that would push the running total over this is refused with rt.write_denied { reason: "doc_too_large" } and the socket stays alive. Text collaboration on markdown / code stays well under this; a caller trying to grow past the cap is either a runaway client or a resource-exhaustion attempt.

Boot-time refuse-to-boot checks ​

Some upgrades add invariants the running database must satisfy BEFORE the new binary can serve traffic. These are enforced by read-only checks that run after sqlx::migrate!() and before the server binds a listen socket. If a check fails, the server exits with a FATAL message spelling out the exact CLI command to run.

The server never silently mutates data at boot — every fix is an explicit oxicloud migrate <name> invocation. Follows the "discovery-only by default, mutation opt-in" rule that also governs the consistency-check jobs.

lowercase-usernames ​

Three outcomes at boot, only one of which stops the server:

  • All lowercase (or NULL) — the check is a no-op, boot proceeds unchanged.
  • Mixed-case rows exist, no LOWER(username) collision — boot auto-lowercases them in one atomic transaction, emits a structured audit line per rename (user.username_lowercased_on_boot, INFO) plus an INFO summary (user.usernames_lowercased_on_boot_summary with renamed=N), and continues. Silent action is confined to the case with exactly one correct move: Alice (with no alice row) becomes alice.
  • LOWER(username) collision — two or more active rows share the same lowercase form (e.g. Alice + alice). Boot refuses to start with a FATAL message spelling out every collision group and the exact CLI command to resolve it. Tiebreak needs a human.

Soft-deleted / disabled accounts (active = false) and NULL usernames (OPAQUE-migrated) are skipped in every case.

Refusal message pattern: FATAL: cannot start — N colliding username group(s) (M affected account(s) in total), followed by each group's canonical form and its members with id + last_login. Up to 10 groups shown; the --dry-run CLI reveals the full list.

Fix (only required when the server refused):

oxicloud migrate lowercase-usernames --dry-run     # preview the tiebreak
oxicloud migrate lowercase-usernames               # apply

What the migration does: lowercases every mixed-case username. On collision (Alice + alice both exist), the tiebreak (last_login_at DESC NULLS LAST, created_at ASC) picks a winner; losers get alice-2, -3, … as a suffix. Sessions and grants survive the rename — both key on the user's UUID.

Client compat: NextCloud clients that cached URLs like /remote.php/dav/files/Alice/… continue to work indefinitely — the Basic Auth middleware and URL parser both lowercase on decode. NC desktop clients will prompt a one-time re-sync on first PROPFIND after upgrade; DAVX5 and NC mobile handle it silently. See docs/install/binary.md § Upgrading from a case-sensitive-usernames release for the full upgrade flow.

Design: docs/plan/username-lowercase.md.

Storage Entries (multi-entry, recommended) ​

Declare one or more named storage backends. The one the app runs on is picked from the DB (admin_settings.storage.active_backend_name); the admin panel's storage tab flips the pointer, and cross-backend migration is a recoverable job that copies blobs between two entries with a read-only safety window. See Admin Settings — Storage & Migration for the operator flow and the multi-entry design doc for the full model.

VariableDefaultDescription
OXICLOUD_STORAGE_ENTRIES—Comma-separated allowlist of entry names. Names must match [a-z0-9_-]{1,32} and be unique. Order is preserved (the first entry is the fallback when the DB pointer is unset — fresh install).

Each declared name <N> then reads its own set of per-entry variables:

VariableDefaultDescription
OXICLOUD_STORAGE_<N>_BACKEND—Backend type for entry <N>: local | s3 | azure (required per entry)
OXICLOUD_STORAGE_<N>_ROOT_DIROXICLOUD_STORAGE_PATHLocal-only: root directory for this entry's .blobs/. Falls back to the ambient OXICLOUD_STORAGE_PATH when unset.
OXICLOUD_STORAGE_<N>_S3_BUCKET—S3-only: bucket name (required when backend=s3)
OXICLOUD_STORAGE_<N>_S3_REGIONus-east-1S3-only: AWS region
OXICLOUD_STORAGE_<N>_S3_ENDPOINT_URL—S3-only: custom endpoint for non-AWS providers
OXICLOUD_STORAGE_<N>_S3_ACCESS_KEY—S3-only: access key ID
OXICLOUD_STORAGE_<N>_S3_SECRET_KEY—S3-only: secret access key
OXICLOUD_STORAGE_<N>_S3_FORCE_PATH_STYLEfalseS3-only: path-style URLs (required for MinIO, R2)
OXICLOUD_STORAGE_<N>_AZURE_ACCOUNT_NAME—Azure-only: storage account name
OXICLOUD_STORAGE_<N>_AZURE_ACCOUNT_KEY—Azure-only: storage account key
OXICLOUD_STORAGE_<N>_AZURE_CONTAINER—Azure-only: blob container name (required when backend=azure)
OXICLOUD_STORAGE_<N>_AZURE_SAS_TOKEN—Azure-only: SAS token (alternative to account key)
OXICLOUD_STORAGE_<N>_AZURE_ENDPOINT_URL—Azure-only: custom endpoint (Azurite, private deployments)
OXICLOUD_STORAGE_<N>_ENCRYPTION_KEY—Comma-separated list of <cipher>:<base64 key> pairs (or bare <base64 key>, which defaults to aes-256-gcm). Presence implies encryption is enabled on this entry — no separate enable flag. The LAST pair wins on writes; every pair is a candidate for reads. Supported ciphers: aes-256-gcm and none (empty-key sentinel used at pair-list head/tail for encrypt/decrypt-in-place rotations). Bad base64, wrong length, duplicate keys, or multiple none pairs abort boot. See Storage key rotation for rotation recipes.

Fail-fast rules (boot aborts with actionable message):

  • A declared name whose required per-entry fields are missing (_BACKEND never set, S3 with no _S3_BUCKET, Azure with no _AZURE_CONTAINER).
  • Setting OXICLOUD_STORAGE_ENTRIES alongside any of the legacy flat vars below (OXICLOUD_STORAGE_BACKEND, OXICLOUD_S3_*, OXICLOUD_AZURE_*, OXICLOUD_STORAGE_ENCRYPTION_*). Pick one mode; the error lists every conflicting var to remove.
  • A DB pointer (admin_settings.storage.active_backend_name) that names an entry not in the current _ENTRIES. The error points at the repair flag oxicloud storage select <name> — verify + UPDATE DB + exit.

Example — two entries, local disk plus an S3 target for planned migration:

OXICLOUD_STORAGE_ENTRIES=local_main,s3_prod

OXICLOUD_STORAGE_local_main_BACKEND=local
OXICLOUD_STORAGE_local_main_ROOT_DIR=/srv/oxicloud

OXICLOUD_STORAGE_s3_prod_BACKEND=s3
OXICLOUD_STORAGE_s3_prod_S3_BUCKET=my-oxicloud-bucket
OXICLOUD_STORAGE_s3_prod_S3_REGION=us-east-1
OXICLOUD_STORAGE_s3_prod_S3_ACCESS_KEY=…
OXICLOUD_STORAGE_s3_prod_S3_SECRET_KEY=…
OXICLOUD_STORAGE_s3_prod_ENCRYPTION_KEY=aes-256-gcm:…  # openssl rand -base64 32

# Rotation window (two pairs, last wins on writes):
# OXICLOUD_STORAGE_s3_prod_ENCRYPTION_KEY=aes-256-gcm:<OLD>,aes-256-gcm:<NEW>

Storage Backend (DEPRECATED — legacy single-backend) ​

⚠️ Deprecated. Use Storage Entries above for new deployments. These flat variables still work when OXICLOUD_STORAGE_ENTRIES is unset — the parser then synthesises one entry named default from them, keeping pre-multi-entry .env files booting unchanged. Booting via this path emits a storage.legacy_flat_vars_deprecated warning so operators see it in logs. Removal target: not yet fixed; migrate at your convenience by moving each variable below into OXICLOUD_STORAGE_<NAME>_* form under an entry declared in OXICLOUD_STORAGE_ENTRIES. Setting any variable from this section alongside OXICLOUD_STORAGE_ENTRIES is a fail-fast boot error — pick one mode.

VariableDefaultDescription
OXICLOUD_STORAGE_BACKENDlocalBlob storage backend: local, s3, or azure

S3-Compatible (AWS S3, Backblaze B2, Cloudflare R2, MinIO) ​

Used when OXICLOUD_STORAGE_BACKEND=s3.

VariableDefaultDescription
OXICLOUD_S3_BUCKET—S3 bucket name (required)
OXICLOUD_S3_REGIONus-east-1AWS region
OXICLOUD_S3_ACCESS_KEY—Access key ID
OXICLOUD_S3_SECRET_KEY—Secret access key
OXICLOUD_S3_ENDPOINT_URL—Custom endpoint for non-AWS providers (e.g. https://s3.example.com)
OXICLOUD_S3_FORCE_PATH_STYLEfalseForce path-style URLs (required for MinIO, R2)

Azure Blob Storage ​

Used when OXICLOUD_STORAGE_BACKEND=azure.

VariableDefaultDescription
OXICLOUD_AZURE_ACCOUNT_NAME—Storage account name (required)
OXICLOUD_AZURE_ACCOUNT_KEY—Storage account key
OXICLOUD_AZURE_CONTAINER—Blob container name (required)
OXICLOUD_AZURE_SAS_TOKEN—SAS token (alternative to account key)

Local Disk Cache for Remote Backends ​

A least-recently-used disk cache that can speed up repeated reads from S3 or Azure.

VariableDefaultDescription
OXICLOUD_STORAGE_CACHE_ENABLEDfalseEnable LRU disk cache
OXICLOUD_STORAGE_CACHE_MAX_SIZE53687091200Max cache size in bytes (50 GB)
OXICLOUD_STORAGE_CACHE_PATH{STORAGE_PATH}/.blob-cacheCache directory

Client-Side Encryption (DEPRECATED — per-entry key is the new home) ​

AES-256-GCM encryption applied to blobs before they are written to any backend.

⚠️ Deprecated. Prefer per-entry OXICLOUD_STORAGE_<NAME>_ENCRYPTION_KEY under an entry declared in OXICLOUD_STORAGE_ENTRIES — presence of the key implies encryption is enabled on that entry (no separate flag), and multi-entry enables cross-key rotation via migration to a new entry. The flat vars below still work in zero-entries mode and get folded into the synthesised default entry, alongside the same deprecation warning at boot.

VariableDefaultDescription
OXICLOUD_STORAGE_ENCRYPTION_ENABLEDfalseEnable at-rest blob encryption
OXICLOUD_STORAGE_ENCRYPTION_KEY—Base64-encoded 32-byte encryption key; generate with openssl rand -base64 32

Retry Policy (Remote Backends) ​

Exponential backoff retries for transient errors on S3 and Azure.

VariableDefaultDescription
OXICLOUD_STORAGE_RETRY_ENABLEDtrueEnable retry with exponential backoff
OXICLOUD_STORAGE_RETRY_MAX_RETRIES3Maximum retry attempts for the backend decorator. On S3 the effective budget is multiplicative, not this number: the AWS SDK retries beneath this layer with its own policy (standard mode, 3 attempts, jittered), so the total is roughly this × the SDK's. The two are deliberate and complementary — the SDK is the better classifier, distinguishing throttling from 5xx from a dead connection, while this is the uniform outer bound across every backend. It is also not a stable multiplier: the SDK's standard mode uses a retry token bucket, so its contribution shrinks under sustained failure, which is exactly the regime an outage creates. Use RUST_LOG=aws_smithy_runtime=debug to see the SDK's own attempts if the real number matters.
OXICLOUD_STORAGE_RETRY_INITIAL_BACKOFF_MS100Initial backoff in milliseconds
OXICLOUD_STORAGE_RETRY_MAX_BACKOFF_MS10000Maximum backoff cap in milliseconds
OXICLOUD_STORAGE_RETRY_BACKOFF_MULTIPLIER2.0Backoff multiplier per retry

OIDC / SSO ​

See the OIDC configuration guide for details.

VariableDefaultDescription
OXICLOUD_OIDC_ENABLEDfalseEnable OIDC
OXICLOUD_OIDC_ISSUER_URL—OIDC issuer URL
OXICLOUD_OIDC_CLIENT_ID—Client ID
OXICLOUD_OIDC_CLIENT_SECRET—Client secret
OXICLOUD_OIDC_REDIRECT_URIhttp://localhost:8086/api/auth/oidc/callbackCallback URL (must match IdP config)
OXICLOUD_OIDC_SCOPESopenid profile emailRequested scopes
OXICLOUD_OIDC_FRONTEND_URLhttp://localhost:8086Frontend URL to redirect to after login
OXICLOUD_OIDC_AUTO_PROVISIONtrueAuto-create users on first SSO login (JIT provisioning)
OXICLOUD_OIDC_AUTO_LINK_EMAIL_MATCHtrueWhen subject-lookup misses on an OIDC login BUT the IdP-returned email (with email_verified=true) matches an existing local user (after +alias normalization), auto-link the OIDC identity to that user instead of refusing. Refuses on ambiguity (>1 local user normalises to same email) or if the matched user is already linked to a different identity. Safe under single-IdP trust model (admin chose the IdP); unsafe for future multi-IdP federation. Set false for postures requiring explicit consent for every link. See OIDC account linking plan.
OXICLOUD_OIDC_ADMIN_GROUPS—Comma-separated OIDC groups that grant admin role
OXICLOUD_OIDC_DISABLE_PASSWORD_LOGINfalseDEPRECATED — emits boot warning; slated for removal in next major release. Use OXICLOUD_AUTH_METHODS=oidc (and optionally OXICLOUD_AUTH_POLICIES=auto_redirect_if_standalone_oidc for server-side /login redirect) instead. Still removes password from the effective allowlist when set to true — kept working so upgrading deployments don't break.
OXICLOUD_OIDC_PROVIDER_NAMESSODisplay name for the provider shown in UI

WOPI (Office Editing) ​

See the WOPI configuration guide for details.

VariableDefaultDescription
OXICLOUD_WOPI_ENABLEDfalseEnable WOPI
OXICLOUD_WOPI_DISCOVERY_URL—Collabora/OnlyOffice discovery URL
OXICLOUD_WOPI_BASE_URLOXICLOUD_BASE_URLURL the editor uses to call OxiCloud's /wopi/* endpoints
OXICLOUD_WOPI_PUBLIC_BASE_URLOXICLOUD_WOPI_BASE_URLURL the browser uses to open OxiCloud's WOPI host page
OXICLOUD_WOPI_SECRET(JWT secret)WOPI token signing key
OXICLOUD_WOPI_TOKEN_TTL_SECS86400Token lifetime (24 hours)
OXICLOUD_WOPI_LOCK_TTL_SECS1800Lock expiration (30 minutes)

When Collabora or OnlyOffice runs on a different hostname, set OXICLOUD_WOPI_PUBLIC_BASE_URL to the public OxiCloud URL that the browser can reach. If the editor reaches OxiCloud through a different internal URL, also set OXICLOUD_WOPI_BASE_URL for those callbacks.

Nextcloud Compatibility ​

Enables the Nextcloud-compatible API layer (/remote.php/, /ocs/, /status.php, Login Flow v2) for clients that use the Nextcloud protocol.

VariableDefaultDescription
OXICLOUD_NEXTCLOUD_ENABLEDfalseEnable Nextcloud compatibility layer
OXICLOUD_NEXTCLOUD_INSTANCE_IDocncaInstance ID suffix used in oc:id formatting
OXICLOUD_NEXTCLOUD_VERSION28.0.4Emulated Nextcloud version reported to clients (format: major.minor.patch)

Outbound Email (SMTP) ​

Used by the magic-link invitation flow and the login-via-email flow. When OXICLOUD_SMTP_HOST is empty (the default), the feature is disabled and any endpoint that needs email returns 503.

VariableDefaultDescription
OXICLOUD_SMTP_HOST—SMTP server hostname or IP. Empty disables the feature.
OXICLOUD_SMTP_PORT587Submission port (587 STARTTLS, 465 implicit TLS, 25 plain)
OXICLOUD_SMTP_USER—SASL username. Leave empty for anonymous relay.
OXICLOUD_SMTP_PASS—SASL password
OXICLOUD_SMTP_FROM—From: mailbox; bare address or RFC 5322 name-address (OxiCloud <noreply@example.com>)
OXICLOUD_SMTP_TLSstarttlsTransport encryption: starttls, tls, or none (emits startup WARN)

There is also OXICLOUD_SMTP_MOCK (false by default), this is for test purpose only, do not activate it

Reliability and retries ​

OxiCloud does not spool mail. Each send() is a single attempt: if the remote SMTP server is unreachable, slow, or temporarily refusing the message, the send fails and the error is logged — there is no in-process retry, queue, or dead-letter handling. This keeps the HTTP path fast and the binary small at the cost of durability guarantees during a relay outage.

For production deployments where you cannot afford to drop invitation mail during a brief relay outage, point OxiCloud at a local MTA configured as a smarthost (Postfix, OpenSMTPD, exim, or msmtp-mta/nullmailer for minimal setups). The local MTA owns the durable queue: it accepts the message from OxiCloud in milliseconds over the loopback, then retries with its own exponential backoff against your real upstream relay until the message is delivered or the queue lifetime expires.

Typical local-relay config:

env
OXICLOUD_SMTP_HOST=127.0.0.1
OXICLOUD_SMTP_PORT=25
OXICLOUD_SMTP_TLS=none           # loopback only — never over the network
OXICLOUD_SMTP_FROM=OxiCloud <noreply@example.com>
# OXICLOUD_SMTP_USER / _PASS unset — local MTA accepts loopback unauthenticated

Then configure the local MTA's smarthost / relayhost to your upstream provider (SendGrid, Amazon SES, your corporate relay, etc.). Verify durability by stopping the upstream relay, sending an invitation, restarting the relay, and confirming the mail eventually arrives.

If you point OXICLOUD_SMTP_HOST directly at a remote SMTP server, treat the absence of retries as a documented constraint: a brief network glitch during invitation flow is a lost invite, and the recipient will need to be re-invited.

Configures the invite-by-email and login-via-email flows. Both require SMTP to be configured above.

VariableDefaultDescription
OXICLOUD_MAGIC_LINK_TTL_HOURS24Lifetime of a freshly-minted magic-link token, in hours
OXICLOUD_ALLOW_EXTERNAL_USERStrueKill switch for the whole flow. false makes POST /api/grants reject subject.type = "email" for unknown addresses and POST /api/auth/magic-link/send return its uniform stub without issuing a token.
OXICLOUD_EXTERNAL_EMAIL_DOMAINS—Comma-separated allowlist of email domains accepted when minting a new external user (case-insensitive, exact match on the post-@ part). Empty = any domain is allowed, subject to OXICLOUD_ALLOW_EXTERNAL_USERS. Subdomains must be listed explicitly: partner.com does NOT match eng.partner.com. Example: partner-a.com,partner-b.io.
OXICLOUD_NOTIFY_INTERNAL_USERS_ON_SHAREtrueOperator-level kill switch for the plain-notification email arm — the "Alice shared 'Project Alpha' with you" mail that fires when the recipient is a password user or OIDC user (i.e. not magic-link eligible). false suppresses the arm entirely; internal users discover new shares only at next login. A coarser knob than the per-user auth.users.notify_on_share column; when this is false the user-level opt-in does not matter. External-user magic-link first-invitations are unaffected and always send.

Internationalization (server-rendered surfaces) ​

Server-rendered HTML pages (magic-link landing, error pages) and outbound transactional emails go through the backend i18n layer. The set of available locales is discovered at boot by listing static/locales/*.json — no rebuild needed to add a 17th locale.

VariableDefaultDescription
OXICLOUD_DEFAULT_LOCALEenFallback locale used when no stronger signal is available. Must match one of the locales under static/locales/; startup fails fast if you set it to a code with no corresponding JSON file.

The resolution priority differs by surface:

  • HTML pages (anonymous, e.g. magic-link landing) — ?lang=xx query override, then the browser's Accept-Language header (q-weighted, with primary-tag fallback so fr-FR resolves to fr when no fr-FR.json is shipped), then this default.
  • Emails to a known user — the user's preferred_locale column (set via OIDC locale claim at JIT or via the UI language switcher), then this default.
  • Emails to a brand-new external user being invited — the inviter's preferred_locale (inheritance at row-creation), then this default.

Today's shipped locales: ar, de, en, es, fa, fr, hi, it, ja, ko, nl, pl, pt, ru, zh, zh-TW. Missing translations on a non-English locale automatically fall back to English at the key level — adding a new locale with even a few translated keys works without manual gap-filling.

Trusted Proxy ​

VariableDefaultDescription
OXICLOUD_TRUST_PROXY_CIDR—Comma-separated list of trusted proxy CIDRs; enables X-Forwarded-For / X-Real-IP extraction for those source IPs
OXICLOUD_TRUST_PROXY_HEADERS—Deprecated. Use OXICLOUD_TRUST_PROXY_CIDR instead

Example: OXICLOUD_TRUST_PROXY_CIDR=127.0.0.1/32,10.0.0.0/8,172.16.0.0/12

Message bus ​

VariableDefaultDescription
OXICLOUD_MESSAGEBUS_ENABLEtrueMaster switch for the message bus. When false, the routes /api/rt/ws and POST /api/rt/ticket are not registered at boot — Axum returns 404 Not Found for both, keeping monitoring dashboards free of 5xx noise. Publish sites in the services stay unchanged (the in-process bus still runs, publishes to nobody are cheap no-ops), so no service code path branches on this flag — the toggle is purely at the API surface. Clients discover this via GET /api/config.features.message_bus and skip WS setup entirely (no reconnect flood, no wasted round-trips). Why an operator might turn it off: each logged-in browser holds a persistent WebSocket connection while a folder view is open. N users × M tabs = N × M sustained TCP + TLS + WS sessions on the server, each consuming an fd, ~a few KB of tokio task state, and any tuple your L4/L7 load balancer keeps for the flow. On tightly-provisioned VPS deployments (low fd ulimit, tight memory), behind WebSocket-hostile reverse proxies that can't be reconfigured, or during an operational triage where you want to shed WS load, set this to false — the SPA transparently falls back to its pre-message-bus behavior (updates land on the next navigation / refresh instead of live).
OXICLOUD_MESSAGEBUS_KEEPALIVE_SECONDS30Server-initiated protocol Ping interval on /api/rt/ws. Prevents intermediate proxies (Traefik, nginx, Cloudflare) and NAT boxes from reaping the TCP session as idle. Read at each WS connect — a change takes effect on new connections, no restart needed. Set 0 or any non-positive value to fall back to the default.

Tuning the keepalive interval: 30 s is comfortably under nginx's 60 s proxy_read_timeout default and Cloudflare's 100 s hard limit. Behind Traefik with respondingTimeouts.idleTimeout bumped to 3600s (as documented in the reverse-proxy setup), you can leave this at 30 s or raise it — the interval should sit at most half the smallest hop's idle timeout so a single missed Ping doesn't reap the connection.

Rename note (feat/message-bus branch): OXICLOUD_RT_WS_KEEPALIVE_SECONDS was renamed to OXICLOUD_MESSAGEBUS_KEEPALIVE_SECONDS — hard cutover, no fallback. Update any .env file that set the old name.

Allocator Tuning ​

These variables are read directly by mimalloc, not by OxiCloud's config parser.

VariableDefaultDescription
MIMALLOC_PURGE_DELAY0Delay in ms before freed memory is returned to the OS (0 = immediately, recommended for Docker)
MIMALLOC_ALLOW_LARGE_OS_PAGES0Enable 2 MiB huge pages (0 = off, recommended for Docker to avoid THP RSS inflation)

Internal Defaults (not configurable via env) ​

ParameterDefault
File cache TTL60 s
Directory cache TTL120 s
Max cache entries10 000
Large file threshold100 MB
Streaming chunk size1 MB
Max parallel chunks8
Trash retention30 days
Argon2id memory cost64 MiB
Argon2id time cost3 iterations
Nextcloud Login Flow v2 TTL600 s

Released under the MIT License.