Skip to content

Configuring .env

Configuration is env-vars only — no config files checked into the services. Locally that means a .env per repo; in deployed environments the pipeline injects the same names from GitHub secrets. This page covers the four things that trip people up: where .env lives, the shared internal-auth secret, the local TLS-off flags, and how deployed injection maps back onto the same names.

Config lives at two levels, and they don’t overlap:

  • Directorycore/ the super-repo
    • .env docker-compose reads this for ${VAR} substitution (whole-stack run)
    • .env.example copy it to .env; make bootstrap does this for you
    • Directorycore-tool-sansaarm/
      • .env per-service config (running one service from source)
      • .env.example copy it to .env
    • Directorycore-gateway-consumer/
      • .env.example
    • Directorysoundverse-saas-2.0/
      • .env.local the frontend’s env
      • .env.local.example copy it to .env.local
  • Whole stack via docker-compose reads the super-repo root .env. Compose substitutes ${VAR} into the container definitions; the inter-service wiring (who-dials-whom) is hard-coded in docker-compose.yml using compose-network DNS names, so the root .env only carries secrets and tunables — GH_TOKEN, POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB, AZURE_CONNECTION_STRING, and the provider keys.
  • One service from source reads that repo’s own .env. Each backend repo ships a .env.example; the frontend ships .env.local.example.

Deployed environments use neither file — see How deployed differs.

  1. Fastest path — let bootstrap do it. make bootstrap copies the root .env.example to .env and generates a strong random INTERNAL_RPC_SECRET in place (so the dev stack isn’t on the placeholder). See Quick start with docker-compose.

    Terminal window
    make bootstrap # inits submodules + writes .env with a generated INTERNAL_RPC_SECRET
  2. Running one service from source? Copy that repo’s template:

    in any backend repo
    cp .env.example .env

    For the SaaS frontend it’s a different filename:

    in soundverse-saas-2.0
    cp .env.local.example .env.local
  3. Fill the required gaps. GH_TOKEN is required to build the images — every backend image pulls private deps (soundverse-proto, soundverse-py, soundverse-proto-go) at build time via a BuildKit secret. Print yours with gh auth token. Provider keys (LLM, song-gen, album-art, billing, browser login) are optional locally — the stack boots without them; see What works offline vs. what needs secrets.

Every internal gRPC/Connect call in the fleet is authenticated with Authorization: Bearer where the token comes from INTERNAL_RPC_SECRET. core-database verifies it (generated handlers with require_internal_auth = true); core-mcp, core-identity, core-storage, core-gateway-consumer and every tool worker send it. The value must be identical across all of them or the receiver answers UNAUTHENTICATED: invalid internal authorization.

In staging/prod, core-database and core-storage sit behind a TLS-only Azure Container Apps ingress on :443, so clients dial TLS by default. Locally you run plaintext h2c, so you must flip the per-service TLS toggles to false — the shipped .env.example files already do, so the trap is only if you override them.

The fleet shares one set of names so a value means the same thing everywhere. Read the environment variable catalog for the full list; these are the ones you touch to run locally:

Env var What it points at
INTERNAL_RPC_SECRET Shared internal-gRPC bearer (must match everywhere).
ENVIRONMENT local / staging / prod — tags queued tasks and selects the tool-registry namespace. Keep it identical across core-database, core-mcp, the gateway and the workers.
REDIS_ADDR Redis URL form, e.g. redis://localhost:6379/0. The DB index is the /N path — there is no separate REDIS_DB knob.
REDIS_PASSWORD Redis AUTH password; blank locally.
CORE_DATABASE_GRPC host:port of core-database (the single data door).
CORE_STORAGE_GRPC host:port of core-storage.
CORE_IDENTITY_GRPC host:port of core-identity (the gateway dials this).
CORE_MCP_GRPC host:port of core-mcp; CORE_MCP_URL overrides it with a full URL if set.
CORE_GATEWAY_CONSUMER_GRPC host:port the frontend BFF dials.
CORE_*_USE_TLS Per-service TLS toggle; false locally (see above).
OTEL_EXPORTER_OTLP_ENDPOINT / _HEADERS OTLP collector; leave unset locally → stdout JSON logs only.

You never edit a .env for staging or prod. The deploy pipeline (soundverse-proto’s reusable service-template workflow) takes every GitHub variable/secret named STAGING_<NAME> (or PROD_<NAME>), strips the prefix, and injects it into the container as <NAME>:

  • STAGING_INTERNAL_RPC_SECRET → env INTERNAL_RPC_SECRET
  • STAGING_CORE_DATABASE_GRPC → env CORE_DATABASE_GRPC
  • service FQDNs are auto-published the same way — STAGING_CORE_STORAGE_GRPCCORE_STORAGE_GRPC

So the container sees the same canonical names your local .env uses — the only difference is where the value comes from. Two consequences worth internalising:

  • A service that reads a different name for the same logical thing silently gets nothing and falls back to its hardcoded default. That’s the whole reason the fleet keeps one canonical name per concept.
  • The names live only in GitHub settings (org-level vars/secrets), invisible in the repo. Confirm STAGING_INTERNAL_RPC_SECRET / PROD_INTERNAL_RPC_SECRET exist before you expect a deploy to authenticate.

The full injection-and-discovery model — including the auto-published *_GRPC FQDNs and the staging TLS-only :443 ingress — is in Environment & service-discovery.

The stack boots and self-registers with zero external accounts. These enable specific features, and every one is injected by name in deployed environments — never printed here:

Feature Env-var names (values injected per env)
Agent LLM turns (core-tool-agent) LITELLM_BASE_URL, LITELLM_API_KEY, AGENT_FLASH_MODEL / AGENT_STANDARD_MODEL / AGENT_PRO_MODEL
Song generation (core-tool-sansaarm) SANSAARM_API_BASE_URL, SANSAARM_API_KEY, SANSAARM_MODEL_V5
Album art (core-tool-media) REPLICATE_API_TOKEN
Browser login (soundverse-saas-2.0) AUTH_SECRET, AUTH_LOGTO_ISSUER, AUTH_LOGTO_RESOURCE, AUTH_LOGTO_ID, AUTH_LOGTO_SECRET
Billing checkout (core-billing) HYPERSWITCH_BASE_URL, HYPERSWITCH_API_KEY, HYPERSWITCH_PROFILE_ID, HYPERSWITCH_WEBHOOK_SECRET

Auth is Logto (the AUTH_LOGTO_* names above) — the OIDC token must be a JWT, which requires requesting an API resource. That wiring has its own guide. The current agent tiers run on Anthropic Claude (standard/pro) and Kimi (flash) via litellm prefix routing, so the AGENT_*_MODEL values are litellm model ids, not a single provider — see core-tool-agent.