List all apps incl. disabled + admin fields. (Not yet in control-api.md prose.)
const url = 'http://localhost:8080/v1/admin/apps';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}use reqwest;
#[tokio::main]pub async fn main() { let url = "http://localhost:8080/v1/admin/apps";
let mut headers = reqwest::header::HeaderMap::new(); headers.insert("Authorization", "Bearer <token>".parse().unwrap());
let client = reqwest::Client::new(); let response = client.get(url) .headers(headers) .send() .await;
let results = response.unwrap() .json::<serde_json::Value>() .await .unwrap();
dbg!(results);}curl --request GET \ --url http://localhost:8080/v1/admin/apps \ --header 'Authorization: Bearer <token>'Steam library discovery Phase 2: THIS IS THE UNFILTERED GOD VIEW and is deliberately NOT entitlement-filtered. That is exactly what lets GET /v1/apps stay filtered for admins too - nothing becomes unreachable, it moves to the route that already means “the whole fleet’s catalogue”. If an admin “cannot see an app”, the answer is to grant the entitlement (audited), never to add a role arm to the library filter.
Authorizations
Section titled “Authorizations”Responses
Section titled “Responses”OK.
object
object
UI-P7: the TILE crop - the 2:3 PORTRAIT library-tile artwork (16:10 before #385; the move to portrait box art is an operator-directed deviation from the signed-off mockup, and the hero crop stays wide). Null when the app has no artwork, which is the shipped default and renders the gradient tile. Written EXCLUSIVELY by the artwork service (/v1/admin/apps/{id}/artwork) for any app that has an artwork record; a value set directly via AppWrite is honoured only while the app has no artwork record.
UI-P7, ADDITIVE: the HERO crop - a much wider banner asset for the detail/hero panels. A DIFFERENT source asset from cover_url, not the same image scaled: a ~2.1:1 tile stretched into a ~3:1 hero reads as a blown-up thumbnail. Null independently of cover_url (a title may have one crop and not the other); a client falls back hero_url -> cover_url -> gradient. Read-only: not accepted on AppWrite, written only by the artwork service.
UI-P1: presentation-only library classification (apps.kind). Always serialized; ‘game’ for every pre-UI-P1 row. Phase 3 added ‘launcher’ - see AppKind.
Steam library discovery Phase 3, ADDITIVE (apps.parent_app_id, migration 0044): the app this tile is DERIVED from, or null for a normal app - which is every app predating 0044. Always serialized. A derived tile carries identity and presentation only and borrows everything executable (image, runtime_spec, managed home, resource defaults, mounts) from its parent at launch; see control-api.md §Derived tiles. ON THE PUBLIC READ SHAPE DELIBERATELY, unlike origin and library_provider, and for one concrete reason: the single-writer lock is held by the PARENT’s home, so with any tile in a family live, every other tile in that family answers 409 home_in_use. The client needs parent_app_id to mark the siblings of a live session as blocked instead of letting the user discover it by clicking. That is a presentation nicety - THE ENFORCEMENT IS THE SERVER’S 409, never this field.
Steam library discovery Phase 1, ADDITIVE (apps.external_source, migration 0042): “this app IS provider X’s title Y”, read together with external_id. Always serialized and never omitted - “” is the meaningful default (“not a provider title”, the state of every pre-0042 app), so a client must be able to tell it from absent. Defined here on AppListItem, so App and AdminApp inherit it exactly as they inherit kind. Phase 1’s only reader is the artwork service (§Cover artwork): a tagged app resolves its art by id and never enters the fuzzy title matcher. Nothing in scheduling, admission, profile/codec resolution, or the agent wire reads it.
Steam library discovery Phase 1, ADDITIVE (apps.external_id, migration 0042): the provider-native id, today a Steam appid. Always serialized; “” when the app is not a provider title. A bare positive integer, no leading zero, no sign, no whitespace, no separators - the grammar is ARGUMENT-INJECTION CONTAINMENT, not tidiness; see AppWrite.external_id and control-api.md.
UI-P1: whether THE CALLING USER has favourited this app. Always serialized. Resolved per request from the bearer identity - never a stored property of the app, never settable via AppWrite, never assertable by a client. Set/cleared via PUT/DELETE /v1/me/favourites/{app_id}.
UI-P4: the LAUNCH PROFILE this app pins or prefers, per profile_policy.
How an app resolves its launch profile. UI-P4 REMOVED the value ‘custom’ (breaking): under the two-object model every app points at a launch profile, and ‘custom’ was also the one mode that could not express a codec. profile_policy:“custom” is now 400 validation_failed. inherit = the user/global default decides; prefer = the app’s default_profile_id, the user may still override; force = the app’s profile always.
The stream advertised in the library. UI-P4: resolved through the launch profile’s TOP rung (position 1), falling back to the app’s default_* columns when no launch profile resolves. Advertised, not resolved - a launch may fall through to a lower rung and stream at a different resolution; the session’s own stream block is the truth.
object
Multi-codec: the resolved session video codec on session.stream; absent on an app’s display_stream. Additive; h264 for every pre-multi-codec session. h264_profile applies only when codec is h264.
AS-02: tier-selected initial receiver playout target (ms) on session.stream; absent on an app’s display_stream.
Microphone capture (2026-08-02): the GRANTED state on session.stream (request mic AND instance mic_capture_enabled); absent on an app’s display_stream and on pre-amendment sessions (absent = false).
Session-display-stream (approved 2026-08-16) (2026-08-16, approved — PR #15). Current EXTERNAL (encoded/streamed) width on session.stream; absent on an app’s display_stream. PRESENT WHENEVER THE CONTROL PLANE KNOWS the current external size — i.e. it has seen a 202 from PATCH /v1/sessions/{id}/display or a session_metrics sample reporting it — INCLUDING when that size equals the launch width/height. ABSENT MEANS UNKNOWN (a fresh control plane, or no such signal yet for this session), NOT “at launch size” — do not infer launch size from absence. Ephemeral, in-memory control-plane cache of the last-known value (agent session_metrics is the authoritative source), lost on a control-plane restart until the next 202 or session_metrics sample repopulates it. See control-api.md for the INTERNAL-vs-EXTERNAL vocabulary.
Session-display-stream (approved 2026-08-16). Pairs with external_width; see its description.
Session-display-stream (approved 2026-08-16). Whether the assigned host’s encoder can live-resize the stream at all (readback of agent-api.md session_metrics.external_resize_supported). Absent until an amendment-aware agent reports = unknown, never false.
Abr-resolution-fps-ladder (approved 2026-08-16) amendment (2026-08-16, approved — PR #15). Who currently owns the live external size on session.stream: “auto” (the host’s ABR resolution ladder) or “pinned” (a PATCH /v1/sessions/{id}/display stream_width/ stream_height set it to a non-launch size, which suspends the ladder for the rest of the session or until released). Readback of agent-api.md session_metrics.external_owner. PRESENT ONLY WHEN KNOWN AND external_width/ external_height differ from the launch width/height — mirrors external_width’s presence rule, since the agent reports external_owner only in that same window. Absent on every pre-amendment session and while the external size sits at launch.
Session-display-stream (approved 2026-08-16). The fixed, aspect-ratio-filtered table of [width,height] pairs this session’s stream_width/stream_height may be set to via PATCH /v1/sessions/{id}/display (always <= the launch size, launch size always included). Always present on session.stream for a running session; absent on an app’s display_stream. Not the admin-configured stream-profile “rungs” (AS10-01) — a separate, fixed table unrelated to the admin encode-rung catalog. 21:9 family membership is by a set of reduced ratios {43:18, 64:27, 7:3} — 3440x1440 reduces to 43:18, 2560x1080 to 64:27 — control-api.md’s table is the reference, not a computed tolerance.
Sessions created for this app in the last 30 days, counted at read time (sessions.created_at >= now() - interval '30 days'), EVERY state included - a launch that failed is still a launch someone attempted, and an admin ranking a catalogue by use wants attempts, not successes. Admin-only: it is fleet-wide across all users, so it never appears on App/AppListItem.
Steam library discovery Phase 3, ADDITIVE. Always serialized; ‘manual’ for every app predating migration 0044. READ-ONLY: it is deliberately NOT on AppWrite. An admin create is ‘manual’ by construction and only the Phase 4 reconciler writes ‘discovered’, so accepting it on write would let an operator mislabel a hand-made tile as discovered - and the reconciler keys its create/suppress behaviour off provenance, so a lie here is a lie to a background job rather than a cosmetic one. BOTH relabel directions are footguns under apps_parent_external_uk, which is what settles it: a tile relabelled ‘manual’ still occupies its (parent, source, appid) slot, so a reconciler reading it as “not mine, therefore missing” cannot re-create it either - a resurrection loop with no visible cause - and hand-creating a tile pre-labelled ‘discovered’ so a sweep adopts it has the same root cause. The field being unwritable removes both. ENFORCEMENT IS DisallowUnknownFields, NOT A FIELD-SPECIFIC BRANCH. Because this property is absent from AppWrite, sending {“origin”: …} to POST /v1/apps or PATCH /v1/apps/{id} is 400 validation_failed with the generic malformed-body message, not a message naming origin. That is the same mechanism that rejects any unknown key on this shape; do not expect a bespoke error string. Admin-only, like runtime_preset_id: it is operator/provenance detail, not library presentation, so it is not on AppListItem.
Steam library discovery Phase 3, ADDITIVE. Always serialized; “” for every app predating migration 0044. THIS - AND ONLY THIS - IS THE TRIGGER FOR LIBRARY DISCOVERY (Phase 4). It is operator-set and NEVER inferred from the image name, because image names change and a wrong inference here starts a filesystem scan of somebody’s home. It is also NOT kind: an admin editor may reasonably suggest kind=‘launcher’ when this is set to steam, but no server path may branch on kind (control-api.md §Derived tiles). A derived tile must carry “” - a tile cannot itself be a provider, and the DB CHECK enforces it. Admin-only, like runtime_preset_id: operator configuration, not library presentation.
#534 AMENDMENT (2026-08-25), ADDITIVE. Always serialized; true when the library-discovery reconciler disabled this provider app because library_discovery_enabled is false (apps.library_discovery_suspended, migration 0060 - the column predates this amendment; it was previously invisible on every read shape, which made a suspended provider app indistinguishable from a deleted one). Admin-only, like origin: a suspended app is correctly ABSENT from the user library and 404 at launch (POST /v1/sessions is the authorization boundary and must not distinguish absent from withheld); the admin surface is where the state must be visible so the operator can see WHY the app is dark and that library_discovery_enabled=true reverses it. READ-ONLY: written only by the reconciler, not on AppWrite.
DEPRECATED (#383). Still accepted on write and returned on read, but no longer read by the scheduler - admission gates on default_encode_slots plus a live free-VRAM veto. It was never enforceable (no VRAM cap is applied to a session).
object
P5-01: whether this app provisions a per-user managed home directory.
P5-01: container path where the home is mounted (default /home/quasar).
UI-P3: the runtime preset this app inherits its container configuration from, or null when the app carries everything itself (the pre-UI-P3 behaviour). Always serialized. This is the app’s OWN stored column - the preset is NEVER flattened into runtime_spec here; the merge happens server-side at launch, so this read shape shows the operator exactly what they configured. Admin-only, like runtime_spec: a preset is container configuration, not library presentation, so it is deliberately NOT on AppListItem.
UI-P5: which LAUNCH PROFILES a user may pick for this app from the menu beside Play. ALWAYS serialized as an array, never null. EMPTY = unrestricted (any launch profile the device is eligible for), which is every pre-UI-P5 app, so the feature ships inert. NON-EMPTY = intersect with eligibility; it can only ever narrow what eligibility already permits, never widen it. The app’s OWN default (default_profile_id under profile_policy ‘prefer’) is IMPLICITLY always included and cannot be removed, and is deliberately absent from this array - it is one field away and duplicating it would create a second copy to keep in sync. Only meaningful for profile_policy ‘inherit’ and ‘prefer’. ‘force’ pins the app’s profile outright, so this is always [] for a forced app and setting it is 400 validation_failed - mirroring the admin UI hiding the control, rather than storing a rule that does nothing today and silently takes effect on a later policy change. Admin-only, for the same reason as runtime_preset_id: it is operator configuration, and a client is served the already-filtered menu by GET /v1/me/profiles?app_id= rather than being asked to intersect anything itself.
Example
{ "items": [ { "kind": "game", "external_source": "", "profile_policy": "inherit", "display_stream": { "h264_profile": "constrained-baseline", "codec": "h264", "external_owner": "auto" }, "origin": "manual", "library_provider": "" } ]}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" } ] }}