Documentation

How VitalsBar works

The formats VitalsBar expects and exactly how it behaves, from the source list you type in Settings to the single colour it shows in your menu bar.

Overview

VitalsBar polls the public status page of each service you care about, reduces every response to one of four health states, and shows the single worst state as a colour (and, when something is wrong, a count) in your menu bar. Click the icon for a pane that breaks the roll-up back down per service and per subsystem.

Everything is local. There is no VitalsBar account, server, or telemetry: the app talks only to the status pages you list, and stores your settings, silences, and history in macOS UserDefaults on your Mac.

Sources Label=host
(one per line)
Fetch GET /api/v2/
summary.json
Parse components
→ health
Roll up worst state
wins
Menu bar colour + badge

Status sources

The Status sources field in Settings is a plain-text list, one entry per line. Every entry must use the Label=host form:

AWS (via Updog)=https://updog.ai/status/amazonaws
Claude=status.claude.com
Cloudflare=www.cloudflarestatus.com
Datadog EU=status.datadoghq.eu
GitHub=www.githubstatus.com
Linear=linearstatus.com
OpenAI=status.openai.com
  • Label is everything before the first =; it’s the display name shown in the pane. host is everything after it.
  • A bare host (status.openai.com) is all you need; VitalsBar finds its status API for you. A leading scheme is optional, so https://status.openai.com/ is equivalent. For a status page split into several sub-pages (incident.io calls them regions/products), keep the sub-page’s path to scope to just that one: www.finstatus.com/eu-hosting.
  • Only the Label=host form is honoured. Bare hosts (a line with no =), and entries with an empty label or empty host, are ignored. Blank lines are ignored too.
  • A line whose first non-blank character is # is a comment and is skipped entirely: not fetched, not classified, not shown. It’s the way to park a source without losing how it was spelled: # Datadog US=status.datadoghq.com. Only a leading # counts, so one inside a value (a URL fragment, say) is left alone.
  • If the field ends up with no valid entries, VitalsBar falls back to the seven defaults above, including when you comment every line out, so #-ing the whole list restores the defaults rather than emptying the pane.
  • Leaving the field blank prefills it with those defaults the next time Settings opens, so you have an editable starting point.

Supported providers, auto-detected

VitalsBar recognises four kinds of public source and works out which one a host serves the first time it sees it (the result is cached per host, so it’s a one-time cost, not a per-refresh one; updown.io is recognised by host name alone, with no probe at all):

  • Atlassian Statuspage: the Statuspage API (https://<host>/api/v2/summary.json). Covers services hosted on statuspage.io: GitHub, Cloudflare, Datadog, Anthropic, and hundreds more.
  • incident.io: its component API (https://<host>/proxy/<host>), since incident.io’s Statuspage-compatible endpoint omits components. A multi-region page (such as Intercom’s Fin) is scoped to the sub-page you point at. An incident opened against no component at all still shows up, as its own silenceable row.
  • Updog (Datadog’s cross-provider outage tracker): point at updog.ai for the whole board, or a single provider with updog.ai/status/<provider> (e.g. /status/amazonaws, whose services become components). Updog infers outages from Datadog telemetry rather than a provider’s own reporting, so a detected outage shows as Degraded.
  • updown.io: point at a status page (demo.updown.io, updown.io/p/<token>) or a single published check (updown.io/<token>). updown publishes no status API, so VitalsBar reads the page itself; on a multi-check page every check becomes a component, keyed by its updown token. Because updown checks for reachability, a failing check is Down rather than degraded, and a disabled check is left out entirely: it isn’t being watched, so it carries no signal either way. Note that updown.io on its own is updown’s homepage, not a status page: it advertises the product with sample checks in the very same markup, so VitalsBar refuses it rather than report a green provider you never configured.

All four give component-level detail, per-component silencing and the menu-bar badge, and all are free, with no license needed. (The exception is a source that is a single check (a one-check updown page), which is reported at the provider level, since a one-component breakdown would just be the row twice.) A host that serves none resolves to the Unknown state.

Stable ids & silence survival

Each source gets an id derived from its host, not its position in the list. A host that matches a known default inherits that default’s slug (e.g. openai); any other host uses the host string itself. True collisions are disambiguated with a numeric suffix (-2, -3). Because ids are stable across reorders and edits, the services you’ve silenced stay silenced when you rearrange the list.

Migration: an older comma-separated list is converted to one-per-line automatically on first launch of a build that expects newlines.

Pro & relay feeds

Public status pages are free, forever. What VitalsBar Pro unlocks is relay feeds: a source whose endpoint serves the native vitalsbar.v1 JSON format instead of a Statuspage summary. That’s the format a self-hosted relay emits: private, aggregated status from your own infrastructure (for example live incident.io incidents), so provider credentials stay on the relay host rather than on each laptop.

ON YOUR MAC VitalsBar EXTERNAL · FREE Public status pages · Atlassian Statuspage · incident.io status pages · Updog · updown.io pages polled directly over HTTPS YOU HOST · PRO Relay holds provider credentials PRIVATE API incident.io live incidents free · direct Pro · vitalsbar.v1 API key stays here
Free · public pages, polled straight from your Mac Pro · private status via a relay you host

The gate is on the format, not the URL: any source that resolves to a vitalsbar.v1 document requires an active Pro license. A Statuspage or incident.io status-page source is never gated. Without a license, a relay source is recognized but not resolved; its row reads VitalsBar Pro required rather than silently disappearing.

incident.io: two different things. A public incident.io status page is a free source like any other. What Pro’s relay adds is live incident.io incidents, pulled straight from your own incident.io account (its API key lives on the relay): private, internal detail a public status page doesn’t expose.

Licensing & devices

  • Enter your key under Settings → License. One license covers a team; each Mac counts as one device (Polar calls it an activation).
  • Activating validates the key with Polar and claims a device. If every device is in use, activation is refused until one is freed. Remove license from this Mac releases the device for another one.
  • The key and its activation id are stored in the macOS Keychain; only the resulting verdict is cached in UserDefaults.
  • VitalsBar re-checks the license on launch and periodically. If Polar is briefly unreachable, an offline grace window keeps Pro working; only a positive revoke (or grace expiry) turns it off, so a network blip never locks you out.

The vitalsbar.v1 format

vitalsbar.v1 is VitalsBar’s native feed format: the contract a relay (or any endpoint of yours) serves so the app can show it as a provider. It’s a plain JSON document served over HTTP; VitalsBar auto-detects it by the schema field, so the same source line works whether it points at a public Statuspage or your own feed. The format is open: publishing one is the intended way to connect your own services.

{
  "schema": "vitalsbar.v1",
  "name": "Payments",
  "url": "https://status.example.com",
  "components": [
    { "id": "api",     "name": "API",      "status": "operational" },
    { "id": "webhook", "name": "Webhooks", "status": "degraded", "label": "elevated latency" },
    { "id": "payout",  "name": "Payouts",  "status": "down",     "label": "processor outage" }
  ]
}

Fields

FieldRequiredMeaning
schemayesMust be the literal "vitalsbar.v1". Any other value and the payload is not treated as native.
namenoProvider display name; overrides the source’s Label=.
urlnoLink opened when the row is clicked; falls back to the source host.
componentsnoThe subsystems that make up this provider (see below). Omit for a single-signal provider.
healthnoOverall health, only used when there are no components (the page-indicator role). One of the four states below.
detailnoOne-line subtitle. Defaults to a rolled-up summary of the components.

Each component

FieldRequiredMeaning
idyesStable per-component id. It keys silences as <source>::<id>, so keep it stable across polls.
nameyesComponent display name.
statusyesOne of the four health states below. Statuspage words (e.g. major_outage) are also accepted and mapped; any unrecognised non-operational value becomes Degraded, never silently Unknown.
labelnoHuman status shown in the row and the silence menu (e.g. “elevated latency”).

The four health states (the values status and health accept):

  • operationalOperational
  • degradedDegraded
  • downDown
  • unknownUnknown

Roll-up

With components, the provider’s overall health is the worst component state, exactly like a Statuspage source, so one component Down turns the row red. Without components, the top-level health is used directly. Either way, a component-less vitalsbar.v1 reply and a Statuspage page-indicator behave the same.

Consuming a vitalsbar.v1 feed is a Pro feature. The format itself is open: you can serve and inspect it freely; the license gates the app’s use of it, not knowledge of it.

Self-host the relay

The relay is a small self-hosted service that emits vitalsbar.v1 feeds, so provider credentials (like an incident.io API key) live on one host you control rather than on every laptop. Each integration is mounted at its own path and served as an isolated feed: live incident.io incidents, and CI status from GitHub.

Feeds are refreshed on a timer, never by a request. A background task per integration polls the provider and stores a snapshot; requests only ever read it. That’s what lets one relay serve a whole team: provider load is 3600 / interval calls an hour no matter how many people poll, how often, or how many arrive at once. There is deliberately no path from a request to a provider API, so a burst of clients can’t stampede one.

Feeds carry an ETag and Cache-Control: no-cache, so a client that already has the current body revalidates and receives a 304 rather than downloading it again. The app does this on its own, with no configuration, and since a feed only changes when its provider does, most polls are 304s in steady state.

A failed refresh keeps the last good snapshot rather than blanking the feed, so a blip doesn’t turn a working row gray. The snapshot keeps ageing though, and past RELAY_STALE_AFTER_SECONDS the row reports Unknown with its age and the reason. It is never reported as Down: the relay being unable to reach a provider is not the same as that provider being broken.

Run it with Docker

Pull the prebuilt image and pass your provider key:

docker run -d --name vitalsbar-relay \
  -p 8787:8787 \
  -e INCIDENT_IO_API_KEY=inc_… \
  ghcr.io/jbbarth/vitalsbar-relay:latest

The container serves on port 8787 and exposes:

PathPurpose
/incident.ioLive incident.io incidents as a vitalsbar.v1 feed (needs INCIDENT_IO_API_KEY)
/github/ciCI status for every watched repository, one component each
/github/ci/<owner>/<repo>Just that repository, as its own row
/Index of mounted integration paths
/healthzLiveness probe, plus each snapshot’s age and last error

Configuration

Env varWhat it doesDefault
INCIDENT_IO_API_KEYEnables the /incident.io feedunset (feed disabled)
RELAY_INCIDENT_IO_MIN_SEVERITYReport only this severity and above (fails open if unknown)all severities
RELAY_AUTH_TOKENRequire a token on the feeds; sent as ?token=… from the appunset (open)
RELAY_REFRESH_SECONDSHow often each provider is polled60
RELAY_STALE_AFTER_SECONDSAge at which a snapshot stops being trusted and the row goes gray600
PORT / WEB_CONCURRENCYListen port / worker count8787 / 1
LOG_LEVELLog verbosity (info/debug)info

The GitHub CI feed takes its own GITHUB_APP_* and RELAY_GITHUB_* variables, listed with its setup. A provider may also override the shared refresh cadence: GitHub polls every 30 seconds, since one batched query costs a single rate-limit point.

Point the app at it

Add one source line per feed in Settings → General → Status sources, using the full URL so it’s fetched as-is:

Incidents=https://relay.your-co.com/incident.io

App Transport Security: a relay on another host must be served over HTTPS: the app blocks plain http:// to non-loopback hosts. On the same Mac, http://127.0.0.1:8787/incident.io is fine (use 127.0.0.1/localhost, not 0.0.0.0).

One worker, on purpose. Each worker runs its own refresh timers and holds its own snapshots, so a second worker polls every provider twice and keeps a second cache that can disagree with the first, so a client polling twice in a row could see the feed jump backwards. Raising WEB_CONCURRENCY means moving the snapshots somewhere shared first. Request recycling is off for the same reason: restarting a worker throws its cache away.

Security: without RELAY_AUTH_TOKEN the relay is open to anyone who can reach the port. Keep it behind a network boundary (VPN/private network) and serve HTTPS if it leaves your LAN. Never expose it unauthenticated to the internet.

CI status from GitHub

The relay can report whether main is green across your repositories, including private ones, which is the point: the credential lives on the relay, not on every laptop. Point one source line at a single repository to give it its own row, or at the whole feed to get one row with a component per repository.

The relay polls GitHub on its own timer and serves every client from that one snapshot, so a team of any size costs GitHub the same handful of API calls an hour. A single batched GraphQL query covers every watched repository.

What “green” means

A repository is judged by the last finished commit on its default branch, not by whatever is building right now. Push to main and the row keeps its previous verdict until the new checks settle. Otherwise every push would flip the row for as long as a build takes, and the signal would be noise. The verdict comes from GitHub’s own rollup, which combines Actions check runs and commit statuses:

Last settled commitHealth
All checks passedOperational
A check failed or erroredDegraded
Every recent commit still buildingUnknown
No checks on the default branchUnknown

A failing build is amber, not red. Red is worth reserving for “a service people depend on is unavailable”. A broken main is a problem for the team that owns it, and on a busy repository it happens often enough that spending red on it would teach everyone to ignore red.

The rollup counts every check on the commit: deploy previews, coverage bots and third-party integrations included. If one of those is flaky it will turn the row red alongside the real build.

Create a GitHub App

Use a GitHub App rather than a personal access token. It can be scoped to exactly the repositories you want, its permissions are read-only and auditable, it gets a higher rate limit, and (the practical argument) it doesn’t stop working the day the person who created the token leaves. Create it under the organisation, not your own account, for the same reason. You’ll need to be an org owner, or hold the “GitHub App manager” role.

  1. Open the registration form

    https://github.com/organizations/<YOUR-ORG>/settings/apps/new

    Or navigate: organisation Settings → Developer settings → GitHub Apps → New GitHub App.

  2. Name it, and turn webhooks off

    GitHub App name must be unique across all of GitHub: VitalsBar CI, prefixed with your org if that’s taken. Homepage URL is required but unused; your relay’s URL will do. Leave Callback URL blank; nobody signs in through this App.

    Under Webhook, uncheck Active. The relay polls, so there is no endpoint to deliver to; leaving it on just accumulates failed deliveries.

    Set Where can this GitHub App be installed? to Only on this account.

  3. Grant four read-only permissions

    Under Repository permissions, and nothing else:

    PermissionAccessWhy
    ContentsRead-onlyThe default branch and its commit history
    ChecksRead-onlyCheck runs in the rollup
    Commit statusesRead-onlyCommit statuses in the rollup
    MetadataRead-onlyMandatory; selected for you

    Both Checks and Commit statuses are needed. The rollup merges the two, and with only one of them GitHub doesn’t refuse cleanly: it returns Resource not accessible by integration in the middle of an otherwise fine response, which surfaces later as an empty feed.

    Leave every organisation and account permission at No access, and subscribe to no events.

  4. Create it, then generate a private key

    Note the App ID shown at the top of the page that follows. Then scroll to Private keys → Generate a private key; a .pem file downloads. GitHub shows it once, so keep it and put it wherever the relay host keeps secrets.

  5. Install it on the organisation

    Install App in the sidebar, then install on the organisation with All repositories. There is nothing to hand-pick and nothing to keep in sync as repositories come and go: the relay narrows the list itself, as described below.

  6. Note the installation id

    After installing, the browser sits on a URL ending in the number you need:

    https://github.com/organizations/<ORG>/settings/installations/<INSTALLATION_ID>

Which repositories get watched

Granting the App an entire organisation would mean querying hundreds of repositories, most of them dormant, forks or experiments: slow, noisy, and a list nobody wants to curate. So the relay derives the watched set itself: the repositories the App can see, minus archived ones, minus anything not pushed in the last RELAY_GITHUB_ACTIVE_DAYS days.

That filtering is free. Listing an installation’s repositories already returns each one’s last push time, so deciding what’s active costs no extra call and reads no commit history. The list is recomputed on each refresh, so a repository that goes quiet drops out and one that wakes up comes back, with no configuration either way.

Any branch counts. The push time GitHub reports covers pushes to any branch, not just the default one, so a repository with only feature-branch activity stays in the set. That’s deliberately generous: the cost of including a quiet repository is one entry in a batched query, while wrongly excluding one means a row that silently stops updating.

Check the App before wiring the relay up

Most mistakes above surface later as an empty feed rather than an error. This pre-flight walks the whole chain: signs a JWT, names any missing permission, finds the installation, mints a token, lists the granted repositories, then runs the real CI query and prints each repository’s last settled commit alongside the query’s measured cost.

curl -O https://vitalsbar.org/verify-github-app.py

export GITHUB_APP_ID=123456
export GITHUB_APP_PRIVATE_KEY_PATH=~/Downloads/vitalsbar-ci.…private-key.pem
uv run --with "pyjwt[crypto]" --with httpx python verify-github-app.py

GITHUB_APP_INSTALLATION_ID is optional; with one installation the script finds it and prints it. Needs uv, or run it with any Python that has pyjwt[crypto] and httpx. (download instead)

Configure the relay

Env varWhat it doesDefault
GITHUB_APP_IDRequired. The App ID from step 4unset (feed disabled)
GITHUB_APP_INSTALLATION_IDRequired. The number from step 6unset (feed disabled)
GITHUB_APP_PRIVATE_KEYRequired, unless the path below is set. The .pem contentsunset (feed disabled)
GITHUB_APP_PRIVATE_KEY_PATH…or a path to the .pem instead, for a mounted secretunset
RELAY_GITHUB_ACTIVE_DAYSOnly watch repositories pushed within this window7
RELAY_GITHUB_HISTORY_DEPTHCommits scanned for the last settled one20

With a required one missing, the relay warns at startup naming the variable, and the feed reports Unknown saying the same thing. It never fails silently or reports a false green.

WARNING:  GitHub CI feed will report unknown: GITHUB_APP_ID is not set

A worked example, with the key mounted as a file rather than pasted into the environment:

docker run -d --name vitalsbar-relay \
  -p 8787:8787 \
  -v /etc/vitalsbar/github-app.pem:/secrets/github-app.pem:ro \
  -e GITHUB_APP_ID=123456 \
  -e GITHUB_APP_INSTALLATION_ID=98765432 \
  -e GITHUB_APP_PRIVATE_KEY_PATH=/secrets/github-app.pem \
  ghcr.io/jbbarth/vitalsbar-relay:latest

Pasting the key into an environment variable? Multi-line values get mangled routinely: shells, compose files and secret managers all like to turn the newlines into a literal \n. The relay repairs that, because the error it otherwise causes (could not deserialize key data) reads like a corrupt key rather than a quoting problem. Mounting the file avoids the question.

Why scan several commits? A busy monorepo can have a dozen commits building at once, so the last settled one may be well behind the head. Depth is nearly free in rate-limit terms (GraphQL prices a query by the connections it opens, not the rows it returns), but it is real work for GitHub to answer, and asking for too much at once earns a gateway error rather than a reply. Hence a modest default. Raise RELAY_GITHUB_HISTORY_DEPTH if a repository keeps reporting Unknown because every commit in the window was still building.

Point the app at it

Checkout=https://relay.your-co.com/github/ci/your-org/checkout
Billing=https://relay.your-co.com/github/ci/your-org/billing
All CI=https://relay.your-co.com/github/ci

A per-repository line gives that repository its own row, with its own history sparkline, its own silence and its own click-through. The bare /github/ci line gives one row covering everything, with each repository as a component you can silence individually. Use whichever matches how closely you watch them.

How a check works

For each source, VitalsBar requests one endpoint and turns the response into one row. Which endpoint depends on the kind detected for that host: an Atlassian Statuspage serves https://<host>/api/v2/summary.json, incident.io its component API, and an updown.io page is fetched as-is and read as HTML. The decision below describes a summary.json response, which carries both the page-level indicator and every component’s status, which is what lets a provider’s health reflect only the subsystems that are actually degraded. The other formats are normalised into those same components, so everything downstream behaves identically.

summary.json
HTTP not 2xx / unreachable Unknown “Could not reach status page”
No components listed fall back to the page indicator
Components, none affected Operational “All Systems Operational”
Components affected worst affected component wins
Ongoing incident mapped to no component Degraded listed as its own subsystem (incident.io)

That last branch is an incident.io speciality: a provider can open an incident without attaching it to any component on the page: every dot stays green while something is plainly broken. VitalsBar surfaces that incident as a subsystem of its own, named after the incident and tagged with its status (investigating, identified, monitoring). It colours the provider like any other affected subsystem, and it can be silenced on its own without muting the provider. An incident that does list components is already carried by those components, so it gets no extra row; and one that has recovered (impact back to operational) reads green.

Component status → health

Each leaf component’s Statuspage status maps to a health state:

Statuspage component statusHealth
operationalOperational
degraded_performanceDegraded
partial_outageDegraded
under_maintenanceDegraded (unless overridden)
major_outageDown
any other non-operational valueDegraded

When a page has no components

Some pages expose only a single overall indicator. VitalsBar maps it directly:

Page indicatorHealth
noneOperational
minor, maintenanceDegraded
major, criticalDown
anything elseUnknown

The one-line detail

  • Nothing affected → “Operational” (or “All Systems Operational”).
  • One subsystem affected → “<Name> — <state>”, e.g. “API — degraded”.
  • Several affected → “N components affected”.

The health model

Every signal lands in exactly one of four states, ordered by severity:

Operationalseverity 0
Unknownseverity 1
Degradedseverity 2
Downseverity 3

The severity ordering is what drives the roll-up: the worst state always wins, so “one thing is down” is never hidden behind “most things are fine”. Note that Unknown outranks Operational but is quieter than an actual problem; an unreachable page nudges the signal without crying outage.

An incident.io incident the provider hasn’t mapped to any component joins the roll-up as a synthetic subsystem at Degraded, so worst-wins still applies and the incident stays addressable on its own rather than only through its provider.

Components Operational Degraded Operational
Provider Degraded
Overall Degraded

Per-source overrides

Two built-in defaults carry overrides so their roll-up reflects product health rather than noise. A source you type whose host matches a default inherits that default’s overrides automatically.

Treat-as-operational

Some providers report normal operating modes as non-operational statuses. Cloudflare, for instance, lists re-routed edge locations as under_maintenance, which is routine and shouldn’t turn the provider amber. Such statuses are forced back to Operational.

Group scoping

A page can expose hundreds of components. Cloudflare enumerates a component per city; counting each one would make product health hostage to a single data centre. When a source is scoped to named component groups (e.g. “Cloudflare Sites and Services”), only components inside those groups count; group headers themselves are always skipped.

These overrides ship with the defaults for Cloudflare. Other hosts you add are evaluated straight, with no components ignored.

Silencing

Silencing mutes a signal you don’t want colouring your menu bar: a whole provider, a single subsystem (e.g. a compliance region you never use, or one updown.io check), or a single ongoing incident the provider hasn’t mapped to any component. A silenced signal stops contributing to the menu-bar colour and badge, but stays visible in the pane’s Silenced group so you can bring it back.

Silencing is view-only: it never touches the provider or its incident. It’s stored locally in UserDefaults and does not sync across machines (the tradeoff of staying backend-free).

Durations

For 2 hours
For 1 day
For 2 days
For 1 week
Until resolved
Forever
  • Timed silences (2 hours … 1 week) expire on their own after each refresh.
  • Until resolved clears the moment the signal returns to Operational.
  • Forever permanently mutes something you never care about, until you un-silence it by hand.

Each silence is keyed to a stable id (<providerId> for a whole provider, <providerId>::<componentId> for one subsystem, or <providerId>::incident:<incidentId> for one unmapped incident), so it survives restarts and list reorders. On an incident, Until resolved clears as soon as that incident leaves the page’s ongoing list.

History & sparkline

Statuspage exposes only current status, so VitalsBar builds the per-row sparkline from observations it accumulates itself. Every refresh records one tick (a timestamp + health) per source.

  • History is kept per source, rolling, up to the last 40 observations.
  • It’s persisted to UserDefaults so the sparkline is populated across restarts.
  • The pane renders it as spaced bars, coloured by each tick’s health.

With a 2-minute interval, 40 ticks is roughly the last 80 minutes of history; at 5 minutes it’s a little over three hours.

Refresh & polling

VitalsBar checks once immediately at launch, then repeats on your chosen interval. All configured sources are checked concurrently, then re-sorted back into the order you listed them.

Every 1 minute
Every 2 minutes default
Every 5 minutes

The header’s refresh control forces an immediate check without waiting for the timer. After every refresh, timed silences are expired and “until resolved” silences whose signal recovered are cleared.

Updates

VitalsBar updates itself with Sparkle, the standard updater for Mac apps distributed outside the App Store. It reads an appcast (a small XML feed listing recent builds) from the same host that serves the downloads:

https://files.vitalsbar.org/vitalsbar/appcast.xml
  • Background checks are on by default and run about once a day. The Check for updates automatically toggle in Settings turns them off; Check now beside it runs one immediately, whatever the toggle says.
  • Every build is signed twice. Sparkle verifies an EdDSA signature against a public key baked into the app, so a build not signed with the release key is refused even if the feed or the download host were tampered with. Separately, builds are Developer ID signed and notarized by Apple (since 0.2.0), so macOS opens them without a Gatekeeper prompt.
  • Nothing about your Mac is sent. The appcast is a static file fetched over HTTPS; Sparkle’s optional system-profile reporting is not enabled.
  • The feed carries a rolling window of recent builds rather than every release ever made. Sparkle always offers the newest one, so an install that’s been idle a long time goes straight to the current version.

The app and its updates are free. A Pro license unlocks relay feeds; it isn’t needed to receive updates.

Settings & storage

Everything VitalsBar remembers lives in macOS UserDefaults under the app’s domain, with no files to manage and nothing off your Mac.

KeyWhat it holdsDefault
VitalsBar.sourcesThe Label=host list, one per linethe seven defaults
VitalsBar.refreshIntervalMinutesPolling interval in minutes2
VitalsBar.showOperationalList operational systems in the pane, or only problemstrue
VitalsBar.silencesPer-signal silences (JSON)empty
VitalsBar.checkHistoryRolling per-source observation history (JSON)empty
VitalsBar.licenseStatusCached Pro verdict (active/revoked)unset
VitalsBar.licenseExpiresAtEnd of the licensed update window, if anyunset
VitalsBar.licenseCheckedAtLast successful license re-check (drives offline grace)unset

The Pro license key and its activation id are the only secrets, and they live in the macOS Keychain, not in UserDefaults. The keys above cache just the verdict.

Also in Settings

  • Launch at login: registers VitalsBar as a macOS login item.
  • Automatic updates: toggle Sparkle’s background checks, or use Check now for an immediate one. See Updates.

Something unclear or missing? It probably means the behaviour is worth refining; the docs track the app release by release. See the changelog for what recently changed.