Authoritative reference for sirloin configuration. Source of truth is
apps/sirloin/internal/app/config/config.go and the SirloinXxx constants
under apps/sirloin/internal/pkg/env/. Cross-cuts:
deployment-env for staging and Railway,
security-model for secret handling,
auth-model for token issuers.
All vars are namespaced SIRLOIN_*. Defaults shown match config.go. “Source”
indicates where the value typically comes from in production.
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_PORT |
no |
:50051 |
Railway / compose |
gRPC + gRPC-Web listen address |
SIRLOIN_STAGE |
no |
— |
Railway |
development | sandbox | staging | production; gates dev-only RPCs, and decides the stage on its own whenever it is non-blank — RAILWAY_ENVIRONMENT_NAME is only the fallback (apps/sirloin/internal/pkg/env/stage.go:36-44). Checked in the Railway UI on 24 September 2026 for beef-sirloin in staging and in beef-pr-3562: it is set in neither, so in those two the Railway environment name is what decides. production was not checked. Setting it is therefore a live change to stage-gated behaviour, not a formality — see SIRLOIN_PAYMENTS_MIRROR_ENABLED for one thing it would switch on. Only one of the two is ever parsed: whichever StageValue returns. Startup fails when both are blank, and when the one that wins is a value ParseStage does not accept — it takes the four canonical names, the aliases prod, qa, testing, test, dev, local and localhost, and anything containing -pr-, which is how beef-pr-N becomes sandbox; anything else is a config error, and main treats that as fatal |
SIRLOIN_URL |
yes |
— |
Railway |
Public URL of sirloin (used in webhooks, links) |
SIRLOIN_GRPC_HOST |
n/a |
— |
— |
Not read by sirloin itself; consumed by clients (e.g. STRIP_SIRLOIN_GRPC_HOST in apps/strip/internal/pkg/env/variables.go:13) |
SIRLOIN_GRPC_URL |
n/a |
— |
— |
Not read by sirloin itself; consumed by clients pointing at sirloin (see compose env for brisket/round services) |
SIRLOIN_TESTER_IDS |
no |
— |
Railway |
Comma-separated user IDs given test perks |
SIRLOIN_NSFW_PROCESSOR_TESTER_IDS |
no |
- |
Railway |
Comma-separated NSFW processors demo accounts |
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_DATABASE_URL |
yes |
— |
Neon (prod), local Postgres (rump) in compose |
BUN ORM Postgres DSN; missing → startup error |
SIRLOIN_DATABASE_POOLED_URL |
no |
falls back to SIRLOIN_DATABASE_URL |
Neon |
Pooled runtime DSN (split read path); when set, used as DatabaseRuntimeURL (apps/sirloin/internal/app/config/config.go:144, :449) |
SIRLOIN_DATABASE_REPLICA_URL |
no |
empty (every query on the primary) |
Neon read replica, pooled endpoint |
Allowlisted read-only RPCs read this replica; everything else stays on the primary. See Neon read replicas (apps/sirloin/internal/pkg/storage/readreplica.go) |
Local compose default: postgres://sirloin:sirloin@rump:5432/rump?sslmode=disable
(see docker-compose.yml).
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_CLERK_API_KEY |
yes |
— |
secret |
Verify Clerk session JWTs (see auth-model) |
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_BRAIN_HOST |
yes |
brain:3000 (compose) |
Railway |
HTTP host for the generated Brain OpenAPI client |
SIRLOIN_BRAIN_API_KEY |
yes |
— |
secret |
Static token for brain |
SIRLOIN_ROUND_HOST |
yes |
round:8080 (compose) |
Railway |
gRPC dial target for round |
SIRLOIN_EXAMPLE_SEMANTIC_V2_BACKFILL_ENABLED |
no |
false |
Railway |
Runs versioned description and CLS embedding workers without changing search reads |
SIRLOIN_EXAMPLE_SEMANTIC_V2_CUTOVER_READY |
no |
false |
Railway |
Explicit operator confirmation that the documented zero-backlog query returned zero; required before semantic v2 search can start |
SIRLOIN_EXAMPLE_SEMANTIC_V2_SEARCH_ENABLED |
no |
false |
Railway |
Reads the semantic CLS vector column; requires both the backfill and cutover-ready flags |
The legacy sirloin↔flank wiring (FlankStorageService, flank execution polling, and
the FlankMCP route) has been deleted; brain owns all workflow data. Sirloin no longer
reads any flank-related variables — SIRLOIN_FLANK_HOST, SIRLOIN_FLANK_SERVICE_TOKEN,
FLANK_ENCRYPTION_KEY, SIRLOIN_FLANK_MCP_ENABLED, and SIRLOIN_FLANK_MCP_BASE_PATH
can be removed from deployed environments.
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_CHARGEBEE_API_KEY |
yes (prod) |
— |
secret |
Chargebee API key |
SIRLOIN_CHARGEBEE_SITE |
yes (prod) |
— |
Railway |
Chargebee site name (<site>.chargebee.com) |
SIRLOIN_CHARGEBEE_WEBHOOK_USERNAME |
recommended |
— |
Railway |
Basic auth user for webhook intake |
SIRLOIN_CHARGEBEE_WEBHOOK_PASSWORD |
recommended |
— |
secret |
Basic auth pw; if either missing, webhooks rejected |
Primer environment is auto-detected from API key prefix.
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_PRIMER_API_KEY |
optional |
— |
secret |
Empty → Primer checkout disabled |
SIRLOIN_PRIMER_WEBHOOK_SECRET |
recommended |
— |
secret |
HMAC secret; empty → Primer webhooks rejected |
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_DUNNING_SOFT_DECLINE_RETRY_ENABLED |
no |
false |
Railway |
FOXY-950 rollout flag: soft and insufficient-funds declines exempt from the 3-consecutive-declines cap (own ceiling of 6), decline counters scoped to scheduled attempts, and an immediate retry when the primary payment method changes during dunning. Only TRUE (case-insensitive) enables it. |
SIRLOIN_DUNNING_CHARGEBEE_PAGE_SIZE |
no |
100 |
Railway |
Page size of the renewal worker’s walk over unpaid Chargebee invoices (FOXY-1381). Valid values are 1–100; anything else means 100. Leave unset in production. To test paging on a preview, set 2 so a few failed renewals span several pages. |
SIRLOIN_APP_BASE_URL |
no |
— |
Railway |
Customer app origin (e.g. https://app.foxy.ai) used to build update_payment_url in Klaviyo dunning events. Empty → the field is omitted. The customer app handles /profile/billing?action=fix-payment and opens the payment dialog there, so set this in prod for recovery emails to carry the link. |
SIRLOIN_PAYMENTS_MIRROR_ENABLED |
no |
true |
Railway |
FOXY-1312: schedules sync_primer_payments, which keeps billing.primer_payments complete from Primer’s payment list for the Strip payments page, about 1,500 list calls a day, nearly all of them re-reading the last day. The renewal worker’s unpaid walk also reads the table to keep invoices with a pending or settled renewal payment in the retry list (FOXY-1381); with the sync off, a charge that wrote no dunning attempt waits for its invoice’s next slot. It ships enabled: the task runs unless the value is FALSE (case-insensitive), and an unset variable means on. The flag is necessary but not sufficient: the job is only registered outside the development and sandbox stages (apps/sirloin/internal/app/worker/river.go:95-97), and which stage a process is in is decided by SIRLOIN_STAGE first. apps/sirloin/internal/app/config/config.go:178 resolves it through env.StageValue, which returns SIRLOIN_STAGE whenever it is non-blank and falls back to RAILWAY_ENVIRONMENT_NAME only when it is blank (apps/sirloin/internal/pkg/env/stage.go:36-44). ParseStage does map any name containing -pr- to sandbox (apps/sirloin/internal/pkg/env/stage.go:47-51), but it only ever sees the Railway environment name when SIRLOIN_STAGE is unset, so a beef-pr-N name is not on its own a guarantee of silence: a preview that inherits SIRLOIN_STAGE=staging from the environment it was cloned from resolves to staging, schedules the sync and issues the Primer list traffic, and this flag is then the only thing that stops it there (apps/sirloin/internal/pkg/env/stage_test.go:45-50 pins exactly that combination). As configured on 24 September 2026 that inheritance does not happen: SIRLOIN_STAGE is set neither on staging nor on beef-pr-3562, the preview checked, so a preview falls back to its own name and resolves to sandbox and does not run the sync. Nothing enforces that, so it holds only for as long as staging leaves the variable unset. That is a property of the Railway configuration, not of the code, so read SIRLOIN_STAGE in the environment rather than reading its name before assuming a preview is quiet. Local development sets SIRLOIN_STAGE=development, so it never runs the sync. Do not use the page itself to confirm the switch took effect: Not synced is driven only by billing.primer_payments holding no rows at all, which needs every writer silent, not just the sync — and nothing ever deletes from that table. Turning the flag off in staging or production leaves a populated decline block that is silently no longer complete. The whole condition is four things at once (apps/sirloin/internal/app/worker/river.go:95-97): a Primer handler, which main builds only when SIRLOIN_PRIMER_API_KEY is set; a repository; a stage that is neither development nor sandbox; and this flag not set to FALSE. Staging and production satisfy the stage half, so staging is where the page is verified before release — provided the Primer key is set there too. Apart from that stage gate the flag gates this task only. The settled and failed payment pollers keep writing the payments they were already fetching, so billing.primer_payments does not go quiet: it stops being complete. The sync is the only writer that guarantees a row per payment, so with FALSE the decline block keeps gaining checkout payments while renewals and anything the pollers skip never arrive — a denominator that moves but cannot be trusted. Turn it off to stop the Primer list traffic, not to freeze the page. Read once at startup, so flipping it needs a restart. It walks its own history forward from 1 June on an empty table, bounded per run, so a deploy needs nothing run by hand; the processor and payment method come from the settled and failed payment pollers and from billing.dunning_attempts. cmd/scripts/import-primer-payments-history is optional and only adds those two dimensions for payments older than the deploy. Off, the page still shows renewal collection from billing.invoices and billing.dunning_attempts, and the decline block keeps gaining the checkout payments the pollers write, without ever covering every payment. |
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_S3_ENDPOINT |
yes |
— |
Railway |
S3 endpoint (Cloudflare R2 in prod) |
SIRLOIN_S3_ACCESS_KEY |
yes |
— |
secret |
— |
SIRLOIN_S3_SECRET_KEY |
yes |
— |
secret |
— |
SIRLOIN_S3_BUCKET |
yes |
— |
Railway |
Object bucket |
SIRLOIN_S3_HMAC_KEY |
yes |
— |
secret |
Signed-URL HMAC |
SIRLOIN_S3_PUBLIC_URL |
yes |
— |
Railway |
Public CDN base. Its host also identifies our own storage URLs: workflow media inputs stored as URLs on that host are reduced back to their storage key before dispatch (apps/sirloin/internal/app/worker/brainmediainputs.go). A URL from any other host is left untouched, and a path-keyed input still holding one is logged as a warning |
SIRLOIN_IMAGE_CGI_URL |
optional |
— |
Railway |
On-the-fly image transform endpoint. Also builds video thumbnails; unset means video media returns an empty media_thumbnail_url. The zone behind it needs Cloudflare Media Transformations enabled with the SIRLOIN_S3_PUBLIC_URL host in its allowed source origins, or frame URLs fail even when this is set |
All optional; required only when KYC is enabled.
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_ONDATO_CLIENT_ID |
conditional |
— |
secret |
OAuth client id |
SIRLOIN_ONDATO_CLIENT_SECRET |
conditional |
— |
secret |
OAuth secret |
SIRLOIN_ONDATO_CLIENT_SETUP_ID |
conditional |
— |
Railway |
Setup id |
SIRLOIN_ONDATO_AUTH_URL |
conditional |
— |
Railway |
OAuth token endpoint |
SIRLOIN_ONDATO_KYC_BASE_URL |
conditional |
— |
Railway |
KYC API |
SIRLOIN_ONDATO_IDV_API_BASE_URL |
conditional |
— |
Railway |
IDV API |
SIRLOIN_ONDATO_REDIRECT_BASE_URL |
conditional |
— |
Railway |
User-facing redirect target |
SIRLOIN_ONDATO_SCOPES |
conditional |
— |
Railway |
Space-separated scopes |
SIRLOIN_ONDATO_WEBHOOK_SECRET |
conditional |
— |
secret |
Shared secret for /webhooks/ondato/verification-status |
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_EMAIL_HOST |
yes |
— |
Railway |
SMTP host |
SIRLOIN_EMAIL_PORT |
yes |
— |
Railway |
SMTP port |
SIRLOIN_EMAIL_USER |
yes |
— |
secret |
SMTP user |
SIRLOIN_EMAIL_PASSWORD |
yes |
— |
secret |
SMTP password |
SIRLOIN_EMAIL_SENDER |
yes |
— |
Railway |
From header |
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_POSTHOG_API_KEY |
recommended |
— |
secret |
Server-side PostHog |
SIRLOIN_KLAVIYO_API_KEY |
optional |
— |
secret |
Klaviyo events |
SIRLOIN_CINDER_API_KEY |
optional |
— |
secret |
Cinder fraud signals |
SIRLOIN_GOOGLE_CREDENTIALS |
optional |
— |
secret |
Google services creds |
OPENROUTER_API_KEY |
yes |
— |
secret |
Legacy example-search maintenance and semantic scene descriptions through OpenRouter; separate from Google image-generation credentials |
OpenTelemetry is enabled when SIRLOIN_AXIOM_TOKEN is set. See
observability.
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_AXIOM_TOKEN |
recommended |
— |
secret |
Axiom API token; enables OTLP export |
SIRLOIN_AXIOM_DATASET |
yes if token set |
— |
Railway |
Traces + logs dataset (axiom:events:v1) |
SIRLOIN_AXIOM_METRICS_DATASET |
yes if token set |
— |
Railway |
Metrics dataset (otel:metrics:v1) |
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_SLACK_WEBHOOK_URL_PAYMENTS |
optional |
— |
secret |
Payment notifications |
SIRLOIN_SLACK_WEBHOOK_URL_SUBSCRIPTIONS |
optional |
— |
secret |
Subscription notifications |
SIRLOIN_SLACK_WEBHOOK_URL_NEW_CHARACTERS |
optional |
— |
secret |
Character-creation feed |
SIRLOIN_SLACK_WEBHOOK_URL_CS_REPORTS |
optional |
— |
secret |
CS-team reports |
SIRLOIN_SLACK_WEBHOOK_URL_ITEAM_ALERTS |
optional |
— |
secret |
Internal-team alerts |
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_MONITOR_ENABLED |
no |
false |
Railway |
Synthetic generation probe |
SIRLOIN_MONITOR_GENERATION_INTERVAL |
no |
2m |
Railway |
Probe trigger cadence |
SIRLOIN_MONITOR_CHECK_INTERVAL |
no |
30s |
Railway |
Probe poll cadence |
SIRLOIN_MONITOR_MAX_PENDING_AGE |
no |
10m |
Railway |
Pending-job age threshold |
Background jobs run on River in the dedicated river Postgres schema
(FOXY-1038); the queue itself needs no configuration beyond the database URLs.
| Var |
Required |
Default |
Source |
Purpose |
SIRLOIN_RIVERUI_USER |
no |
— |
Railway |
Basic-auth user for the embedded River UI at /riverui on the webhook server; UI mounts only when both credentials are set |
SIRLOIN_RIVERUI_PASSWORD |
no |
— |
Railway |
Basic-auth password for the River UI |
Disabled by default. Required only when SIRLOIN_FOXY360_ENABLED=true.
| Var |
Default |
Purpose |
SIRLOIN_FOXY360_ENABLED |
false |
Master switch |
SIRLOIN_FOXY360_HTTP_PORT |
:8086 |
MCP HTTP listen |
SIRLOIN_FOXY360_BASE_PATH |
/foxy360 |
Mount path |
SIRLOIN_FOXY360_CLERK_API_KEY |
— |
Clerk verification for MCP |
SIRLOIN_FOXY360_ANTHROPIC_API_KEY |
— |
LLM provider |
SIRLOIN_FOXY360_OPENAI_API_KEY |
— |
LLM provider |
SIRLOIN_FOXY360_GEMINI_API_KEY |
— |
LLM provider |
SIRLOIN_FOXY360_GOOGLE_CREDENTIALS_JSON |
— |
Google services creds |
SIRLOIN_FOXY360_AXIOM_API_TOKEN |
— |
Foxy360-scoped Axiom token |
SIRLOIN_FOXY360_AXIOM_DATASET |
— |
Foxy360 dataset |
SIRLOIN_FOXY360_PRIMER_API_KEY |
— |
Primer connector |
SIRLOIN_FOXY360_META_ADS_ACCESS_TOKEN |
— |
Meta Ads connector |
SIRLOIN_FOXY360_META_AD_ACCOUNT_IDS |
— |
Comma-separated ad accounts |
SIRLOIN_FOXY360_MCP_AXIOM_URL |
— |
MCP connector URL |
SIRLOIN_FOXY360_MCP_AXIOM_AUTH_HEADER |
— |
Full Authorization header value |
SIRLOIN_FOXY360_MCP_CHARGEBEE_DATA_URL |
— |
MCP connector URL |
SIRLOIN_FOXY360_MCP_CHARGEBEE_DATA_BEARER_TOKEN |
— |
Raw token (no Bearer prefix) |
SIRLOIN_FOXY360_MCP_CHARGEBEE_KB_URL |
— |
MCP connector URL |
SIRLOIN_FOXY360_MCP_INTERCOM_URL |
— |
MCP connector URL |
SIRLOIN_FOXY360_MCP_INTERCOM_BEARER_TOKEN |
— |
— |
SIRLOIN_FOXY360_MCP_POSTHOG_URL |
— |
MCP connector URL |
SIRLOIN_FOXY360_MCP_POSTHOG_AUTH_HEADER |
— |
Full Authorization header value |
SIRLOIN_FOXY360_MCP_DB_DSN |
— |
Read-only DB DSN |
SIRLOIN_DATABASE_RUNTIME_URL does not exist as a sirloin env var. The
runtime-only DSN comes from optional SIRLOIN_DATABASE_POOLED_URL; when
unset, sirloin reuses SIRLOIN_DATABASE_URL
(apps/sirloin/internal/app/config/config.go:144, :449,
apps/sirloin/internal/pkg/env/variables.go:10).
- Ondato required-when-enabled vars (those that block
ondato.NewClient):
SIRLOIN_ONDATO_CLIENT_ID, SIRLOIN_ONDATO_CLIENT_SECRET,
SIRLOIN_ONDATO_CLIENT_SETUP_ID, SIRLOIN_ONDATO_AUTH_URL,
SIRLOIN_ONDATO_IDV_API_BASE_URL, SIRLOIN_ONDATO_KYC_BASE_URL. The rest
(SCOPES, REDIRECT_BASE_URL, WEBHOOK_SECRET) are optional but the
webhook secret is required to accept Ondato webhooks
(apps/sirloin/internal/pkg/ondato/client.go:43-:62,
apps/sirloin/internal/app/config/config.go:289-:302).
SIRLOIN_GRPC_HOST and SIRLOIN_GRPC_URL are not read by sirloin —
see Core table notes. They are client-side conventions used by other
services to dial sirloin: SIRLOIN_GRPC_URL by flank, SIRLOIN_GRPC_HOST
by the k6 load tests (load-tests/.env.example). Brain does not use
either; it dials CharacterRuntimeControlService through
BRAIN_SIRLOIN_CHARACTER_CONTROL_HOST, the mirror of sirloin’s own
SIRLOIN_BRAIN_CHARACTER_RUNTIME_HOST
(apps/brain/src/modules/application/sirloin/providers/sirloin-grpc-client.provider.ts:34-:42).
- None outstanding for env in this pass.