Development Environment¶
This page explains how local development works across orbital and orbital-manager-backend, how to get from a fresh clone to a working stack, and the team's preferred flow for making a code change.
For architecture background, see Orbital Platform and Orbital Manager Backend. For coding standards, see Best Practices and Code Reviews.
How the repos fit together¶
orbital is the main .NET repo. In normal local development, Orbital.AppHost is the entry point and starts:
Orbital.ApiServiceOrbital.KDSOrbital.Web(React + Vite)
orbital-manager-backend is a separate Python repo with FastAPI microservices:
kitchen_batch_tool_serviceonhttp://localhost:8000order_management_serviceonhttp://localhost:8001kitchen_prep_tool_serviceonhttp://localhost:8002recipe_serviceonhttp://localhost:8003busy_level_management_serviceonhttp://localhost:8004
For local development, the Python repo also provides an nginx reverse proxy on http://localhost:8080. That proxy is important because both the .NET apps and the React frontend expect to talk to the manager backend through a single base URL.
Current local proxy routes are:
/batch-service->localhost:8000/order-service->localhost:8001/prep-service->localhost:8002/recipe-service->localhost:8003/busy-level-service->localhost:8004
That is why the local defaults line up this way:
orbitaldevelopment config setsOrbitalManagerBackendUrltohttp://localhost:8080Orbital/Orbital.Web/.env.examplesetsVITE_MANAGEMENT_SERVICE_BASE_URL=http://localhost:8080Orbital/Orbital.Web/.env.examplealso setsVITE_KITCHEN_PREP_TOOL_BASE_URL=http://localhost:8002, so kitchen prep work can talk directly to the prep service when needed
Two implementation details are worth knowing up front:
Orbital.Webproxies/apito the local .NET API service. When you run throughOrbital.AppHost, Aspire injects theservices__apiservice__*environment variables that Vite uses for that proxy. If you runpnpm devby itself, pages that call/apimay not work unless you provide that target yourself.orbital-manager-backendprefers environment variables first, then falls back to Azure Key Vault viashared/utils/secret_manager.py. In practice, that means local.envvalues override Key Vault, and missing values are fetched from Azure if your local auth is set up.
Team Default Dev Model¶
The usual team setup is a hybrid model:
- App code runs locally
- Secrets come from local
.envfiles and/or Azure Key Vault - Some backing services may stay remote (MongoDB for reading new orders, Snowflake for testing)
- RabbitMQ and PostgreSQL (and also Redis) can be run locally when a change needs them or when you want a more isolated environment
For day-to-day coding, the fastest loop is usually:
- Run
Orbital.AppHostinorbital - Run only the Python service or services you are changing in
orbital-manager-backend - Keep the local nginx proxy on
:8080
You do not need to boot the full Python repo in Docker for every task.
Prerequisites¶
Before starting from scratch, make sure you have:
- Access to the
orbital,orbital-manager-backend, andorbital-wikirepositories - Access to the Orbital Azure tenant and the relevant Key Vaults
- Access to the shared development data sources you need for your feature, such as MongoDB, PostgreSQL/Supabase, or Snowflake
.NET 8 SDK.NET AspireworkloadMetalamaNode.jspnpmPython 3.11+DockerAzure CLI
If you are new to the team, start with Engineering Onboarding.
From Scratch Setup¶
1. Clone both repos¶
Clone orbital and orbital-manager-backend side by side. The examples below assume you have both checked out locally and can open separate terminals for each repo.
2. Authenticate to Azure¶
Both repos are set up to use Azure-backed secrets in local development.
If you do not have access yet, ask the team lead for the correct Key Vault permissions before continuing.
3. Set up orbital-manager-backend¶
From the repo root:
python -m venv .venv --prompt orbital-backend-manager
source .venv/bin/activate
pip install -r requirements.txt
cp .env.dev.example .env
Notes:
- Use
.env.dev.exampleas the normal starting point for local service development. It contains the secret names the code actually looks up, such asPOSTGRES-SUPABASE-URLandRABBITMQ-URL. .env.exampleis the minimal file used by the Docker Compose setup when the containers will fetch most secrets from Azure Key Vault.- Because many keys use hyphenated names, it is easier to keep them in
.envthan to try toexportthem manually in your shell.
If you want local infrastructure for RabbitMQ and PostgreSQL instead of shared remote services, start it now:
That starts:
- RabbitMQ on
5672with management UI on15672 - PostgreSQL on
5432 - pgAdmin on
5050(uncomment indocker-compose.local.yml) - Redis on
6379(uncomment indocker-compose.local.yml)
And make sure the non-UI containers are healthy
Run database migrations:
Start the local nginx proxy:
Useful checks:
http://localhost:8080/nginx-healthhttp://localhost:15672for RabbitMQ UIhttp://localhost:5050for PostgreSQL UI
4. Set up orbital¶
From the repo root:
dotnet workload restore
dotnet dev-certs https --trust
cp .env.example .env
cd Orbital/Orbital.Web
pnpm install
cp .env.example .env
cd ../..
Notes:
- The root
.envis loaded automatically by the local .NET services, so use it for local overrides instead of editing committedappsettings*.jsonfiles. - Keep the frontend on port
3000. The current Google login flow is configured around that port.
Recommended Run Modes¶
Fastest Normal Cross-Repo Loop¶
Use this for most product work that touches the React app or a single Python service.
Start nginx once:
Run only the Python service you are working on:
cd orbital-manager-backend
source .venv/bin/activate
cd kitchen_batch_tool_service
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
Equivalent commands for the other services:
cd order_management_service && uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload
cd kitchen_prep_tool_service && uvicorn app.main:app --host 0.0.0.0 --port 8002 --reload
cd recipe_service && uvicorn app.main:app --host 0.0.0.0 --port 8003 --reload
cd busy_level_management_service && uvicorn app.main:app --host 0.0.0.0 --port 8004 --reload
Then start the .NET side:
What you should expect:
- The Aspire dashboard opens and shows the local .NET processes
- The React app is available on
http://localhost:3000 - Manager backend calls go through
http://localhost:8080
Frontend-Only Work¶
If you are changing Orbital.Web, you will usually still want Orbital.AppHost running because it wires the Vite /api proxy to Orbital.ApiService.
Use pnpm dev by itself only when:
- you are doing isolated UI work
- the page does not depend on
/api - or you are manually providing the missing proxy target environment variables
KDS Work¶
KDS changes are a little more coupled than normal React work.
For KDS work, run:
Orbital.AppHostinorbital- The local backend nginx proxy on
:8080 - At minimum,
order_management_serviceon:8001
That matches the current local development configuration used by Orbital.KDS.
Full Containerized Backend¶
Use this only when you specifically want container parity or need to debug service startup as deployed:
This is slower than the normal uvicorn --reload loop, so it is not the preferred default for everyday feature work.
Sanity Checks¶
Health checks for the Python services:
http://localhost:8000/pinghttp://localhost:8001/pinghttp://localhost:8002/pinghttp://localhost:8003/pinghttp://localhost:8004/ping
Useful local URLs:
- React app:
http://localhost:3000 - Manager backend proxy:
http://localhost:8080 - RabbitMQ UI:
http://localhost:15672 - pgAdmin:
http://localhost:5050
Useful Helper Scripts¶
orbital-manager-backend also includes two useful database helper scripts under scripts/:
supabase_schema_diff.py¶
Use this when you want to compare your local Postgres schema with the currently linked Supabase project, which is usually production or a shared remote environment.
- Requires
supabase loginandsupabase linkfirst - Uses
POSTGRES-SUPABASE-URLfor the local database connection - This script automatically dumps the selected local schemas, runs
supabase db diff --linked, and writes the generated diff SQL intosupabase/
Examples:
python scripts/supabase_schema_diff.py
python scripts/supabase_schema_diff.py --schemas otter,prep_tool
supabase_fdw_sync.py¶
Use this when you want to seed your local Postgres database with a realistic subset of data from Supabase. The script reads table-level rules from scripts/supabase_fdw_sync.yaml and syncs only the tables you keep enabled there.
- Reads from
SUPABASE-PROD-URL - Writes into your local database via
POSTGRES-SUPABASE-URL - Uses
postgres_fdwunder the hood to import selected remote tables, then copies data into local target tables - Supports syncing the full config, a single table subset, or just bootstrapping the FDW setup
Examples:
python scripts/supabase_fdw_sync.py
python scripts/supabase_fdw_sync.py --table otter.orders
python scripts/supabase_fdw_sync.py --bootstrap-only
Desired Flow For Making a Code Change¶
1. Pick the owning repo first¶
Use the repo that owns the behavior:
orbitalfor React UI, .NET APIs, and KDSorbital-manager-backendfor Python manager APIs and workers- both repos only when the feature crosses the boundary between UI/.NET and the Python services
2. Run the smallest slice that can prove the change¶
Default to the narrowest loop:
- one Python service instead of all five
uvicorn --reloadinstead of full Docker ComposeOrbital.AppHostinstead of starting .NET pieces manually
If the feature only touches one screen and one backend service, that should usually be the only local stack you boot.
3. Change code in the layer that owns the behavior¶
Some common patterns:
- React-only UI behavior belongs in
Orbital/Orbital.Web - .NET domain logic belongs in
Orbital.Logicor the owning .NET service, not in the React layer - Python HTTP behavior belongs in the owning FastAPI service, following the router -> service -> data layering described in Best Practices
Avoid pushing logic into a neighboring repo just because it is already running locally.
4. Run repo-appropriate checks before opening a PR¶
For orbital-manager-backend:
If the change is scoped, it is fine to run the most relevant service tests first and then broaden out as needed.
Then do a manual smoke test in the running app for the flow you changed.
5. Validate the end-to-end path, not just the edited file¶
A good local sign-off usually means:
- the edited service or UI starts cleanly
- the relevant
/pingor app route works - the UI points at the intended local service, not production
- the changed flow works through the same entry point a teammate or operator would use
6. Open a normal team PR¶
Before asking for review:
- keep the diff scoped
- link the Linear ticket if there is one
- mention any setup or migration steps in the PR description
- get approval before merge
Common Failure Modes¶
If the frontend is calling the wrong backend:
- confirm
Orbital/Orbital.Web/.envstill points tohttp://localhost:8080 - confirm nginx is running on
:8080 - confirm the target Python service is actually running on its expected port
If a Python service starts but cannot find secrets:
- make sure the value exists in
.env - or make sure
AZURE_KEY_VAULT_URLis set and your Azure login is valid - remember that the service looks up the exact secret names from
.env.dev.example
If the React app loads but /api calls fail:
- run the app through
Orbital.AppHost - or manually provide the Vite proxy target that AppHost normally injects
If Google login behaves strangely in local development:
- keep the frontend on port
3000 - try another browser if the popup flow does not complete