Skip to content

Ignore or un-ignore one appid (one route, two directions).

PUT
/v1/admin/apps/{id}/library/rules/{external_id}
curl --request PUT \
--url http://localhost:8080/v1/admin/apps/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/library/rules/example \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "rule": "ignore", "note": "example", "external_source": "steam" }'

Phase 4. rule:“ignore” is the Ignore action and it does THREE things in one transaction, NONE OF THEM A DELETE - writes the rule row, sets enabled=false on the tile if one exists, and revokes that tile’s granted_by=‘provider’ entitlements FLEET-WIDE (an Ignore is a fleet decision; a half-revoke would leave the tile live for everyone who happens not to be scanned next). It is not a delete because apps cascades to user_app_favourites and app_artwork, so deleting a junk tile destroys every user’s favourite of it and its artwork row irreversibly - and the next scan re-creates a bare row anyway, because the appid is still on disk. Durability rests on two INDEPENDENT facts, deliberately, because either alone is a resurrection bug: the rule row makes the reconciler skip the appid, AND the reconciler never modifies an existing tile, so enabled=false is never flipped back. rule:“allow” outranks the built-in denylist and writes ONLY the rule row - it does NOT re-enable an existing disabled tile; the next scan republishes the appid through the ladder. That asymmetry is intentional: a tile may have been disabled by hand for a reason unrelated to discovery, and an un-ignore must not override the admin who did it. The primary key (parent_app_id, external_source, external_id) is the idempotency key, so a repeat is a REPLACE and never an accumulation. Audited as app.library.rule.set. The 400 on this route now covers TWO causes: a malformed appid, and a real {id} that is not a library provider - the latter checked INSIDE the write transaction, so a concurrent un-marking of the provider cannot slip a rule in behind it.

id
required
string format: uuid
external_id
required
string
/^[1-9][0-9]{0,9}$/

Steam library discovery Phase 4: the provider title id an appid rule applies to - today a Steam appid. VALIDATED AT THE HANDLER AS WELL AS BY THE DATABASE CHECK, deliberately: this table is ADMIN-WRITABLE and takes the value straight from a URL, and the value ends up in STEAM_STARTUP_FLAGS, which the quasar-steam entrypoint word-splits with read -r -a. The handler is what makes a bad value a 400 instead of a 500; the CHECK is the durable guard and the only one of the two that survives an admin editing the row directly later. Bounded below 2^32 in addition to the pattern.

Media typeapplication/json
object
rule
required

‘ignore’ suppresses an appid the built-in denylist does not know about; ‘allow’ un-suppresses a game it wrongly caught, and OUTRANKS the built-in list - rung 1 of the ladder sits above rung 3.

string
Allowed values: ignore allow
note

Optional. Truncated at 512 characters.

string
external_source

Optional, defaults to ‘steam’; any other value is 400 validation_failed. The vocabulary is provider-shaped so Heroic or RomM slot in later with no re-model, but only ‘steam’ exists today.

string
Allowed values: steam

Rule written.

Media typeapplication/json

NOTE THE SHAPE ASYMMETRY, which is real and is recorded rather than smoothed over: rule on the REQUEST is the two-value string, and rule here is the whole stored rule object (whose own rule field is that string).

object
rule
required

One operator-written layer-2 rule. Layer 1 is a shipped code constant (an appid set plus case-insensitive Valve tool name prefixes) and UPDATING IT IS A CODE CHANGE AND A RELEASE - which is exactly why this layer exists, because an operator cannot wait for one.

object
external_source
required
string
Allowed values: steam
external_id
required
string
/^[1-9][0-9]{0,9}$/
rule
required

‘ignore’ suppresses an appid the built-in denylist does not know about; ‘allow’ un-suppresses a game it wrongly caught, and OUTRANKS the built-in list - rung 1 of the ladder sits above rung 3.

string
Allowed values: ignore allow
note
required

Why, for the next operator. Truncated at 512 characters.

string
created_by
required

The acting admin, resolved server-side from the bearer identity. NULL once that account is deleted (ON DELETE SET NULL, not CASCADE) - deleting the operator who wrote a rule must not silently un-suppress every appid they ignored.

string | null format: uuid
created_at
required

Set, and re-set on a replace.

string format: date-time
disabled
required

Whether this call disabled a tile. Always false for an ‘allow’, which by design changes nothing but the rule row - it does NOT re-enable an existing disabled tile, because that tile may have been disabled by hand for a reason unrelated to discovery; the next scan republishes the appid through the ladder instead.

boolean
revoked
required

Provider entitlements removed by this call - FLEET-WIDE, because an Ignore is a fleet decision and a half-revoke would leave the tile live for everyone who happens not to be scanned next. granted_by=‘admin’ rows are never touched.

integer
Example
{
"rule": {
"external_source": "steam",
"rule": "ignore"
}
}

Malformed or invalid request.

Media typeapplication/json
object
error
required
object
code
required

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.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

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.

string format: uuid
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.

Array<object>
object
id
required
string format: uuid
name
required
string
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.

Media typeapplication/json
object
error
required
object
code
required

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.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

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.

string format: uuid
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.

Array<object>
object
id
required
string format: uuid
name
required
string
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).

Media typeapplication/json
object
error
required
object
code
required

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.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

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.

string format: uuid
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.

Array<object>
object
id
required
string format: uuid
name
required
string
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"
}
]
}
}

No such resource.

Media typeapplication/json
object
error
required
object
code
required

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.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

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.

string format: uuid
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.

Array<object>
object
id
required
string format: uuid
name
required
string
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"
}
]
}
}