Mac Studio Local Dev Environment Setup
Status: Draft — tracked in TRA-376. Written before the setup was actually performed, so treat each step as the plan rather than a confirmed-working record until you've run through it once and corrected anything that didn't match reality.
Purpose: run the RoundTrip API and web app fully locally on the Mac Studio, so pure application-logic bugs can be caught in seconds instead of a full Azure pipeline round-trip. This deliberately does not replace dev — see the "What this won't catch" section before treating a clean local run as proof something's production-ready.
1. OrbStack + SQL Server
You already run SQL Server locally via OrbStack on your current machine for this same project — the goal here is the same setup, transferred to the Mac Studio, not something new to figure out from scratch.
- Install OrbStack on the Mac Studio (same as your existing machine).
- Bring up the same SQL Server container config you already use — check your existing OrbStack setup for the exact image tag, port mapping, and
SA_PASSWORD/MSSQL_SA_PASSWORDyou're currently running, and replicate it exactly rather than starting from a fresh default. Consistency here avoids a subtle "works on the old machine, not the new one" gap. - Confirm you can connect via DataGrip (or
sqlcmd) before moving on — cheap to verify now, expensive to debug later if the API can't reach it.
2. .NET SDK
Install .NET SDK 10.0.302 — the version confirmed working across this project as of the security-patch investigation a few weeks back. Don't assume "latest" is safe; confirm the version explicitly:
dotnet --version
3. Clone and configure the API
git clone https://dev.azure.com/traxs/RoundTrip/_git/RoundTripAPI
Set the connection string environment variable the way AppDbContextFactory already expects it (same pattern used for design-time migration commands all session):
export ROUNDTRIP_CONNECTION_STRING="Server=localhost,{port};Database=RoundTripLocal;User Id=sa;Password={your local SA password};TrustServerCertificate=True;"
Add this to your shell profile (~/.zshrc) rather than typing it fresh every session.
4. Open decision: secrets — Key Vault or local
Not decided yet — pick one before going further, since it changes how several services get configured.
- Real Key Vault: the API pulls Stripe, SendGrid, Graph API, and Google Maps credentials from Azure Key Vault, same as dev/production. Requires being authenticated to Azure locally (
az login) and having read access to the Key Vault. Pro: identical config path to every other environment, nothing to keep in sync manually. Con: local runs now have an Azure network dependency — no working offline. - Local secrets: use
dotnet user-secretsor a local.envpopulated with dev-tier (not production) credentials for each service. Pro: fully offline-capable once set up. Con: another place secrets can drift out of date, and initial setup means pulling each value out of Key Vault once by hand anyway.
5. Entra External ID redirect URI
MSAL requires the exact redirect URI to be pre-registered — this isn't optional or something that "just works" the way BASE_URL does. In the Entra admin center, on the roundtripapp app registration, add:
http://localhost:{your local frontend port}
alongside the existing https://roundtrips.app / https://dev.roundtrips.app entries. Without this, login will fail with a redirect URI mismatch the moment MSAL tries to complete the flow — worth doing before you get to actually testing login, not after hitting the error.
6. Frontend configuration
Point the local frontend at your local API instead of any deployed one:
VITE_API_BASE_URL=https://localhost:{your local API port}
7. Hangfire — confirm isolation
Once the API runs locally against the local database, TrialLifecycleSweepJob and MarkOverdueInvoicesJob will register and run on their normal schedules against whatever database ROUNDTRIP_CONNECTION_STRING points to. As long as that's genuinely the local OrbStack instance and not accidentally still pointed at dev, this is self-contained and safe — but worth an explicit one-time check the first time you bring this up, given how much of this session was spent on "what's actually connected to what" surprises.
8. Blob storage — open decision, same shape as the secrets question
Either point ConnectionStrings:BlobStorage at a real Azure Storage account (dev's, most likely — same tenant-scoped blob key convention as everywhere else), or accept that NullBlobStorageService will handle calls locally, meaning file/photo uploads won't work in this environment until a real connection string is added. Fine to defer this decision if local testing won't touch the photo-upload or invoice-PDF paths right away.
9. Confirm it actually works
Once everything above is in place:
- API starts cleanly, connects to the local database
- Frontend loads, points at the local API
- Login completes against Entra (confirms the redirect URI step worked)
- A basic read (e.g. loading the ticket list) succeeds
That's the baseline "this environment is real" checkpoint — worth confirming explicitly rather than assuming everything's wired correctly just because nothing threw an error yet.
What this setup won't catch — keep testing on dev too
A real share of the hardest bugs from the weeks of work that led to this ticket were specifically about how Azure hosts the app, not the application code itself:
- Stale
BASE_URLafter a domain migration - A zombie Hangfire server left behind after a deploy
- A missing blob storage connection string on one environment but not another
- Kudu deployment-timestamp mysteries (confirming what's actually deployed vs. what the local files show)
None of these can reproduce on a Mac Studio running Kestrel directly — there's no Azure edge/proxy layer, no deployment slots, no Kudu. Treat this environment as a fast first pass to catch logic bugs before they ever reach a pipeline, not a substitute for verifying real behavior on dev before anything reaches production.