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.
Two layers of .env
Section titled “Two layers of .env”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 bootstrapdoes 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
- .env docker-compose reads this for
- 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 indocker-compose.ymlusing compose-network DNS names, so the root.envonly 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.
Set it up
Section titled “Set it up”-
Fastest path — let bootstrap do it.
make bootstrapcopies the root.env.exampleto.envand generates a strong randomINTERNAL_RPC_SECRETin 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 -
Running one service from source? Copy that repo’s template:
in any backend repo cp .env.example .envFor the SaaS frontend it’s a different filename:
in soundverse-saas-2.0 cp .env.local.example .env.local -
Fill the required gaps.
GH_TOKENis 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 withgh 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.
The shared internal-auth secret
Section titled “The shared internal-auth secret”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.
Turn TLS off locally
Section titled “Turn TLS off locally”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 canonical fleet-wide names
Section titled “The canonical fleet-wide names”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. |
How deployed differs
Section titled “How deployed differs”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→ envINTERNAL_RPC_SECRETSTAGING_CORE_DATABASE_GRPC→ envCORE_DATABASE_GRPC- service FQDNs are auto-published the same way —
STAGING_CORE_STORAGE_GRPC→CORE_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_SECRETexist 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.
Provider keys (all optional locally)
Section titled “Provider keys (all optional locally)”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.
Related
Section titled “Related”- Quick start with docker-compose —
make bootstrapwrites your first.env - Run the full stack from source — where each service’s
.envis read - What works offline vs. what needs secrets — which provider keys unlock what
- Environment & service-discovery — the
STAGING_/PROD_injection model in depth - Environment variable catalog — every name, grouped by service
- core-database — the service that verifies
INTERNAL_RPC_SECRET