This replaces docker-compose.yml + up.ps1 + hand-edited secrets/URLs for local development.
The MAUI mobile app still runs the normal way (emulator/device) — Aspire's job for mobile is
config/secret materialization and tunnel wiring, not running the app.
You need three things installed, plus one Keeper record. That's it.
| # | Install | Command | Who needs it |
|---|---|---|---|
| 1 | .NET 10 SDK | from https://dot.net — global.json pins 10.0.301 |
everyone |
| 2 | Aspire CLI ≥ 13.4.6 | dotnet tool install -g aspire (update: dotnet tool update -g aspire) |
everyone |
| 3 | Docker running | Docker Desktop / OrbStack | everyone |
Steps 2 + 3 can also be done from the dashboard later: rewards-sql ▸ Tools: Install/upgrade Aspire CLI and Tools: Diagnose (aspire doctor) check your machine in one click.
Most backend devs don't need these — skip unless you're building the mobile app or doing on-device testing.
- MAUI workloads — only to build/run the mobile app. The app ships for both iOS and
Android, so for full coverage install both:
You can also install just one platform's workload if that's all you build (e.g. you test on an Android device and don't have an iPhone):
dotnet workload install maui # iOS + Android (needs sudo on a system-wide SDK)The MobileUI project multi-targets iOS + Android;dotnet workload install maui-android # Android only → `rewards-dev mobile android` dotnet workload install maui-ios # iOS only → `rewards-dev mobile ios` (Mac + Xcode)
rewards-dev mobile <android|ios>(and the dashboard Build & Run (Android) / Build & Run (iOS) commands) pass-p:MobileTargetFrameworks=net10.0-<platform>so a single-platform build only needs that platform's workload. Or use the dashboard: mobile-app ▸ MAUI workload restore / Update .NET workloads. - Tailscale — only for on-device phone testing against your local API (stable URL + trusted
HTTPS cert). Install from https://tailscale.com/download, then
tailscale upon the Mac and the phone (same tailnet). See Phone dev with Tailscale below. Not needed for emulator + staging.
Keeper is the only external resource you need. Every stack secret (SQL password, Firebase, SendGrid, SMTP, identity authority, mobile Firebase config) flows from one store the dev pastes into: the AppHost user-secrets. No copying files around, no per-project pasting.
rewards-dev secrets edit # opens the AppHost secrets.json in your editor
# → paste the whole "SSW.Rewards — Aspire Dev Secrets" record from Keeper ▸ SSW.Rewards, save
rewards-dev secrets check # ✓/✗ for every required key — tells you exactly what's missingsecrets check lists each required key and, for anything missing/placeholder, where in Keeper to
get it. You can also drive both from the dashboard: mobile-app ▸ Secrets: Open file and
Secrets: Validate. Prefer a raw command? rewards-dev secrets path prints the file location
(macOS/Linux ~/.microsoft/usersecrets/<id>/secrets.json, Windows
%APPDATA%\Microsoft\UserSecrets\<id>\secrets.json) — open it with open/notepad and paste.
The Keeper record body is the literal
secrets.json(flatParameters:*keys). Aspire adds its own per-machineAppHost:*keys on first run — those are not in Keeper.
The mobile app gets its own user-secrets store (MobileUI.csproj has a separate
<UserSecretsId>), so backend credentials can never leak into the mobile project / APK. You still
paste only the one Keeper record into the AppHost store; the mobile store is derived:
rewards-dev secrets sync-mobile # copies ONLY the 2 Firebase client-config keys → mobile store,
# then writes the git-ignored google-services.json + plistThis copies only mobile-google-services-json + mobile-google-service-info-plist — never the
SQL/SendGrid/email/Firebase-service-account secrets. The dashboard button mobile-app ▸ Sync mobile
secrets (isolated) runs the same thing.
rewards-dev secrets check # make sure the Keeper blob is in place (see above)
aspire run # from the repo root — .aspire/settings.json points the CLI at src/AppHostIf a secret is missing, Aspire reports the parameter as ValueMissing and SQL won't start — run
rewards-dev secrets edit, paste, and re-run.
The dashboard opens automatically. SQL Server + Azurite come up as persistent containers
with data volumes (ssw-rewards-sql-data, ssw-rewards-azurite-data), so your data survives
restarts. WebAPI + AdminUI start once SQL is healthy; migrations apply on WebAPI startup.
The Graph tab gives the same model as a topology view — handy to see how
rewards-webapiwires up to SQL, Azurite, the secret parameters and the mobile config files:![]()
Both containers are grouped under an SSW.Rewards project/folder in Docker Desktop & OrbStack (instead of cluttering the top level). Aspire stamps
com.docker.compose.project=SSW.Rewards+com.docker.compose.service=<name>labels viaInDockerProject()inHosts/DockerGrouping.cs.
Secrets now flow only from the AppHost — one store, see Secrets above. WebAPI/AdminUI no longer carry their own
UserSecretsId. Aspire injectsConnectionStrings:DefaultConnection/:HangfireConnection,CloudBlobProviderOptions:ContentStorageConnectionString(→ Azurite),Firebase:FirebaseCredentials,SendGridAPIKey,EmailUser,EmailPassword,SigningAuthorityas env vars.
The dashboard groups the local-dev chores under two resources (Actions ▸ Commands):
Commands that need input (e.g. Switch API target…) open a prompt right in the dashboard. Target pickers are dropdowns (local / staging / prod / tailscale) — no typos, valid by construction:
On rewards-sql (database + tooling):
- DB: Apply migrations / Add migration… — EF update / add (prompts for the name)
- Tools: Install/upgrade dotnet-ef, Trust dev HTTPS cert
- Tools: Install/upgrade Aspire CLI —
dotnet tool update aspire --global(installs if missing) - Tools: Diagnose (aspire doctor) —
aspire doctorhealth check (CLI / SDK / Docker / dev-cert)
On mobile-app (a virtual, lifetime-less resource for the MAUI app — Aspire doesn't run it,
but it gives the mobile chores a home):
- Secrets: Validate / Secrets: Open file —
rewards-dev secrets check/edit: confirm the one AppHost secrets store is complete, or opensecrets.jsonto paste the Keeper record (see Secrets above) - Show current target —
rewards-dev show(prints the API + identity the apps point at) - Switch API target… / API → Tailscale (one-click) / Switch identity target… — shell out
to the
rewards-devCLI (below), which switches all apps, not just mobile - Tailscale: Status —
tailscale status(verify connectivity before using the tailscale target) - Sync mobile secrets (isolated) —
rewards-dev secrets sync-mobile: copies ONLY the two Firebase client-config keys into MobileUI's own isolated user-secrets store, then writesgoogle-services.json+GoogleService-Info.plist(git-ignored; only*.templateis committed). Backend secrets are never copied — nothing sensitive can reach the APK. - Build & Run (Android) —
rewards-dev mobile android: builds the MAUI app for Android and deploys to a running emulator/device. Only themaui-androidworkload is needed. Start an emulator and pick a reachable backend (api staging/api tailscale) first. - Build & Run (iOS) —
rewards-dev mobile ios: builds for iOS and deploys to a running simulator. Needs themaui-iosworkload + a Mac with Xcode. - MAUI workload restore —
dotnet workload restorefor the iOS/Android prereqs - Update .NET workloads —
dotnet workload update(keep MAUI/iOS/Android workloads current)
Stop hand-editing Constants.cs and the AdminUI appsettings. One command switches the
identity authority and/or API URL across all apps, in sync, via git-ignored overrides.
Run it from the repo root with the ./rewards-dev wrapper (or add an alias —
alias rewards-dev="$(git rev-parse --show-toplevel)/rewards-dev" — to call it from anywhere).
| App | Override file (git-ignored) | Falls back to |
|---|---|---|
| Mobile | src/MobileUI/Constants.LocalDev.cs |
committed Constants.LocalDev.Default.cs |
| AdminUI | src/AdminUI/wwwroot/appsettings.Local.json |
committed appsettings(.Development).json |
| WebAPI | AppHost user-secret Parameters:signing-authority |
prompted at aspire run |
# identity → Mobile + AdminUI + WebAPI ; api → Mobile + AdminUI ; env → both
./rewards-dev help # full self-teaching usage (targets, examples, files)
./rewards-dev secrets check # validate the one AppHost secrets store (Keeper blob)
./rewards-dev env staging # everything → staging (safe default)
./rewards-dev api local # https://localhost:5001 (AdminUI + Mobile)
./rewards-dev api tailscale # stable phone URL + auto `tailscale serve` (see below)
./rewards-dev identity local # https://localhost:14330 (all apps)
./rewards-dev show --json # current effective targets (machine-readable)
./rewards-dev reset # remove overrides → committed defaultsapi/identity change only their own dimension and leave the other untouched. The same logic
runs head-less (AI / scripts) and from the Aspire dashboard commands. Rebuild the mobile app and
restart aspire run (AdminUI refresh + WebAPI) to pick up a change.
./rewards-devis a thin wrapper overdotnet run --project tools/RewardsDev. Code lives intools/RewardsDev/:Core/(presets, repo paths, state, process helpers) andApps/(one config writer per app —MobileConfig,AdminUiConfig,WebApiConfig).
Aspire does not run the MAUI app — it runs on an emulator/simulator/device the usual way. The app targets both iOS and Android; build one platform at a time.
- Install the MAUI workload once (above). Building only one platform?
maui-androidormaui-iosis enough for that platform. - Pick a backend:
./rewards-dev api staging(emulators/simulators can't reachlocalhost, so useapi stagingorapi tailscale— notapi local— on a device). Useapi(notenv) so you only repoint the API and leave your local identity/WebAPI untouched. - Ensure Firebase config exists (git-ignored): run
rewards-dev secrets sync-mobile(or the mobile-app ▸ Sync mobile secrets (isolated) dashboard command). It isolates the two Firebase keys into the mobile store and writesgoogle-services.json/GoogleService-Info.plist. - Build + deploy with one command:
Each wraps
./rewards-dev mobile android # Android emulator/device ./rewards-dev mobile ios # iOS simulator (Mac + Xcode)
dotnet build -t:Run -f net10.0-<platform> -p:MobileTargetFrameworks=net10.0-<platform>. The-p:MobileTargetFrameworksoverride matters — the project multi-targets iOS+Android, and overriding the well-knownTargetFrameworksinstead would break referenced libraries. Building a single platform also means you only need that platform's workload (handy when you test on Android and don't want the iOS/Apple toolchain). Raw dotnet form, e.g. to target a specific emulator:(dotnet build src/MobileUI/MobileUI.csproj -t:Run -f net10.0-android -c Debug \ -p:MobileTargetFrameworks=net10.0-android -p:AdbTarget="-s <emulator-id>"-t:Runpushes the Fast-Deployment assemblies; a plainadb installof the Debug APK won't run.)
Boot an emulator first:
~/Library/Android/sdk/emulator/emulator -avd <avd-name> &. In a non-interactive shell (script/CI/agent), usenohup ~/.../emulator -avd <avd-name> &so it survives the shell session, and wait foradb wait-for-device shell getprop sys.boot_completed.
This version of .NET for iOS (26.5.xxxxx) requires Xcode 26.5. The current version of Xcode is 26.6.— version check only: dropping theglobal.jsonworkload pin resolves the iOS workload to SDK 26.5, whose build-time check doesn't yet recognise Xcode 26.6, even though the two SDKs are identical (dotnet/macios#25658).MobileUI.csprojsets<ValidateXcodeVersion>false</ValidateXcodeVersion>for iOS, so this is already handled — remove that property once a workload that formally validates Xcode 26.6 is adopted.actool error: No simulator runtime version ... available to use with iphonesimulator SDK version— the matching iOS simulator runtime isn't installed. Install it withxcodebuild -downloadPlatform iOS(~8.5 GB), then confirm withxcrun simctl runtime list.
Dev tunnels / ngrok give a new URL every session and an untrusted cert on iOS. Tailscale fixes both:
- Install Tailscale on the Mac and the phone; sign both into the same tailnet (
tailscale up). - Point the mobile app at the stable hostname (this also starts the HTTPS proxy for you):
It auto-detects your MagicDNS name (e.g.
./rewards-dev api tailscale
https://<machine>.<tailnet>.ts.net:5001) and runstailscale serve --bg --https 5001 https+insecure://localhost:5001so the phone gets a real Let's Encrypt cert for your*.ts.netname — iOS trusts it, no dev-cert sideloading.First-time setup can be slow (cert issuance), and your tailnet must have HTTPS enabled in the admin console (Settings → Keys / HTTPS Certificates). If auto-serve can't start,
rewards-devstill switches the URL and prints the manual command to run once:tailscale serve --bg --https 5001 https+insecure://localhost:5001. No more per-session URL churn inConstants.cs.
aspire runmust run with the Development environment so user-secrets load (otherwise secret parameters report ValueMissing and SQL never starts). The committedsrc/AppHost/Properties/launchSettings.jsonsets this; if you launch differently, exportDOTNET_ENVIRONMENT=Development.Aspire.AppHost.Sdkis pinned to 13.4.6 — older versions are rejected by the DCP runtime.IInteractionService(the command-time masked prompts) is experimental →ASPIREINTERACTION001is suppressed in the AppHost csproj.docker-compose.yml/up.ps1can be retired for local dev (kept for now if any pure-container CI path still relies on them).- Two clones/worktrees on one machine share the persistent SQL + Azurite data volumes. The
containers use stable names (
ssw-rewards-sql,ssw-rewards-azurite) so every clone reuses the same container instead of spawning a second one on the same volume. Only runaspire runfrom one clone at a time. Symptom if you ever see a second container fighting for the volume: SQL logsSetup FAILED copying system data file … Access is deniedand exits 255 — stop the otherssw-rewards-sql*container (docker stop ssw-rewards-sql) and re-run. - New worktree bootstrap — machine-global state (MAUI workloads, the AppHost + isolated mobile
user-secrets stores) is shared across worktrees, but the git-ignored Firebase files
(
src/MobileUI/Platforms/{Android/google-services.json, iOS/GoogleService-Info.plist}) are materialized into the working tree, so a fresh worktree doesn't have them even if another worktree on the machine does. In every new worktree, run once:That's the whole mobile fix (idempotent; safe to re-run). Prereq: the machine's AppHost store must already hold the Keeper record — a one-time-per-machinerewards-dev secrets sync-mobile # writes the two Firebase files into THIS worktreerewards-dev secrets edit, not per-worktree. Building the fullSSW.Rewards.slncompiles MobileUI, so skipping this makes the solution build fail on Firebase even though the backend projects build and test fine.





