A debug-only screen reachable from the bottom of the More tab that lets you switch the active backend environment and sign in as one of several pre-configured test users without typing credentials. Lives in the client/developer-settings module pair and is hidden from release builds via BuildConfig.IS_DEBUG.
| Setting | Effect |
|---|---|
| Environment | Switches between PROD, TESTING, and FAKE. Persisted via multiplatform-settings. Selecting a new env signs you out, wipes the local DB (recipes, grocery, meal plans), seeds Recipe.Samples if FAKE is chosen, and prompts for an app restart so the Supabase client rebinds. |
| Login as test user | Lists pre-baked users sourced from Gradle env vars / properties and signs in via the existing Supabase auth repo. Persists which user is selected so the screen can show "currently signed in as User N" across restarts. If the credentials are wrong, an error dialog surfaces the Supabase error message. |
| Sign out | Visible only when authenticated. Signs the test user out and wipes the local DB so the next user starts from a clean cache. |
The Supabase URL/key for TESTING is read at build time. PROD already has its values (see buildconfig-setup.md for the existing supabase.url / supabase.key). FAKE re-uses PROD.
supabase.testing.url=https://your-staging-project.supabase.co
supabase.testing.key=your-staging-anon-public-keyexport SUPABASE_TESTING_URL=https://your-staging-project.supabase.co
export SUPABASE_TESTING_KEY=your-staging-anon-public-keyIf supabase.testing.* is not set, the build falls back to the PROD values, so TESTING will be a no-op until you provide them.
Users are detected by incrementing n (starting at 1) until a pair (email + password) is missing. Provide as many as you like.
Heads-up on key naming:
local.propertiesandgradle.propertiesuse dotted lower-case keys (chefmate.user.1,chefmate.user.password.1). TheCHEF_MATE_USER_1/CHEF_MATE_USER_PASSWORD_1form is for shell environment variables only — putting that form inlocal.propertieswill silently do nothing because Java'sPropertiesloader doesn't translate the underscored uppercase form into a dotted key.
Lookup order per index, first match wins:
local.properties(project root) — key formchefmate.user.<n>/chefmate.user.password.<n>- Gradle properties (
~/.gradle/gradle.properties,-Pflags, etc.) — same dotted key form - Environment variables — uppercase form
CHEF_MATE_USER_<n>/CHEF_MATE_USER_PASSWORD_<n>
chefmate.user.1=alice@chefmate.test
chefmate.user.password.1=hunter2
chefmate.user.2=bob@chefmate.test
chefmate.user.password.2=hunter3After editing local.properties, re-run the JVM/Android target so Gradle regenerates BuildConfig.TEST_USERS:
./gradlew :client:composeApp:run # desktop
./gradlew :client:composeApp:installDebug # Androidchefmate.user.1=alice@chefmate.test
chefmate.user.password.1=hunter2export CHEF_MATE_USER_1=alice@chefmate.test
export CHEF_MATE_USER_PASSWORD_1=hunter2
export CHEF_MATE_USER_2=bob@chefmate.test
export CHEF_MATE_USER_PASSWORD_2=hunter3The build script in client/shared/build.gradle.kts collects every contiguous user pair and serializes them as a single BuildKonfig string field (TEST_USERS = "email1|password1;email2|password2"). At runtime, TestUserProvider parses the string and exposes a List<TestUser> to the dev-settings ViewModel. A gap (e.g. _1 defined, _2 missing, _3 defined) stops collection at the first miss — define users contiguously.
The "Developer Settings" row only appears when BuildConfig.IS_DEBUG is true. The flag is derived at configuration time from gradle.startParameter.taskNames: any task containing Release flips it to false. This is a UI-gate, not a security boundary — the dev-settings code itself ships in every binary.
| Build path | IS_DEBUG |
|---|---|
./gradlew :client:composeApp:installDebug |
true |
./gradlew :client:composeApp:run (desktop) |
true |
./gradlew :client:composeApp:packagePkg (desktop debug package) |
true |
./gradlew :client:composeApp:bundleRelease |
false |
./gradlew :client:composeApp:packageReleasePkg (desktop release package) |
false |
| IDE sync (no tasks) | true |
Desktop packaging caveat: the plain
packagePkg/packageMsi/packageDebtasks do not contain "Release", so they ship with the developer UI visible. The desktop release workflow uses thepackageRelease*variants (output underbuild/compose/binaries/main-release/) precisely soIS_DEBUGisfalse. See deployment.md.
client/sharedowns theEnvironmentenum and the read-onlyEnvironmentProvidercontract, so production code (e.g.SupabaseModule) doesn't depend on the dev-settings module.client/developer-settings/publicowns theDeveloperSettingsBloc, screen, andDeveloperPreferencesinterface (which extendsEnvironmentProvider).client/developer-settings/implprovidesDeveloperPreferencesImpl(russhwolf-Settings-backed),TestUserProvider(parses the BuildKonfig string),FakeRecipeSeeder(insertsRecipe.Samples), and the BLoC + ViewModel that orchestrate env changes.- The Supabase client is
@SingleIn(AppScope::class)and binds the URL/key at first injection. Switching env requires an app restart — the screen surfaces a dialog telling the user to do so.
- Add at least one
chefmate.user.1/chefmate.user.password.1and (optionally)supabase.testing.*to one of the lookup sources above. ./gradlew :client:composeApp:installDebug— open the More tab, confirm "Developer Settings" appears at the bottom.- Switch Environment → TESTING. Verify: signed out, recipe list empty, restart-required dialog. Force-stop + reopen, confirm sync now hits the testing URL.
- Switch Environment → FAKE. Verify several seeded recipes appear after the wipe.
- Tap "Login as test user" → User 1. Verify signed in; user persists across an app restart.
- Tap "Login as test user" with a deliberately bad password (edit
local.properties, rebuild). Verify an error dialog appears with the Supabase message. - Tap "Sign out". Verify recipes / meal plan / grocery list are wiped locally.
- Build a release APK. Confirm the row is absent.