Skip to content

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.ApiService
  • Orbital.KDS
  • Orbital.Web (React + Vite)

orbital-manager-backend is a separate Python repo with FastAPI microservices:

  • kitchen_batch_tool_service on http://localhost:8000
  • order_management_service on http://localhost:8001
  • kitchen_prep_tool_service on http://localhost:8002
  • recipe_service on http://localhost:8003
  • busy_level_management_service on http://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:

  • orbital development config sets OrbitalManagerBackendUrl to http://localhost:8080
  • Orbital/Orbital.Web/.env.example sets VITE_MANAGEMENT_SERVICE_BASE_URL=http://localhost:8080
  • Orbital/Orbital.Web/.env.example also sets VITE_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.Web proxies /api to the local .NET API service. When you run through Orbital.AppHost, Aspire injects the services__apiservice__* environment variables that Vite uses for that proxy. If you run pnpm dev by itself, pages that call /api may not work unless you provide that target yourself.
  • orbital-manager-backend prefers environment variables first, then falls back to Azure Key Vault via shared/utils/secret_manager.py. In practice, that means local .env values 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 .env files 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:

  1. Run Orbital.AppHost in orbital
  2. Run only the Python service or services you are changing in orbital-manager-backend
  3. 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, and orbital-wiki repositories
  • 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 Aspire workload
  • Metalama
  • Node.js
  • pnpm
  • Python 3.11+
  • Docker
  • Azure 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.

az logout
az login --tenant "<Tenant/Directory ID>" --scope "https://vault.azure.net/.default"

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.example as the normal starting point for local service development. It contains the secret names the code actually looks up, such as POSTGRES-SUPABASE-URL and RABBITMQ-URL.
  • .env.example is 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 .env than to try to export them manually in your shell.

If you want local infrastructure for RabbitMQ and PostgreSQL instead of shared remote services, start it now:

docker compose -f docker-compose.local.yml up -d

That starts:

  • RabbitMQ on 5672 with management UI on 15672
  • PostgreSQL on 5432
  • pgAdmin on 5050 (uncomment in docker-compose.local.yml)
  • Redis on 6379 (uncomment in docker-compose.local.yml)

And make sure the non-UI containers are healthy

Run database migrations:

alembic -c shared/migrations/alembic.ini upgrade heads

Start the local nginx proxy:

docker compose -f docker-compose.nginx.yml up -d

Useful checks:

  • http://localhost:8080/nginx-health
  • http://localhost:15672 for RabbitMQ UI
  • http://localhost:5050 for 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 .env is loaded automatically by the local .NET services, so use it for local overrides instead of editing committed appsettings*.json files.
  • Keep the frontend on port 3000. The current Google login flow is configured around that port.

Fastest Normal Cross-Repo Loop

Use this for most product work that touches the React app or a single Python service.

Start nginx once:

cd orbital-manager-backend
docker compose -f docker-compose.nginx.yml up -d

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:

cd orbital
dotnet watch run --project Orbital/Orbital.AppHost/Orbital.AppHost.csproj

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:

  1. Orbital.AppHost in orbital
  2. The local backend nginx proxy on :8080
  3. At minimum, order_management_service on :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:

cd orbital-manager-backend
docker compose -f docker-compose.yml up --build -d

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/ping
  • http://localhost:8001/ping
  • http://localhost:8002/ping
  • http://localhost:8003/ping
  • http://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 login and supabase link first
  • Uses POSTGRES-SUPABASE-URL for the local database connection
  • This script automatically dumps the selected local schemas, runs supabase db diff --linked, and writes the generated diff SQL into supabase/

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_fdw under 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:

  • orbital for React UI, .NET APIs, and KDS
  • orbital-manager-backend for 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 --reload instead of full Docker Compose
  • Orbital.AppHost instead 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.Logic or 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:

ruff check .
ruff format --check .
pytest

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 /ping or 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/.env still points to http://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_URL is 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