Ready to build, not yet deployed — deploying the repointed scheduler command
without the new image would break the seed on schedule, which is why this sits on
a branch with the ordering written down.
- Dockerfile: COPY sync-keys.js /app/code/sync-keys.js; rule 3 in the header now
states the one exception (operational scripts that must not depend on a volume).
- CloudronManifest.json: scheduler command repointed to /app/code/sync-keys.js.
- README: ordering (build -> deploy -> verify -> delete the volume copy), and the
table row moved from the volume layer to the image layer.
After deploy: delete /app/data/sync-keys.js, but keep it until you are past the
rollback window — the previous image tag still runs the volume copy.
Three drifts between this repo and reality, found while moving the per-member key
seed into the app:
- The 'verify after every image deploy' block asserted the CREAM palette
(rgb(250,248,245)). The deployed palette has been the green wash
(rgb(234,243,234) with white text areas) for some time, so a correct deploy
would have read as a failure — and invited someone to 'fix' the CSS back.
- The block still verified against librechat-test.inference.coop; the swap to
chat.inference.coop happened 2026-10-21 and that alias is not provisioned.
- Nothing checked that the scheduler task's script exists in the running image,
which is exactly the failure mode a manifest repoint can introduce.
release.sh encodes the documented release flow (dirty-tree guard, always
--no-cache, pin-before-deploy) and its verify subcommand runs checks 1-5 plus the
seed task's log lines. sync-keys.js now prints its own md5 prefix on every run,
so a stale volume copy can never hide again — the repo copy's md5 is the
authority.
Verified: ./release.sh verify passes all six checks against the live app;
./release.sh volume sync-keys.js confirms md5 parity.
The first scheduled run failed closed (good) — Cloudron's scheduler addon runs
each task in its OWN container, so the app's process env is absent there and
/proc/1/environ belongs to the task launcher, not to LibreChat. CREDS_KEY/CREDS_IV
now come from /app/data/.librechat-creds (root:cloudron 0640), written from the
live app env; a stale key makes the decrypt-against-existing-rows guard refuse to
write rather than corrupt member keys. Credential failure now also writes
/app/data/key-sync-diag.txt so a future failure explains itself.
Verified: the 22:40 scheduled fire logged 'creds check: decrypt-ok (25 existing
row(s))' / 'done: 50 seeded, 15 skipped'.
The per-member key seed ran from a steward cron on the co-op agent VM, shelling
into this app over the Cloudron API. That made a member-facing function depend on
a separate machine's uptime and paths (it went silently stale once, exiting 127
on schedule while the job looked healthy).
- CloudronManifest.json: addons.scheduler gains librechat_key_sync
(cd /app/code/librechat/api && node /app/data/sync-keys.js, */10 * * * *).
Official addon set (postgresql, mongodb, localstorage, oidc, sendmail) and the
dockerImage pin are untouched.
- sync-keys.js: refuse to write unless CREDS_KEY can first decrypt an existing
key row, and unless each fresh ciphertext round-trips back to the plaintext.
Scheduled runs get a different environment than an interactive exec, so a wrong
or absent CREDS_KEY must fail loudly instead of overwriting working member keys.
A host-side daily job creates the next day's dated log files as root:root at
00:00 UTC; LibreChat (cloudron user) then EACCES-crashes on its first log
write of the day (2026-10-22 and 2026-10-23 incidents). The guard pre-creates
today's and tomorrow's files as cloudron and repairs strays every minute.
Defense-in-depth until the host rotation config is fixed.
- maskable-icon.png regenerated: logo at ~66% on the brand background —
inside the maskable safe zone, so Android no longer crops it ('zoomed in').
- index.html: viewport-fit=cover + manifest link without
crossorigin=use-credentials (Firefox Android fails to resolve manifest
icons on credentialed fetches — missing launcher icon).
- custom.css: standalone-mode safe-area padding so app content clears the
system bars; the bands show the theme-correct page background.
The Oct 19 theming (light = #eaf3ea dashboard green wash, white cards/surfaces,
per approval) was never committed - the Oct 21 pin-migration rebuild from this
repo silently reverted the deployed theme to the older cream iteration.
- custom.css: light block restored to the approved green-wash palette
(primary #eaf3ea, white secondary/chat cards, green hover/tertiary/border);
loading screen + bare page background match.
- Dockerfile: manifest theme_color/background_color and the index.html
theme-color media pair now follow the live palette (#eaf3ea light /
#22271f deep pine dark).
- custom2.js: sync ONE meta theme-color to the app's resolved theme
(html.dark class + prefers-color-scheme listener) so Android status/nav
bands and iOS safe areas track the theme at runtime; the static media
pair remains as the no-JS fallback.