Update instance settings.
const url = 'http://localhost:8080/v1/admin/settings';const options = { method: 'PATCH', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"registration_mode":"closed","storage_provider":"auto","library_discovery_enabled":true,"library_discovery_interval_minutes":1,"library_discovery_appdetails_enabled":true,"mic_capture_enabled":true,"image_update_policy":"manual","allowed_origins":["example"]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}use std::str::FromStr;use serde_json::json;use reqwest;
#[tokio::main]pub async fn main() { let url = "http://localhost:8080/v1/admin/settings";
let payload = json!({ "registration_mode": "closed", "storage_provider": "auto", "library_discovery_enabled": true, "library_discovery_interval_minutes": 1, "library_discovery_appdetails_enabled": true, "mic_capture_enabled": true, "image_update_policy": "manual", "allowed_origins": ("example") });
let mut headers = reqwest::header::HeaderMap::new(); headers.insert("Authorization", "Bearer <token>".parse().unwrap()); headers.insert("Content-Type", "application/json".parse().unwrap());
let client = reqwest::Client::new(); let response = client.request(reqwest::Method::from_str("PATCH").unwrap(), url) .headers(headers) .json(&payload) .send() .await;
let results = response.unwrap() .json::<serde_json::Value>() .await .unwrap();
dbg!(results);}curl --request PATCH \ --url http://localhost:8080/v1/admin/settings \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "registration_mode": "closed", "storage_provider": "auto", "library_discovery_enabled": true, "library_discovery_interval_minutes": 1, "library_discovery_appdetails_enabled": true, "mic_capture_enabled": true, "image_update_policy": "manual", "allowed_origins": [ "example" ] }'Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”object
Managed-home backing store: auto = local when the session host has an effective home root, volume otherwise. Affects new homes only.
Steam library discovery Phase 4. EVERY FIELD ON THIS BODY IS OPTIONAL AND ABSENCE MEANS UNCHANGED - a plain (non-pointer) decode of an absent library_discovery_enabled would read false and silently switch discovery off every time an admin changed the registration mode. A PATCH naming no known field is a no-op that returns current state.
Admin-libraries amendment (2026-08-01). Absent = unchanged. Out of bounds (below 15 or above 10080 minutes) is 400 validation_failed. Setting it does not lift an environment override - LibraryStatus reports the resolved value.
Admin-libraries amendment (2026-08-01). Absent = unchanged (pointer decode, same rule as library_discovery_enabled above).
Microphone capture amendment (2026-08-02). Absent = unchanged (pointer decode). Default false. Gates the POST /v1/sessions mic request; affects subsequent launches only.
P3 (image management). Absent = unchanged (pointer decode). Applied by POST /v1/admin/images/sync — see the App-image management P3 section.
First-run wizard v2 §S6e (migration 0064). The signaling origin allow-list. ABSENT = UNCHANGED; an explicitly-sent [] CLEARS the list. Those are different requests and the server distinguishes them (pointer decode), so a PATCH that only changes the registration mode can never wipe the allow-list. Each entry must be scheme + host only (http/https, no path, query, credentials or trailing slash); the server stores the NORMALIZED form, so what is saved is exactly what /v1/signal compares against. “*” IS REJECTED OUTRIGHT with 400 validation_failed - a wildcard would discard the layer entirely. A bad entry is 400 naming its position, and nothing is written. Setting this does NOT lift an environment override: when QUASAR_ALLOWED_ORIGINS is SET it wins, and GET /v1/admin/access-check reports which source is in force.
Responses
Section titled “Responses”OK.
object
object
Managed-home backing store: auto = local when the session host has an effective home root, volume otherwise. Affects new homes only.
P3 (image management). Instance-wide update policy applied by POST /v1/admin/images/sync. Column exists since migration 0054 (DDL default notify) but only reaches the wire from P3. Optional in the envelope so pre-P3 servers stay conformant.
Microphone capture amendment (2026-08-02). Instance-wide gate for the POST /v1/sessions mic request. Default false (ship-dark). Server-enforced at launch; flipping it affects subsequent launches only.
Admin-libraries amendment (2026-08-01). The operator-set scan interval, minutes, default 360. THE DATABASE COLUMN IS THE OPERATOR PATH AND THE ENV VAR IS AN OVERRIDE, NOT A DEFAULT: when QUASAR_LIBRARY_SCAN_INTERVAL is set it wins - and its documented 0 = hard-kill-regardless-of-the-database-flag semantics are unchanged. LibraryStatus.scan_interval_secs remains the RESOLVED value (database-then-env-override), and LibraryStatus.interval_overridden_by_env says which source won so a UI can grey a control the environment has pinned.
Admin-libraries amendment (2026-08-01). The operator-set third-party appdetails lookup switch, default false - the same privacy decision QUASAR_STEAM_APPDETAILS_LOOKUP carried, now settable from the admin UI. Same override rule as the interval: a SET env var wins (a privacy-hardened deployment can pin it off in the environment), and LibraryStatus.appdetails_overridden_by_env reports when that happened.
Steam library discovery Phase 4 (migration 0045). THE MASTER SWITCH, AND THE ONLY SWITCH. Default false - ship-dark, the posture artwork already holds - and with it false the scheduler returns before its first query and the agent pull answers an empty list: no scan rows, no agent work, no third-party calls. Under operator decision 1 AUTO-PUBLISH IS THE BEHAVIOUR, NOT A MODE (there is no review queue), so there is deliberately no second “publish” toggle: its false branch would select a path that was never built. Read PER PASS and per request rather than cached at boot, so flipping it in the admin UI takes effect without a restart.
First-run wizard v2 §S6e (migration 0064). The admin-editable signaling origin allow-list, normalized (scheme + lowercased host). THIS IS THE DATABASE COLUMN, NOT NECESSARILY WHAT /v1/signal ENFORCES: QUASAR_ALLOWED_ORIGINS, when SET, overrides it outright - including when set to the empty string, which is how a hardened deployment pins the list off. That override rule is what makes the migration a behavioural no-op on upgrade for every existing deployment. GET /v1/admin/access-check reports the RESOLVED list plus which source won, so a UI can grey out a control the environment has pinned - the same shape library_discovery_interval_minutes uses. AN EMPTY LIST IS NOT “DENY ALL”: /v1/signal still admits a same-origin request and a request with no Origin header at all, so a fresh instance with nothing configured works. Optional in the envelope so pre-amendment servers stay conformant.
Example
{ "settings": { "registration_mode": "closed", "storage_provider": "auto", "image_update_policy": "manual" }}Malformed or invalid request.
object
object
E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.
Present on restart_required.
Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.
Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.
object
Examplegenerated
{ "error": { "code": "example", "message": "example", "live_sessions": 1, "session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "derived_tiles": [ { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "name": "example" } ] }}Missing/invalid/expired/revoked token.
object
object
E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.
Present on restart_required.
Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.
Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.
object
Examplegenerated
{ "error": { "code": "example", "message": "example", "live_sessions": 1, "session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "derived_tiles": [ { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "name": "example" } ] }}Authenticated but insufficient role / not the owner (precedes resource lookup).
object
object
E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.
Present on restart_required.
Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.
Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.
object
Examplegenerated
{ "error": { "code": "example", "message": "example", "live_sessions": 1, "session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "derived_tiles": [ { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "name": "example" } ] }}