bsky-avatar#
My Bluesky avatar updates itself. It changes with the weather where my phone is, the season, the date, and wherever I'm travelling, and I can pin a custom one by hand for any stretch of time (a holiday, a heads-down week).
It stacks hand-drawn transparent layers over a fixed portrait of me (the
avatar-system design) and pushes the result to gui.do on
Bluesky, and optionally mirrors it to Signal. It runs on a schedule a few times
a day, and every run is idempotent, so a missed run or an extra one does no harm.
Design, internals, and the decisions behind it: ARCHITECTURE.md.
How it decides#
Each run builds a small state object, { bg, weather, season, holiday, ring, halo }, and renders it in a fixed z-order (ported straight from the design's
studio.html). Each field resolves on its own, with a manual override winning
first:
| Field | Source |
|---|---|
bg |
manual override, else the country flag where my phone is, else a date background (e.g. Pride), else none |
weather |
Open-Meteo current weather for the phone's coordinates, mapped to one of 6 layers |
season |
the date (dropped while a flag is up) |
holiday |
the date (config/holidays.yaml: Sinterklaas, King's Day, NYE, birthday, …) |
ring |
daily colour rotation at home, neutral while abroad |
halo |
on only when a flag background is showing |
Where my phone is#
Coordinates come from a Home Assistant person entity, rounded to about a kilometre before they leave the LAN, then reverse-geocoded to a country through OpenStreetMap Nominatim. If I'm not home, that country's flag fills the background. Every external call fails soft: if HA, Nominatim, or Open-Meteo is down, it falls back to home coordinates or the last sensible value instead of crashing.
Configure#
The repo ships example configs. Copy each one and edit your own. The real files are gitignored and live only on the NAS, because they hold personal things like travel dates that don't belong in a public repo.
config/settings.example.yaml→settings.yaml: timezone, home country/coords, HA entity, birthday, abroad ring.config/holidays.example.yaml→holidays.yaml: date → holiday layer, plus ranges (festive week, Pride month).config/overrides.example.yaml→overrides.yaml: manual periods, either a partial state or a full pre-made image..env.example→.env: secrets (Bluesky app password, HA token).
Layer and flag names have to match the files in assets/layers/ (without .png).
Run#
npm install
cp .env.example .env # fill in BSKY_APP_PASSWORD + HASS_TOKEN
cp config/settings.example.yaml config/settings.yaml # same for holidays + overrides
npm run dry # compute + render to ./out.png, no upload
node src/index.js --state '{"bg":"flag-gr","weather":"weather-rain","ring":"none","halo":true}' --dry-run # preview any combo
node src/index.js # real run, uploads only if the state changed
node src/index.js --force # upload even if nothing changed
Schedule#
It runs as a single detached, self-scheduling container (Docker): updates at
04:00, 09:00, 14:00, 18:00 and 22:00 (Amsterdam time), restarts on boot, and
fails soft. Holidays show over a window from 22:00 the evening before to 04:00
the morning after (so they land across time zones); the 22:00 and 04:00 runs
flip those edges. A sample container setup is in deploy/.
Why an app password#
This logs in with a Bluesky app password, a deliberate exception for a low-stakes
personal toy (I use OAuth everywhere else). The secret stays in .env on the NAS
and never gets committed.