Update the per-install appearance configuration
PATCH /admin/system/appearance
Persists the brand / display / body / mono font slot IDs.
Empty string in any field means “use the slot default”.
Requires the system.appearance.write capability (auto-granted
to admins).
Authorizations
Section titled “Authorizations ”Request Body required
Section titled “Request Body required ”Per-install brand and typography choices. Each field holds a
font catalogue ID (e.g. limelight, inter-variable,
system-sans); the frontend resolves the ID to an
@font-face declaration at boot. Empty string means the slot
uses the frontend’s default.
object
Logo / hero font slot. Recommended for display faces (Limelight, Bebas Neue, Playfair Display); body text never uses this slot.
H1–H3 heading slot. Falls back to the body font when empty.
Body / UI text slot. Falls back to the system sans-serif stack when empty.
Code / tabular numerics slot. Falls back to the system monospace stack when empty.
Display name of this install. Read-only here — it is owned
and edited via the Site config (/admin/system/site); it is
surfaced on this public boot path so the wordmark, login
card, and document titles can render the configured name
before the user has signed in. Defaults to “Artist Alley”
when the operator hasn’t set one.
Example
Artist AlleyTrue when this install runs in demo mode (env
AA_DEMO_MODE=1). Surfaced on the public boot path so the
login card can advertise the shared demo credentials and a
read-only banner can render once signed in. Absent/false in
a normal install — zero footprint when off.
True when this install can answer a reverse-image search —
i.e. search.visual.enabled is on AND the CLIP visual
encoder sidecar was reachable when the process booted.
It is the RESOLVED capability, not the config flag, and the
difference is the whole point (#1163): an install whose
sidecar was unreachable at boot has enabled: true in
sysconfig and still serves 501 sidecar_not_installed on
POST /search/by-image, so publishing the config value
would advertise an arm that cannot work. Read it as “the
by-image endpoint will do something”.
Surfaced here, on the boot path the frontend already reads, so the reverse-image dropzone can be absent on an install without the channel instead of being discovered by dropping an image and getting an error. Absent/false on a default install — the feature is off unless an operator turns it on.
True when this install lets anonymous visitors browse
(public_mode, #445/#709). Read-only here — the switch is
owned by GET/PUT /system/public-mode, which are gated on
system.config.read/write; this is the same fact
published on the boot path every client already fetches, so
an ordinary member can ask “does ‘public’ mean anything on
this instance?” without an admin capability.
It exists for the tier pickers (#1195). A public
visibility option on a collection or a post promises
something an install with public mode OFF cannot deliver —
there are no anonymous readers to see it — so the option is
offered only where it is meaningful, the same
resolved-capability discipline visual_search_enabled
follows (#1163).
It discloses nothing: with public mode on, an anonymous visitor can simply browse; with it off, every anonymous request is already refused by the public-mode middleware. Either way the answer is one unauthenticated request away.
URL of the operator-uploaded instance logo, or absent when none is set — absent means “render the shipped default mark”, exactly as an empty font slot means “use the slot default”.
Read-only here. The logo is written only by
POST /admin/system/appearance/logo, which validates the
image bytes, and cleared only by the matching DELETE.
It is deliberately NOT settable through this object: if a
PATCH could name the underlying storage object, an admin
could point the public, unauthenticated logo route at any
hash on the install — including someone’s private asset.
Carries a v content-hash parameter, so the URL changes
whenever the logo does and the response can be cached
immutably.
Example
/api/v1/appearance/logo?v=9f2b1c4d5e6aIntrinsic width in pixels of the uploaded logo. Published so a client can reserve layout space and avoid a reflow when the image lands. Absent when no logo is set.
Example
512Intrinsic height in pixels of the uploaded logo. Absent when no logo is set.
Example
512The operator’s recent logos, most-recently-used first, capped at 5, so a previous mark can be picked back up without re-uploading it.
Admin surface only. Populated on
GET /admin/system/appearance and on the logo write
responses; deliberately absent from the public /appearance
boot payload, which needs only the active logo and must not
pay for the per-entry availability check on every anonymous
page load.
The shipped default is NOT an entry here — it is always
available and is represented by logo_url being absent.
One entry in the operator’s recent-logo list. A reference to stored bytes plus what the server proved about them when they were uploaded — never the image data itself.
object
Content hash of the image. Pass this to
POST /admin/system/appearance/logo/select to re-activate
the entry, or as v on /appearance/logo to fetch it.
Example
9f2b1c4d5e6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2cReady-made URL for rendering this entry’s thumbnail.
Example
/api/v1/appearance/logo?v=9f2b1c4d5e6aMIME type derived by decoding the image at upload time.
Example
image/pngExample
512Example
512Example
18244Whether the bytes are still readable from the storage backend right now.
Listed entries are pinned, so this is normally true. It
can be false when the blob was lost outside the pin
system — a restored database pointed at a fresh bucket,
manual deletion, backend corruption. Clients MUST render
an unavailable entry as explicitly unavailable rather than
as a broken image, and MUST NOT offer it for selection:
“the file is gone” is exactly the situation this list
exists to help an operator through, so it has to be legible
rather than silently broken.
Example
trueWhether this entry is the currently applied logo.
Responses
Section titled “ Responses ”Updated appearance config
Per-install brand and typography choices. Each field holds a
font catalogue ID (e.g. limelight, inter-variable,
system-sans); the frontend resolves the ID to an
@font-face declaration at boot. Empty string means the slot
uses the frontend’s default.
object
Logo / hero font slot. Recommended for display faces (Limelight, Bebas Neue, Playfair Display); body text never uses this slot.
H1–H3 heading slot. Falls back to the body font when empty.
Body / UI text slot. Falls back to the system sans-serif stack when empty.
Code / tabular numerics slot. Falls back to the system monospace stack when empty.
Display name of this install. Read-only here — it is owned
and edited via the Site config (/admin/system/site); it is
surfaced on this public boot path so the wordmark, login
card, and document titles can render the configured name
before the user has signed in. Defaults to “Artist Alley”
when the operator hasn’t set one.
Example
Artist AlleyTrue when this install runs in demo mode (env
AA_DEMO_MODE=1). Surfaced on the public boot path so the
login card can advertise the shared demo credentials and a
read-only banner can render once signed in. Absent/false in
a normal install — zero footprint when off.
True when this install can answer a reverse-image search —
i.e. search.visual.enabled is on AND the CLIP visual
encoder sidecar was reachable when the process booted.
It is the RESOLVED capability, not the config flag, and the
difference is the whole point (#1163): an install whose
sidecar was unreachable at boot has enabled: true in
sysconfig and still serves 501 sidecar_not_installed on
POST /search/by-image, so publishing the config value
would advertise an arm that cannot work. Read it as “the
by-image endpoint will do something”.
Surfaced here, on the boot path the frontend already reads, so the reverse-image dropzone can be absent on an install without the channel instead of being discovered by dropping an image and getting an error. Absent/false on a default install — the feature is off unless an operator turns it on.
True when this install lets anonymous visitors browse
(public_mode, #445/#709). Read-only here — the switch is
owned by GET/PUT /system/public-mode, which are gated on
system.config.read/write; this is the same fact
published on the boot path every client already fetches, so
an ordinary member can ask “does ‘public’ mean anything on
this instance?” without an admin capability.
It exists for the tier pickers (#1195). A public
visibility option on a collection or a post promises
something an install with public mode OFF cannot deliver —
there are no anonymous readers to see it — so the option is
offered only where it is meaningful, the same
resolved-capability discipline visual_search_enabled
follows (#1163).
It discloses nothing: with public mode on, an anonymous visitor can simply browse; with it off, every anonymous request is already refused by the public-mode middleware. Either way the answer is one unauthenticated request away.
URL of the operator-uploaded instance logo, or absent when none is set — absent means “render the shipped default mark”, exactly as an empty font slot means “use the slot default”.
Read-only here. The logo is written only by
POST /admin/system/appearance/logo, which validates the
image bytes, and cleared only by the matching DELETE.
It is deliberately NOT settable through this object: if a
PATCH could name the underlying storage object, an admin
could point the public, unauthenticated logo route at any
hash on the install — including someone’s private asset.
Carries a v content-hash parameter, so the URL changes
whenever the logo does and the response can be cached
immutably.
Example
/api/v1/appearance/logo?v=9f2b1c4d5e6aIntrinsic width in pixels of the uploaded logo. Published so a client can reserve layout space and avoid a reflow when the image lands. Absent when no logo is set.
Example
512Intrinsic height in pixels of the uploaded logo. Absent when no logo is set.
Example
512The operator’s recent logos, most-recently-used first, capped at 5, so a previous mark can be picked back up without re-uploading it.
Admin surface only. Populated on
GET /admin/system/appearance and on the logo write
responses; deliberately absent from the public /appearance
boot payload, which needs only the active logo and must not
pay for the per-entry availability check on every anonymous
page load.
The shipped default is NOT an entry here — it is always
available and is represented by logo_url being absent.
One entry in the operator’s recent-logo list. A reference to stored bytes plus what the server proved about them when they were uploaded — never the image data itself.
object
Content hash of the image. Pass this to
POST /admin/system/appearance/logo/select to re-activate
the entry, or as v on /appearance/logo to fetch it.
Example
9f2b1c4d5e6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2cReady-made URL for rendering this entry’s thumbnail.
Example
/api/v1/appearance/logo?v=9f2b1c4d5e6aMIME type derived by decoding the image at upload time.
Example
image/pngExample
512Example
512Example
18244Whether the bytes are still readable from the storage backend right now.
Listed entries are pinned, so this is normally true. It
can be false when the blob was lost outside the pin
system — a restored database pointed at a fresh bucket,
manual deletion, backend corruption. Clients MUST render
an unavailable entry as explicitly unavailable rather than
as a broken image, and MUST NOT offer it for selection:
“the file is gone” is exactly the situation this list
exists to help an operator through, so it has to be legible
rather than silently broken.
Example
trueWhether this entry is the currently applied logo.
Malformed request
object
Human-readable error summary
Example
the request could not be completedAuthentication required, missing, or invalid
object
Human-readable error summary
Example
the request could not be completedExample
{ "error": "authentication required: sign in and retry with a valid session or API token"}Authenticated but missing required capabilities
object
Human-readable error summary
Example
the request could not be completed