If you only want to use VoicEra, run the whole stack in Docker instead — see Install and run. This page is for changing the code.
What you need
Clone the repository and copy the environment template:
SECRET_KEY and again into INTERNAL_API_KEY, and the Fernet key into PROVIDER_AUTH_ENCRYPTION_KEY. See Environment variables for what each one does.
There is exactly one
.env, at the repository root, plus a separate model-server/.env for the optional model stack. There are no per-app env files. apps/api/app/config.py resolves the root .env four directories up from itself, so running uvicorn from inside apps/api still picks it up.Repository layout in brief
The PYTHONPATH and the apps namespace
apps/ is a Python namespace package. apps/runtime imports apps.providers and apps.telephony directly:
PYTHONPATH. Docker arranges this two different ways, which is why the two services look different when you run them by hand:
What resolves what, with the repository root on the path:
Running from source, export the repository root once per shell:
There is no
pip install -e . — pyproject.toml is an empty placeholder, so the project is not pip-installable. Install dependencies with an explicit pip install -r apps/<app>/requirements.txt, and set PYTHONPATH yourself.The Makefile does cover the Docker stack — make application-up, make application-down, make application-logs, and the model-server-* equivalents; run make help for the list. It has no targets for running services from source, which is what the rest of this page describes.Database-only Compose
Start just the storage layer and leave the application processes to your shell:27018 on the host and listens on 27017 inside the container, so your .env must point at the host port:
.env.example already ships those values — it is docker-compose.yaml that overrides them to mongodb:27017 for the containers.
If you are working on campaigns, the knowledge base, or call artifacts, add the services those need:
minio-init creates the bucket named by MINIO_BUCKET and then exits.
Running the API
apps/api/Dockerfile’s CMD and the compose command: on the api service, which adds --reload.
Confirm it came up:
http://localhost:8000/docs is the always-current route list.
The API imports
apps.providers and apps.telephony too, and finds them through the apps/ directory in the repository root when you run from apps/api with the root on PYTHONPATH. If you see ModuleNotFoundError: No module named 'apps', that export is missing.Running the runtime
The runtime is a separate virtualenv — its requirements pull in Pipecat and its vendor extras, which the API does not need.apps/runtime — the module path is apps.runtime.app. The uvicorn line matches apps/runtime/Dockerfile’s CMD; the compose runtime service declares no command: and so inherits it.
apps/runtime/app.py also has a main() entry point, reachable as a module:
RUNTIME_HOST (default 0.0.0.0) and RUNTIME_PORT (default 7860) and starts uvicorn with reload=False. Use the explicit uvicorn command while developing; use python -m apps.runtime.app when you want the environment variables to pick the bind address.
Confirm it came up:
pipecat-ai[deepgram,cartesia,openai,silero,websocket]==1.8.1 is a large install and pulls model weights for Silero VAD on first use. Expect the first pip install and the first call to be slow.Running the worker and orchestrator
Both run theapps/api package, so reuse the API virtualenv and run them from apps/api.
The ARQ worker executes campaign batches and CSV source syncs off the request path:
arq-worker and campaign-orchestrator command: lines in docker-compose.yaml. Neither service listens on a port.
ENABLE_CAMPAIGN_ORCHESTRATOR (default True) lets API startup spawn the orchestrator in-process. Docker runs it as a separate container instead. If you are running the API from source and also start the orchestrator by hand, set ENABLE_CAMPAIGN_ORCHESTRATOR=False so you do not run two.Hot reload
uvicorn --reload watches the working directory. Because the API and the runtime resolve apps/ differently, reload covers different trees:
Editing a provider or telephony vendor while the API is running therefore needs a manual restart. The same is true in Docker:
./apps:/app/apps:ro is mounted read-only into the API container, but uvicorn’s reloader is rooted at /app, whose contents come from ./apps/api.
Linting
The only lint configuration in the repository ismodel-server/ruff.toml, and it applies to the model-server/ tree:
line-length = 100, target-version = "py312", and selects E, F, W, I, B, UP, SIM, C4, RET, ARG. Vendored model folders are excluded path by path — the file explains why, and the exclusions are deliberately not blanket stt/** / tts/** globs, so files you add inside a vendored folder stay linted.
There is no ruff, black, or isort configuration covering apps/. Match the surrounding style: from __future__ import annotations at the top of every module, PEP 8, and type hints on public functions.
There is no CI. Nothing runs lint or tests on a push. Run the test suites and
ruff check . yourself before opening a pull request.