Skip to content

Configuration reference

Every option in config.yaml, generated from the schema in src/server/config.ts, so it can’t drift. Unknown keys are errors, and Valet names the closest match. ${NAME} anywhere is replaced from the environment: see secrets.

The top level of config.yaml.

OptionTypeDefaultWhat it does
titlestringHomeThe page’s name, and the browser tab’s.
greetingstringoptionalwhose homelab it is, e.g. “Sam”
sandboxbooleanfalsefor trying Valet out: nothing is saved, each visitor gets a private copy in memory
noticestringoptionala quiet pill above the greeting, e.g. “Demo · simulated home”
locationstringoptionalTold to the assistant, for questions about here. Weather has its own coordinates.
timezonestringoptionaldefault: TZ, or else the first browser’s
weatherstringoptionala widget key (e.g. weather.now) shown as a ring beside the clock
calendarstringoptionala calendar widget (e.g. family.agenda) whose events appear when you hover the clock
searchduckduckgo, google, kagi, brave, startpage, searxng or URLoptionalweb search from ⌘K: a provider name, or a URL with {q} for the words
linksmap of list of object{}Bookmarks by group. Each group is a widget: links.<group>.
recapbooleantruethe year in review
welcomebooleanfalsethe getting-started card (a fresh config turns it on)
reloadbooleantruewatch config.yaml and apply edits live (a bad edit keeps the last good one)

Look and feel. Every option becomes a CSS variable or a class, so a change shows without a rebuild. Left out, it sets nothing and the stylesheet’s own value stands.

OptionTypeDefaultWhat it does
appearanceobject{}Look and feel.
appearance.themeauto, light, darkautoauto: the system’s, or dark glass over a wallpaper; the others also drop the switch
appearance.colorsobjectoptionalThe page’s colours.
appearance.colors.accentstringoptionalThe one accent colour. Text on it turns black or white to suit, if it’s a hex value.
appearance.colors.goodstringoptionalThe colour for good news.
appearance.colors.warnstringoptionalThe colour for warnings.
appearance.colors.badstringoptionalThe colour for trouble.
appearance.colors.cardstringoptionalThe tint of cards.
appearance.fontobjectoptionalThe page’s type.
appearance.font.familystringoptionalthe UI’s font, which must be installed or come from url
appearance.font.urlstringoptionala stylesheet (Google Fonts, Bunny Fonts), a font file, or a local font file served by Valet
appearance.font.monostringoptionalThe font for code and logs, which must be installed.
appearance.font.scalenumberoptionalevery size of text; 1 is as designed Range 0.7 to 1.5.
appearance.font.headingWeightwhole numberoptionalThe weight of headings, 100 to 900. Range 100 to 900.
appearance.shapeobjectoptionalCorners, borders and shadows.
appearance.shape.radiusnumberoptionalpx; everything rounded follows, and 0 squares off the pills too Range 0 to 40.
appearance.shape.bordernumberoptionalpx around cards Range 0 to 8.
appearance.shape.borderColorstringoptionalThe colour of card borders.
appearance.shape.shadownone, soft, deepoptionalHow much shadow cards cast.
appearance.glassobjectoptionalthe frost: how much of the card’s tint shows (0..1), and how hard the page behind blurs in px. 0 turns blur off everywhere.
appearance.glass.opacitynumberoptionalthe frost: how much of the card’s tint shows (0..1), and how hard the page behind blurs in px. 0 turns blur off everywhere. Range 0 to 1.
appearance.glass.blurnumberoptionalthe frost: how much of the card’s tint shows (0..1), and how hard the page behind blurs in px. 0 turns blur off everywhere. Range 0 to 80.
appearance.wallpaperstring or list of stringoptionallocal file path (served by Valet), image URL, colour or gradient; any of them turns cards to glass
appearance.wallpapersmap of string or list of string{}a different one per tab, by the tab’s name
appearance.rotatenumber or stringoptionalhow long each wallpaper in a list stays
appearance.dimnumber0.35How much to darken the wallpaper, 0 to 0.9. Range 0 to 0.9.
appearance.blurnumber0the frost: how much of the card’s tint shows (0..1), and how hard the page behind blurs in px. 0 turns blur off everywhere. Range 0 to 40.
appearance.densitycompact, comfortable, spaciouscomfortablescales all spacing at once
appearance.gapnumberoptionalpx between cards Range 0 to 64.
appearance.sectionGapnumberoptionalpx between sections Range 0 to 120.
appearance.maxWidthnumber or stringoptionalof the page: 1320px, or 1720px with a sidebar
appearance.rowHeightnumberoptionalpx, the least a row in a section with sizes takes; rows otherwise fit their tallest card Range 60 to 600.
appearance.sizesmap of object{}what the sizes mean, in a grid of 2, 4 or 6 columns as the screen widens: { tall: { rows: 3 }, large: { cols: 6 } }
appearance.sizes.<name>.colswhole numberoptionalHow many columns this size spans. Range 1 to 6.
appearance.sizes.<name>.rowswhole numberoptionalHow many rows this size spans. Range 1 to 6.
appearance.rimnumberoptionala lit bevel on the edge of dark glass, 0 to 1; left out, or 0, there’s none Range 0 to 1.
appearance.motionfull, reduced, nonefullreduced: nothing moves or settles in; none: not even fades. A system’s reduce-motion always counts.
appearance.cssstringoptionalyour own CSS, loaded after everything else
appearance.cssFilestringoptionalthe same from a file; css comes after it

The top of the overview.

OptionTypeDefaultWhat it does
heroobject{}The top of the overview.
hero.greetingboolean or stringtruetext replaces “Sam’s homelab”; {name} is greeting, {greeting} is “Good evening”
hero.statusbooleantruethe sentence under it
hero.searchbooleantruethe ask box (the search box without an assistant)
hero.reviewboolean or endtruethe year in review link, or end: a card closing the overview; recap turns off the review itself
hero.aligncenter, left, rightcenterWhere the greeting and ask box sit.
hero.scopeboolean or list of stringoptionalthe ask box names what it can help with (“Ask about downloads, lights, what to watch…”): true works it out from the integrations present; a list says it yourself

The assistant’s window on the page. What it knows and may do is under assistant.

OptionTypeDefaultWhat it does
chatobject{}How the assistant’s window looks.
chat.opencenter, right, left, bottom, pagecenterphones always get the full sheet
chat.widthnumber or stringoptionalThe window’s width: pixels as a number, or 90%, 40rem, 80vw.
chat.heightnumber or stringoptionalThe window’s height, in the same forms.
chat.overviewbooleantruefalse: no ask box or chips on the overview; it’s a tab or button away
chat.placeholderstringoptionalin the ask box; {name} is the assistant’s name
chat.suggestionsai, fixed, offaiai: written for what’s happening, no model calls with fixed (assistant.suggestions as they are) or off
chat.followUpslist of stringoptionaloffered under every answer, in place of what the tool it used suggests
chat.stepsbooleantruethe tool calls it made; approvals and result cards show either way
chat.thinkingcollapsed, open, hiddencollapsedA model’s reasoning: folded away, open, or not shown.
chat.sendenter, mod-enterentermod-enter: ⌘/Ctrl+Enter sends and Enter starts a line
chat.bubblestringoptionalyour messages
chat.beamoff, always, focus, thinking, onceoffa light travelling around the chat box: all the time, while typing, while it answers, or once: a lap on load, then while typing
chat.beamColorcolorful, mono, ocean, sunsetmonoThe colours of the light around the chat box.
chat.beamStrengthnumber0.45how bright: a hint by default Range 0 to 1.
chat.iconstringoptionalan icon name, or an image path or URL, on its buttons and header

The tab buttons.

OptionTypeDefaultWhat it does
navobject{}The tab buttons.
nav.sideleft, right, top, bottomleftwhere the tab buttons sit (wide screens; phones always bottom)
nav.styleicons, labels, bothiconswide screens; phones have room for icons
nav.hidelist of string[]tabs to leave out, by name; also Overview, and the assistant’s name for its button
nav.orderlist of string[]tabs in this order, by name; the rest follow in layout order
nav.sizenumber1Scale, where 1 is as designed. Range 0.6 to 2.

The rings in the top bar. A plain list of widget keys works too.

OptionTypeDefaultWhat it does
menubarlist of string or object[]widget keys whose headline numbers sit as small rings in the top bar; or the object, to say more about the bar
menubar.widgetslist of string[]Widget keys whose headline numbers sit as rings in the top bar.
menubar.positionleft, center, rightrightwide screens; below that it’s centred
menubar.labelsbooleantruethe words on the page, for another language or your own: { overview: Accueil, thinking: Réflexion }
menubar.clockbooleantruetheme: the light/dark switch
menubar.datebooleantruetheme: the light/dark switch
menubar.themebooleantruetheme: the light/dark switch
menubar.sizenumber1Scale, where 1 is as designed. Range 0.6 to 2.

Rings in the top bar for what’s happening.

OptionTypeDefaultWhat it does
liveobjectoptionalLeft out, nothing shows as live.
live.activitieslist of string[]Widget keys to show, most important first.
live.notifylist of object[]short notices when things change, e.g. { widget: downloads.active, on: end, text: “{title} finished” }: start/end of a widget’s live activity, a service going down/up (status cards). {title}, {name} fill in. push: true sends it to your notification channels as well
live.notify[].widgetstringrequiredThe widget to watch.
live.notify[].onstart, end, down, uprequiredstart and end of its live activity, or a service going down or up.
live.notify[].textstringoptionalThe notice. {title} and {name} fill in.
live.notify[].pushbooleanfalseshort notices when things change, e.g. { widget: downloads.active, on: end, text: “{title} finished” }: start/end of a widget’s live activity, a service going down/up (status cards). {title}, {name} fill in.

How things are written. auto follows the browser.

OptionTypeDefaultWhat it does
formatobject{}How things are written.
format.clockauto, 12h, 24hauto12 or 24 hours.
format.dateshort, long, numeric, isoshortSat, Oct 31 · Saturday, October 31 · 10/31/2026 · 2026-10-31
format.weekStartauto, monday, sunday, saturdayautoThe first day of the week in calendars.
format.temperatureauto, c, fautoconverts what services report; auto leaves it alone
format.distanceauto, km, miautoKilometres or miles.
format.localestringautosets numbers (1.234,5) and month names

The words on the page. {name}, {year}, {n} and {scope} fill in.

OptionTypeDefaultWhat it does
labelsobject{}the words on the page, for another language or your own: { overview: Accueil, thinking: Réflexion }
labels.homelabstringoptionalDefault: “{name}‘s homelab”
labels.yourHomelabstringoptionalDefault: “Your homelab”
labels.goodMorningstringoptionalDefault: “Good morning”
labels.goodAfternoonstringoptionalDefault: “Good afternoon”
labels.goodEveningstringoptionalDefault: “Good evening”
labels.goodNightstringoptionalDefault: “Good night”
labels.overviewstringoptionalDefault: “Overview”
labels.searchstringoptionalweb search from ⌘K: a provider name, or a URL with {q} for the words
labels.reviewstringoptionalDefault: “Your {year} in review”
labels.reviewMorestringoptionalDefault: “What your homelab got up to this year, in a few screens”
labels.reviewGostringoptionalDefault: “Play”
labels.askstringoptionalDefault: “Ask {name}”
labels.askAnythingstringoptionalDefault: “Ask {name} anything”
labels.askAboutstringoptionalDefault: “Ask about {scope}”
labels.allOfstringoptionalDefault: “All {n}”
labels.dismissstringoptionalDefault: “Dismiss”
labels.messagestringoptionalDefault: “Message {name}”
labels.messageHintstringoptionalDefault: “Message {name}, paste a link or drop a file”
labels.chatTitlestringoptionalDefault: “Talk to your homelab”
labels.thinkingstringoptionalDefault: “Thinking”
labels.writingstringoptionalDefault: “Writing”
labels.thoughtstringoptionalDefault: “Thought”
labels.thoughtForstringoptionalDefault: “Thought for {n}s”
labels.stepsstringoptionalDefault: “{n} steps”
labels.workedstringoptionalDefault: “Worked for {n}s”
labels.allowstringoptionalDefault: “Allow”
labels.notNowstringoptionalDefault: “Not now”

Where “tell me when” reminders go when you’re not looking.

OptionTypeDefaultWhat it does
notifyobject{"channels":[],"insights":false}short notices when things change, e.g. { widget: downloads.active, on: end, text: “{title} finished” }: start/end of a widget’s live activity, a service going down/up (status cards). {title}, {name} fill in. push: true sends it to your notification channels as well
notify.channelslist of object or object or object or object or object or object or object or object[]where “tell me when” reminders go when you’re not looking (ntfy, gotify, telegram, discord, slack, pushover, apprise, webhook). insights: also push the assistant’s shared-cause line.
notify.channels[ntfy].urlURLrequiredThe full topic URL.
notify.channels[ntfy].tokenstringoptionalAn access token, if the topic needs one.
notify.channels[gotify].urlURLrequiredYour Gotify server.
notify.channels[gotify].tokenstringrequiredAn application token.
notify.channels[telegram].tokenstringrequiredYour bot’s token.
notify.channels[telegram].chatstring or numberrequiredThe chat to send to.
notify.channels[discord].urlURLrequiredA Discord webhook URL.
notify.channels[slack].urlURLrequiredA Slack webhook URL.
notify.channels[pushover].tokenstringrequiredYour application’s token.
notify.channels[pushover].userstringrequiredYour user key.
notify.channels[apprise].urlURLrequiredAn Apprise endpoint, for example http://apprise:8000/notify/mykey.
notify.channels[webhook].urlURLrequiredWhere to send it.
notify.channels[webhook].headersmap of stringoptionalHeaders to send with it.
notify.insightsbooleanfalsewhere “tell me when” reminders go when you’re not looking (ntfy, gotify, telegram, discord, slack, pushover, apprise,

Sign-in, and the names Valet answers to. See sign-in and network safety.

OptionTypeDefaultWhat it does
authobject{"mode":"none","sessionDays":30,"secureCookie":true,"trustNetwork":false,"proxy":{"header":"Remote-User","trusted":["127.0.0.1"]}}Sign-in. Off by default, which leaves Valet read-only.
auth.modenone, local, proxy, oidcnonelike other dashboards, no sign-in by default: keep it on a private network/VPN or behind your proxy’s auth. local: built-in accounts · proxy: trust the user header from an auth proxy (Authelia, Authentik…) · none
auth.sessionDayswhole number30how long “keep me signed in” lasts Range 1 to 365.
auth.secureCookiebooleantruefalse only when serving over plain http
auth.trustNetworkbooleanfalsewith mode: none, Valet is read-only (no buttons, no assistant tools that add or change things) unless you say the network itself is the lock, e.g. it’s only reachable over your VPN/tailnet or behind a sign-in proxy
auth.oidcobjectoptionalmode: oidc. Register Valet at your provider with the redirect URL https://<valet>/api/auth/oidc/callback
auth.oidc.issuerURLrequirede.g. https://auth.example.com (it must serve /.well-known/openid-configuration)
auth.oidc.clientIdstringrequiredThe client id from your provider.
auth.oidc.clientSecretstringrequiredThe client secret from your provider. Use ${VAR}.
auth.oidc.namestringSSOon the sign-in button
auth.oidc.claimstringpreferred_usernamewhich claim is the person’s name here
auth.oidc.scopestringopenid profile emailThe scopes to ask for.
auth.oidc.allowlist of string[]only these names or emails may sign in (empty: anyone the provider lets through)
auth.oidc.redirectUrlURLoptionalif Valet can’t tell its own address (an odd proxy setup)
auth.proxyobject{"header":"Remote-User","trusted":["127.0.0.1"]}like other dashboards, no sign-in by default: keep it on a private network/VPN or behind your proxy’s auth.
auth.proxy.headerstringRemote-Userthe header your proxy puts the signed-in user in
auth.proxy.trustedlist of string["127.0.0.1"]proxy addresses whose header is believed
allowedHostslist of string[]the names Valet answers to besides localhost and IP addresses, e.g. [valet.example.com, “*.home.example.com”]. Anything else is refused, so a web page elsewhere can’t reach it through your browser (DNS rebinding).

Each widget’s headline numbers over time, so tiles can show a graph. Set to false to turn it off.

OptionTypeDefaultWhat it does
historyfalse or object{"hours":72,"every":5,"background":true}remember each widget’s headline numbers so tiles can show a graph even for services that only report “now”. background keeps sampling while nobody has the page open (one quiet request per widget per every minutes).
history.hoursnumber72How many hours of samples to keep. Range 1 to 336.
history.everynumber5minutes between samples Range 1 to 60.
history.backgroundbooleantrueKeep sampling while nobody has the page open: one quiet request per widget per every minutes.

Community integrations. A plain list of npm packages works too. See the plugin toolkit.

OptionTypeDefaultWhat it does
pluginslist of string or object[]community integrations: npm packages to load (files in data/integrations/ load on their own); replace lets a plugin take over a built-in type
plugins.packageslist of string[]community integrations: npm packages to load (files in data/integrations/ load on their own); replace lets a plugin take over a built-in type
plugins.replacelist of string[]community integrations: npm packages to load (files in data/integrations/ load on their own); replace lets a plugin take over a built-in type

Left out, there is no assistant and the page loses nothing. See models.

OptionTypeDefaultWhat it does
assistantobjectoptionalLeft out, there is no assistant.
assistant.provideropenai-compatible, anthropicopenai-compatibleany OpenAI-compatible server (Ollama, llama.cpp, vLLM, OpenRouter, OpenAI…) or Anthropic
assistant.baseURLstringoptionalThe model server. For openai-compatible, it defaults to http://localhost:11434/v1, which inside a container is the container itself.
assistant.apiKeystringoptionalThe model server’s key, if it wants one. Use ${VAR}.
assistant.modelstringrequiredThe model’s name on that server.
assistant.namestringAssistantWhat the assistant is called on the page.
assistant.instructionsstringoptionalextra system prompt
assistant.accessmap of none, read, write, control{"default":"read"}what the assistant may use: a default plus per-integration overrides, e.g. { default: read, lights: control }
assistant.maxStepswhole number6Tool rounds per answer. The last is kept for answering. Range 1 to 20.
assistant.agentobjectoptionalhand the chat to another agent (Hermes, say): anything with an OpenAI-compatible API. It runs its own tools, so Valet’s access levels and approvals don’t apply to it, and a read-only page stays on Valet’s own assistant.
assistant.agent.namestringrequiredThe agent’s name on the page.
assistant.agent.baseURLURLrequiredThe agent’s OpenAI-compatible address.
assistant.agent.apiKeystringoptionalThe agent’s key, if it wants one. Use ${VAR}.
assistant.agent.modelstringrequiredThe model name to send it.
assistant.backgroundobject{"watch":true,"brief":true,"suggest":true,"perDay":100}what it does on its own: a morning brief, and spotting a shared cause when several things break at once. These get no tools, so they can only say something. perDay caps the model calls they make.
assistant.background.watchbooleantruewhat it does on its own: a morning brief, and spotting a shared cause when several things break at once. These get no tools, so they can only say something. perDay caps the model calls they make.
assistant.background.briefbooleantruewhat it does on its own: a morning brief, and spotting a shared cause when several things break at once. These get no tools, so they can only say something. perDay caps the model calls they make.
assistant.background.suggestbooleantruewhat it does on its own: a morning brief, and spotting a shared cause when several things break at once. These get no tools, so they can only say something. perDay caps the model calls they make.
assistant.background.perDaywhole number100what it does on its own: a morning brief, and spotting a shared cause when several things break at once. These get no tools, so they can only say something. perDay caps the model calls they make. Range 1 to 500.
assistant.suggestionslist of string["Is anything down?","How are my downloads doing?","What's playing on Plex?","Show me the GPU and Plex side by side"]questions offered on an empty chat and in ⌘K

MCP servers the assistant may use. See MCP.

OptionTypeDefaultWhat it does
mcpmap of object{}MCP servers, by name.
mcp.<name>.urlURLrequiredThe server’s address.
mcp.<name>.transporthttp, ssehttpHow to reach it.
mcp.<name>.headersmap of stringoptionalSent with each request. Use ${VAR} for secrets.
mcp.<name>.accessnone, read, write, controlreadread offers only read-only tools, as does write. control offers all of them, and anything not read-only asks first.
mcp.<name>.readOnlylist of string[]Tool names to treat as read-only, when the server doesn’t mark them.
mcp.<name>.namestringoptionalA friendly name, shown to the model.

The sections of the page, in order. See sections and layout.

OptionTypeDefaultWhat it does
layoutlist of objectoptionalThe sections of the page, in order.
layout[].titlestringoptionalThe section’s heading, and its tab’s name unless tab says otherwise.
layout[].tabColumnswhole numberoptionalcolumns on its own tab, when that should differ from the overview Range 1 to 6.
layout[].widgetslist of string or objectrequiredWhat’s in it: instance.widget, or an object with widget and the options below.
layout[].widgets[].widgetstringrequiredThe widget, as instance.widget.
layout[].widgets[].sizesmall, wide, medium, tall, largemediumsmall is its headline as a tile, wide that tile at twice the width, medium the card, tall the card over two rows, large the card across two columns of cards, with room for more.
layout[].widgets[].everynumber or stringoptionalHow often it refreshes while someone is looking, overriding the integration: seconds, or 30s, 5m, 1h.
layout[].widgets[].rowswhole numberoptionalhow many rows of its section it spans, so smaller cards can stack beside it Range 1 to 6.
layout[].widgets[].viewline, bars, stream, race, heatmap, ranked, pie, row, spotlight, coverflow, desk, windows, tickets, mosaic, flashback, albumoptionalhow to draw it: any chart for numbers, any gallery for pictures (src/shared/views.ts)
layout[].widgets[].titlestringoptionalReplaces the card’s title.
layout[].widgets[].iconstringoptionalReplaces the card’s icon.
layout[].widgets[].headerbooleanoptional“instance.widget”, or { widget, size } to pick a size: small (its headline as a tile), wide (that tile at twice the width), medium (the card), tall (the card over two rows), large (the card across two columns, with room for more) every overrides how often it refreshes while someone’s looking: seconds, or ”30s”, “5m”, “1h”
layout[].widgets[].tintstringoptionalA colour for this card’s surface.
layout[].widgets[].opacitynumberoptionalHow much of the tint shows, 0 to 1. Range 0 to 1.
layout[].widgets[].blurnumberoptionalHow hard the page behind this card blurs, in px. Range 0 to 80.
layout[].widgets[].backgroundstringoptionalAn image behind this card: https://… or /api/icon/name.
layout[].columnswhole number or objectoptionala number (from tablet width up), or per screen: { phone: 1, tablet: 2, desktop: 3 }
layout[].columns.phonewhole numberoptionalColumns on a phone. Range 1 to 6.
layout[].columns.tabletwhole numberoptionalColumns from tablet width. Range 1 to 6.
layout[].columns.desktopwhole numberoptionalColumns on a wide screen. Range 1 to 6.
layout[].gapnumberoptionalpx between its cards Range 0 to 64.
layout[].displaycards, tiles, rings, mediacardstiles: headline tiles with live graphs · rings: the same as ring gauges coloured by health · cards: full cards media: every item of the section’s media cards (streams, downloads…) as artwork tiles in one grid
layout[].placemain, left, rightmainleft / right: a sidebar beside the rest of the page (on wide screens), e.g. services as a tall list. columns then sets how many columns its list uses.
layout[].overviewbooleantruefalse: only on its own tab in the rail, not on the overview
layout[].variantfull, minimalfullminimal: smaller rows, just the essentials (e.g. services: icon, name, dot)
layout[].headerbooleantrue“instance.widget”, or { widget, size } to pick a size: small (its headline as a tile), wide (that tile at twice the width), medium (the card), tall (the card over two rows), large (the card across two columns, with room for more) every overrides how often it refreshes while someone’s looking: seconds, or ”30s”, “5m”, “1h”
layout[].headingbooleantruefalse: no section title above it on the page (it still names the tab)
layout[].iconstringoptionalIts tab’s icon, when the first card’s isn’t right.
layout[].tabstringoptionalwhich tab in the rail it belongs to (default: its title); sections can share one
layout[].chatbooleanfalseits tab also holds the assistant, full page
layout[].favoriteslist of stringoptionalon the overview, a status card shows only these services (and whatever’s down), with a link to the rest

One block per instance. The options depend on type: see integrations.

OptionTypeDefaultWhat it does
integrationsmap of object{}One block per instance. The key is the instance’s name, used in widget keys and in assistant.access.
integrations.<name>.typestringrequiredWhich integration this is. The other options depend on it.