Dentolize · Layered Sandbox Settings Walkthrough
On this page1. The sandbox this PR deployed is live2. Logged in as the seeded owner3. Tracing this sandbox's own settings back through the layers4. What the compose step actually does, in order

Walkthrough

This PR has no product screen of its own — it changes a GitHub Actions workflow, not the clinic app. There's nothing to click through in the Dentolize UI that looks different because of it. What is real and screenshottable is the sandbox this exact change produced: the deploy job that runs the new layering logic is the same job that stood up ci-sandbox-layered-settings.sandbox.anastawfik.com, the environment used for this documentation. So this walkthrough proves the pipeline works by showing its output, then traces what that output was made of.

1. The sandbox this PR deployed is live

Login screen of the sandbox deployed by this PR's own workflow run
Login screen of the sandbox deployed by this PR's own workflow run

This is the ordinary Dentolize login page — nothing about it changed. What matters is that it's serving at all: this sandbox only exists because the deploy job in .github/workflows/sandbox.yml ran the new "Compose sandbox settings" step and then handed a settings file to the sandbox CLI. If the new layering logic had thrown (a bad JSON layer, a rejected secret reference), this page would 404 instead.

2. Logged in as the seeded owner

Dashboard after logging in as the rich-seed owner account for
Dashboard after logging in as the rich-seed owner account for "Sandbox Dental"

Logged in with the owner / sandbox company login from the roster this sandbox seeded. The company name "Sandbox Dental" and the full role roster (doctor1, doctor2, receptionist1, accountant1, hrmanager1 — each with distinct commission/salary setups) come from SEED_MODE=rich, i.e. the seed key this PR's layering logic can set (see below). This is a pre-existing seeding feature; what's new here is that it's now something a team-wide SANDBOX_ALL_SEED variable could set for every sandbox, without every PR author having to type seed: rich themselves.

3. Tracing this sandbox's own settings back through the layers

This sandbox's actual computed settings, read from its own deploy output:

two_regions=false  whatsapp=false  cron=true  queue=true  env_overrides=0

And this PR's own description (the text you'd find in ## What / ## Precedence / ## Details above) has no ## 🧪 Sandbox heading with a fenced block under it — check the PR body and you'll only find a ## Precedence section describing the feature, not a settings block. Feeding that body through the same header-matching awk the workflow uses (.github/workflows/sandbox.yml:118-124) finds nothing, so the PR-inline layer contributed zero keys — consistent with env_overrides=0 above. Every setting this sandbox actually runs with (cron=true, queue=true, the rest defaulted off) came from lower layers: the SANDBOX_ALL_* / SANDBOX_<branch>_* GitHub Environment variables, or the built-in compose defaults if none were set. That's the exact scenario this PR was built for — a sandbox that inherits the team's baseline without its author writing a settings block at all.

4. What the compose step actually does, in order

The deploy job's first step, "Compose sandbox settings (env vars → secrets → PR inline)" (.github/workflows/sandbox.yml:98-188), runs through four stages every time:

  1. Extract — pull the PR body's ## 🧪 Sandbox fenced block, if any,

and convert it from YAML to JSON with yq (lines 116–129). Empty PR body or no matching heading → {}.

  1. Flatten the GitHub Environment — read toJSON(vars) and

toJSON(secrets) (both include the sandbox environment's values, because the job now declares environment: sandbox), and pull out everything prefixed SANDBOX_ALL_*, then everything prefixed SANDBOX_<SANITIZED_BRANCH>_*, each time letting a later source overwrite a key an earlier one already set (lines 146–157).

  1. Split into toggles vs. envWHATSAPP, TWO_REGIONS, CRON,

QUEUE become boolean profile toggles, SEED becomes the seed mode string, everything else becomes a SOME_KEY=value env override (lines 158–162).

  1. Apply the PR block last — any toggle/seed key the PR body set

overrides the GitHub-side value; any env key does too, resolving secret:<NAME> references against the same secrets JSON and rejecting github_token or an unknown name outright (lines 163–176).

The full precedence and the security guardrails around step 4 are covered in The precedence chain and Secrets and safety guardrails.