6. Environment variables¶
"Environment variables" are settings the app reads at runtime — like who the admins are. Some are plain settings; one kind (secrets) is for sensitive values. You set these in the Cloudflare Pages dashboard so they apply to the live site.
The settings this app uses¶
| Name | Required? | What it does | Example |
|---|---|---|---|
ADMIN_EMAILS |
Yes | Comma-separated emails that are always admins. Bootstraps the very first admin. | robertbettencourt@totlcom.com |
APP_ENV |
Yes | Which environment this is. | production |
DEFAULT_SCHEDULE_WEEK_START |
Yes | The day weeks start on. Locked to Wednesday for TOTLCOM. | wednesday |
ONCALL_CHANGEOVER_WEEKDAY |
No | Weekday the home page rolls to the next on-call week (name or 0–6, Sun=0). Should match the week-start day. Defaults to Wednesday. |
wednesday |
ONCALL_CHANGEOVER_TIME |
No | Local 24-hour time (HH:MM) the roll happens. Defaults to 00:01. |
00:01 |
ONCALL_CHANGEOVER_TZ |
No | IANA timezone the weekday/time are read in. Defaults to America/Los_Angeles. |
America/Los_Angeles |
CWM_ENC_KEY |
Yes, to use ConnectWise (secret) | Encrypts the stored ConnectWise keys. See below. | (32-byte base64) |
CWM_SYNC_TOKEN |
No (secret) | Only if you want to trigger a sync over HTTP from outside the app. Not needed for the scheduled sync, which runs as a Worker. Leave unset. | (unset) |
GEMINI_API_KEY |
No (secret) | Optional AI helpers. Leave unset to keep AI fully off. Core scheduling never uses it. | (unset) |
GEMINI_MODEL_ORDER |
No | Fallback order for the optional AI. Only matters if the key above is set. | gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite |
Changing when the schedule flips to the next week
The Schedule home page shows who's on call "now". It advances to the next
week at ONCALL_CHANGEOVER_TIME on ONCALL_CHANGEOVER_WEEKDAY, read in
ONCALL_CHANGEOVER_TZ — by default Wednesday 00:01 America/Los_Angeles.
Because it's evaluated in a real timezone, it no longer flips early in the
prior evening (which happened when "today" was read in UTC). Leave these unset
to keep the defaults; set them (and redeploy) to change the cutover.
These also appear in wrangler.toml
The non-secret ones (APP_ENV, DEFAULT_SCHEDULE_WEEK_START, ADMIN_EMAILS) are listed under [vars] in wrangler.toml, which is the default. Setting them in the dashboard too is the reliable way to be sure production has the right values — the dashboard wins. Set ADMIN_EMAILS in the dashboard to be safe.
Set them in the dashboard¶
- Open your Pages project → Settings → Variables and secrets (older dashboards: Environment variables).
- Under Production, click Add variable for each plain setting:
ADMIN_EMAILS= your real admin email(s), comma-separated, no spaces needed:you@totlcom.com,boss@totlcom.comAPP_ENV=productionDEFAULT_SCHEDULE_WEEK_START=wednesday
- Click Save.
Set ADMIN_EMAILS before relying on the app
If ADMIN_EMAILS is empty in production, no one is an admin, the Admin tab is unreachable, and you can't promote anyone. Always set your own address here first.
The ConnectWise encryption key¶
The ConnectWise credentials themselves are not set here — they're entered on the app's ConnectWise tab so they can be rotated without a deploy. But something has to protect them where they're stored, and that's CWM_ENC_KEY.
Generate one:
Set it as a secret for both Production and Preview:
Write it down somewhere you'll find it
Cloudflare will never show you this value again — secrets are write-only once set. You'll need it again if you add the scheduled-sync Worker, so put it in IT Glue or a password manager now rather than rediscovering the problem later.
Rotating it¶
Changing CWM_ENC_KEY makes the saved ConnectWise credentials unreadable until you re-enter them. That's recoverable in about five minutes, and nothing else is affected — the schedule, member mapping, effective date, ledger and pending queue are all plain data.
- Generate a new key and save it somewhere durable.
- Set it on Pages for both Production and Preview.
- Set the same value on the scheduled-sync Worker, if you've deployed it:
npm run cron:secret. - Redeploy Pages.
- On the ConnectWise tab, re-enter all five ConnectWise values and Test connection.
Between steps 4 and 5 the credentials are unreadable and syncing will fail, so don't do it while someone is mid-push. The app will say so plainly — the ConnectWise tab shows a red banner explaining exactly this, rather than a green "configured" tick over a sync that can't run.
You need the ConnectWise private key to finish
Step 5 needs all five values, and ConnectWise shows a private key only once. If you don't have it saved, generate a new key pair first (System → Members → API Members → your API member → API Keys) — the member ID and everything else stays the same.
Use a different key locally
Your local .dev.vars should have its own CWM_ENC_KEY. A development secret shouldn't be guarding production keys.
Automatic ConnectWise sync (optional)¶
Cloudflare Pages cannot run anything on a schedule — only Workers have cron triggers. So the scheduled sync is a small separate Worker (workers/cwm-cron) bound to the same D1 database as the app. It runs entirely on Cloudflare; nothing has to stay switched on at the office.
Only set this up after the manual push has been running happily for a full rotation cycle.
# 1. Deploy the Worker (creates it the first time)
npm run cron:deploy
# 2. Give it the SAME encryption key as the Pages project — it reads the same
# stored ConnectWise credentials, so a different key cannot decrypt them.
npm run cron:secret # prompts for CWM_ENC_KEY
Then on the app's ConnectWise tab, tick Allow unattended sync and set the refusal limit.
That's it. It runs every four hours, and only touches weeks already marked pending by a schedule change.
The key must match
CWM_ENC_KEY on the Worker has to be byte-identical to the one on the Pages project. If it isn't, every run logs "Stored ConnectWise credentials cannot be decrypted" at error level — it checks this on every tick, not only when there is work to do, so a mismatch shows up immediately rather than weeks later when somebody is waiting on a sync.
The Worker has no public URL
workers_dev = false in its config. A workers.dev subdomain is publicly reachable by default, and this Worker writes to real calendars — so cron is the only way in. To run it on demand, use the Cloudflare dashboard (this Worker → Settings → Triggers → Trigger Cron Event) or npm run cron:dev locally.
Checking on it¶
Each run logs one line: what it did, or why it did nothing.
To fire one immediately rather than waiting up to four hours:
That runs the scheduled handler against the real database. Then, in a second terminal:
Use curl.exe, not curl, in PowerShell
PowerShell aliases curl to Invoke-WebRequest, which parses the response as HTML and prompts about script execution. curl.exe calls the real thing.
cron:run does not use the Worker's uploaded secret
wrangler dev reads secrets from a .dev.vars file beside the config, not from what wrangler secret put uploaded. So npm run cron:run needs workers/cwm-cron/.dev.vars containing CWM_ENC_KEY=..., and it tells you about that key rather than the deployed one.
To check the deployed Worker, watch a real tick in npm run cron:tail instead — or temporarily set crons = ["*/1 * * * *"], redeploy, watch, and set it back. A run that logs CWM_ENC_KEY is not set means the file is missing; is not the key the stored credentials were sealed with means it is genuinely the wrong value.
If you'd rather test the deployed Worker exactly as it runs on schedule, temporarily set crons = ["*/1 * * * *"] in workers/cwm-cron/wrangler.toml, run npm run cron:deploy, watch one tick in cron:tail, then set it back and redeploy. A refused batch logs as a warning with the counts; a routine "nothing pending" stays at info level so the log is worth reading.
To change the schedule, edit crons in workers/cwm-cron/wrangler.toml and redeploy. To turn it off, untick Allow unattended sync in the app — no redeploy needed, and the Worker keeps running harmlessly and doing nothing.
Secrets (only if you use optional AI)¶
Secrets are hidden values. The app's only optional secret is GEMINI_API_KEY, and you can skip it entirely — AI features are off by default and core scheduling never sends names or phone numbers anywhere.
If you do want the optional AI helpers, set the key as a secret (not a plain variable) either in the dashboard (mark it as a secret) or from the terminal:
It prompts you to paste the value, which is then encrypted.
Make changes take effect¶
Environment variable changes apply on the next deploy. Trigger one:
- the easy way — Pages project → Deployments → ⋯ on the latest → Retry deployment, or
- push any commit to GitHub.
Local development settings (not production)¶
For running the app on your own machine, settings live in local files that are never committed: .env (copied from .env.example) and .dev.vars. In particular, .dev.vars contains DEV_ADMIN=1, which makes your local machine act as an admin during development (production never sets this). See Deploying updates → Running locally.