Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.emergence.ai/llms.txt

Use this file to discover all available pages before exploring further.

Local Development

This page covers three local-dev modes — choose based on what you’re iterating on. The Quickstart uses Kind; this page also covers the docker-compose loop (faster) and port-forward to a shared dev cluster (slowest, but real platform).

Pick a mode

ModeUse whenSetup timeIteration time
docker-composeIterating on your service code only; platform services can be mocked or stubbed~2 min<5s (hot reload)
KindYou need a real Kubernetes context (probes, configmaps, ingress)~5 min~30s per redeploy
Port-forward to shared dev clusterYou need real platform services (real identity/authorization services, real Connections)<1 min~30s per redeploy
Most loops are docker-compose. Move to Kind for the integration-test pass. Use the shared dev cluster only when you need cross-service coordination.

Mode 1: docker-compose with hot reload

This is the daily-driver loop. Your service runs on the host with uvicorn --reload; supporting services (Postgres for your DB, MinIO for shared storage, a local identity provider) run in containers.
1

Write the compose file

docker-compose.yaml
services:
  postgres:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: hello_solution
    ports: ["5432:5432"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev"]
      interval: 2s
      retries: 20

  minio:
    image: minio/minio:latest
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: minioadmin
    ports: ["9000:9000", "9001:9001"]
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/ready"]
      interval: 2s
      retries: 20

  # Local identity provider for JWT validation. Uses the platform's standard
  # dev image so JWKS endpoints behave realistically.
  keycloak:
    image: quay.io/keycloak/keycloak:26.0
    command: ["start-dev"]
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    ports: ["8080:8080"]
2

Write a .env.example

.env.example
# Database (matches docker-compose service)
DATABASE_URL=postgresql+asyncpg://dev:dev@localhost:5432/hello_solution

# Object storage (MinIO with path-style addressing)
OBSTORE_ENDPOINT=http://localhost:9000
OBSTORE_ACCESS_KEY=minioadmin
OBSTORE_SECRET_KEY=minioadmin
OBSTORE_BUCKET=hello-solution
OBSTORE_PATH_STYLE=true

# Auth (point at the local identity provider realm you create with the bootstrap below)
KEYCLOAK_ISSUER_URL=http://localhost:8080/realms/dev
KEYCLOAK_AUDIENCE=hello-solution-api

# LLM gateway (set to a real LiteLLM gateway URL in dev/stg/prod;
# for local-only loops you can stub the LLM client)
LLM_GATEWAY_URL=http://localhost:4000
LLM_GATEWAY_API_KEY=local-dev-key
LLM_DEFAULT_MODEL=gpt-4o-mini
Copy to .env (which should be in .gitignore) before running.
3

Boot supporting services and run your app with hot reload

docker compose up -d
docker compose ps  # confirm all healthy

# Pre-create the MinIO bucket
docker run --rm --network host minio/mc \
  sh -c "mc alias set local http://localhost:9000 minioadmin minioadmin && mc mb -p local/hello-solution"

# Run your service with hot reload
set -a; source .env; set +a
uv run uvicorn api.main:app --host 0.0.0.0 --port 8000 --reload
Editing packages/api/src/api/*.py reloads the server inside ~1 second. Hit http://localhost:8000/echo?msg=hi to confirm.
4

Tear down

docker compose down -v   # -v also removes the named volumes

Mode 2: Kind cluster

Use Kind when you need real Kubernetes semantics (probes firing, ConfigMaps mounting, Ingress routing). The Quickstart walks through this end-to-end. Make iteration faster with this rebuild-and-load script:
kind-load.sh
#!/usr/bin/env bash
set -euo pipefail

CLUSTER="${1:-hello-solution}"
TAG="${2:-dev}"

docker build -t hello-solution-api:$TAG .
kind load docker-image hello-solution-api:$TAG --name $CLUSTER

# Trigger a rolling restart (image pull policy IfNotPresent + same tag won't redeploy)
kubectl -n em-hello-solution rollout restart deployment/hello-solution-api
kubectl -n em-hello-solution rollout status deployment/hello-solution-api
Run ./kind-load.sh after each code change. ~30 seconds per cycle.

Mode 3: Port-forward to shared dev cluster

Use this when you need real platform behavior — real identity provider realms, real authorization data, real Connections. Run your code locally and forward platform services from a shared dev cluster.
# Connect to your shared dev cluster (context provisioned out of band by your platform team)
kubectl config use-context <your-dev-cluster-context>

# Port-forward platform services to localhost
kubectl -n em-runtime port-forward svc/em-runtime-governance 8001:8001 &
kubectl -n em-runtime port-forward svc/em-runtime-assets    8002:8002 &
kubectl -n em-runtime port-forward svc/em-runtime-utils     8003:8003 &
kubectl -n em-runtime port-forward svc/keycloak             8080:8080 &

# Run your service against them
set -a; source .env.dev; set +a
uv run uvicorn api.main:app --reload
Useful pitfall to know about: when port-forward drops (any error in the cluster), curl from your service will fail until you restart the forward. Wrap forwards in a tiny supervisor script (while true; do kubectl port-forward ... ; done) for long-running loops.

Mocking platform services

Three things are commonly mocked in unit tests:
  1. JWT verification — generate a signed test token with a fixture private key; load the matching public key as KEYCLOAK_JWKS_OVERRIDE (a path to a JSON file your auth code prefers over the JWKS endpoint when set).
  2. Permission checks — patch the Governance SDK’s permission method to return True (or the value under test).
  3. LiteLLM gateway — point LLM_GATEWAY_URL at a stub HTTP server that returns canned completions; or use litellm’s built-in mock provider (model="mock-openai/...").
See SDKs › Python for SDK extension points and Authenticate Users for the JWT fixture pattern.

Common pitfalls

Containers in Kind cannot use localhost to reach your host machine. Use host.docker.internal (macOS/Windows) or the host’s LAN IP (Linux). Or — better — run dependencies inside Kind with another helm install.
MinIO requires path-style addressing. Set OBSTORE_PATH_STYLE=true (or the equivalent in your S3 client config). Virtual-hosted style works on real S3 but not on MinIO without DNS magic.
uvicorn --reload watches the working directory by default. If your source is in a non-default location (e.g., packages/api/src/api/), pass --reload-dir packages/api/src. For container-based reload, mount the source directory as a volume and run uvicorn inside the container.
Every call to Governance, Assets, or Utils requires X-Project-ID. In local dev, hardcode a fixture project ID (e.g., 00000000-0000-0000-0000-000000000001) until you wire user-driven project switching.

CI smoke test

The Quickstart runs end-to-end in CI weekly via .github/workflows/quickstart-smoke.yml. The workflow provisions a Kind cluster, builds the example image, installs the chart, port-forwards, and asserts both endpoints respond. If it fails, an issue is opened automatically with the label solution-dev-guide — that is your signal to update the guide. This is the strongest possible self-sustainability signal: the quickstart cannot rot silently.

Next steps

Authenticate users

Wire JWT validation into your local app.

Manage secrets

Move secrets out of .env into the platform pipeline.

Use shared storage

Replace MinIO test code with the obstore client.

Troubleshooting

Stuck? Check known causes by error symptom.