feat(deploy): wire Stripe credentials through ExternalSecrets, gated off - #876
Merged
Conversation
Adds the three env vars billing needs — STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_ID — sourced from Secret Manager via the existing api-keys ExternalSecret. The remote refs are gated behind `billing.enabled` (default false), following the errorTracking precedent directly above them. That gate is not stylistic: ESO rejects the ENTIRE ExternalSecret when any referenced remote key is missing, so referencing a Stripe secret before it exists would stop api-keys syncing altogether and take JWT, LiteLLM and every OAuth credential down with it. An empty secret container is not enough either — `describe` succeeds while `versions list` is empty, and ESO needs a version. The deployment env vars are deliberately NOT gated. Every ref is `optional: true`, so with the flag off the keys are simply absent and billingService.STRIPE_ENABLED() reports false, which makes /api/billing/* return 503 billing_not_configured rather than half-working. Keeping them unconditional means enabling billing is a one-line values change with no template edit to forget. The price id is not secret but lives in Secret Manager anyway, so swapping test credentials for live ones is `gcloud secrets versions add` rather than a chart edit and redeploy. Rendered both ways: flag off yields zero stripe refs with jwt-secret intact; flag on yields all three. helm lint clean in both states. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8
…ed present
Flipped only after confirming each of commonly-dev-stripe-{secret-key,
webhook-secret,price-id} has an ENABLED version, and that each value carries
the expected prefix with no trailing whitespace. The whitespace check is not
ceremony: a newline captured by `echo` instead of `printf '%s'` survives into
the secret and surfaces later as a webhook signature rejection that looks
nothing like its cause.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8
This was referenced Aug 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wires the three Stripe env vars the merged billing code reads (#875) through the
existing
api-keysExternalSecret, with the remote refs gated behind a newbilling.enabledflag that defaults off.Why the gate exists
api-keys.yamlalready carries this warning above the Sentry block:So adding an unconditional
commonly-dev-stripe-secret-keyref before thatsecret exists would not fail quietly on Stripe alone — it would stop
api-keyssyncing entirely, taking JWT, LiteLLM, and every OAuth credential with it.
Gating means this PR is safe to merge and deploy right now, before any
credential is in place.
A subtlety worth writing down: an empty secret container is not enough.
gcloud secrets describesucceeds on a secret with zero versions while ESOstill fails to resolve it. The check is
gcloud secrets versions list <name>.Why the deployment env vars are not gated
Every
secretKeyRefisoptional: true, so with the flag off the vars aresimply absent — and
billingService.STRIPE_ENABLED()isBoolean(process.env.STRIPE_SECRET_KEY), so checkout, portal, and the webhookall disable together. There is no state where we accept a webhook we cannot
verify. Keeping them unconditional makes enabling billing a one-line values
change with no template edit to forget.
Secret Manager state
Containers are created and IAM is already sufficient —
commonly-secrets-saholds
roles/secretmanager.secretAccessorat the project level, and theexisting secrets carry no per-secret policy, so nothing extra was granted.
commonly-dev-stripe-price-idcommonly-dev-stripe-secret-keycommonly-dev-stripe-webhook-secretThe price id is not secret, but it lives in Secret Manager anyway so swapping
test credentials for live ones is a
versions addrather than a chart edit andredeploy.
Verification
helm templatewith the flag off: zerostriperefs in theExternalSecret,
commonly-dev-jwt-secretstill present.helm templatewith the flag on: all three refs render.helm lintclean in both states.Flipping
billing.enabled: trueinvalues-dev.yamlis the follow-up, oncethe two remaining secrets have versions.