feat(stripe): make the database the only source of Stripe config #482

Merged
Claude-Run merged 1 commit from chore/BUNYIP-482-remove-stripe-env-vars into main 2026-08-06 03:36:57 +02:00
Member

Stripe configuration was readable from two places, and env won whenever the database row was empty or a field was NULL: stripe_config_from_env() built a runtime config from STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET / STRIPE_SUCCESS_URL / STRIPE_CANCEL_URL / STRIPE_APP_TAG / STRIPE_FREE_PRICE_ID, and stripe_config_from_db_model() used it as the per-field fallback for every NULL column, so an operator who cleared a field in the admin UI silently fell back to whatever the container was started with.

The stripe_config and tier_config rows already hold everything the runtime needs, so delete the env builders and make the DB the single source. unconfigured_stripe_config() replaces stripe_config_from_env() and keeps is_configured() false via the placeholder secrets; success_url_from_env / cancel_url_from_env become default_success_url / default_cancel_url, still derived from the first CORS_ORIGIN entry so just dev reaches Checkout with only the admin-entered keys. StripeConfigResponse::from_env() becomes unconfigured() reporting source: "unconfigured", and free_price_id_from_env() is gone: TierConfig reads the column only.

StripeConfig.free_price_id is now always None, so all four StripeService::free_price_id() call sites read the live Arc<RwLock<TierConfig>> instead (via the new handlers::live_free_price_id helper), which is always fresh after an admin tier-settings save rather than baked in at startup.

Operator-facing text that named env vars (the startup webhook-secret warning, the webhook fail-closed message, and the web checkout error, which also matched on the literal secret-key variable name) now points at the admin Stripe page.

The env surface is removed from compose.dev.yml, the two Stripe key secrets in compose.yml, scripts/init-secrets.sh, .env.example, docs/stripe-test-mode.md, and docs/e2e.md. STRIPE_ENCRYPTION_KEY and its _FILE variant stay everywhere: that is at-rest AES-256-GCM key material for the DB row (shared with email_config), not Stripe API information, and consolidating it is BUNYIP-483.

scripts/check-no-stripe-env.sh gates the removal in CI and just check: any STRIPE_* name outside the encryption-key allowlist fails the build. Committed migrations are excluded because sqlx checksums them and an edit stops a deployed database from booting, so their historical SQL comments stay as written.

#BUNYIP-482

Stripe configuration was readable from two places, and env won whenever the database row was empty or a field was NULL: `stripe_config_from_env()` built a runtime config from `STRIPE_SECRET_KEY` / `STRIPE_WEBHOOK_SECRET` / `STRIPE_SUCCESS_URL` / `STRIPE_CANCEL_URL` / `STRIPE_APP_TAG` / `STRIPE_FREE_PRICE_ID`, and `stripe_config_from_db_model()` used it as the per-field fallback for every NULL column, so an operator who cleared a field in the admin UI silently fell back to whatever the container was started with. The `stripe_config` and `tier_config` rows already hold everything the runtime needs, so delete the env builders and make the DB the single source. `unconfigured_stripe_config()` replaces `stripe_config_from_env()` and keeps `is_configured()` false via the placeholder secrets; `success_url_from_env` / `cancel_url_from_env` become `default_success_url` / `default_cancel_url`, still derived from the first `CORS_ORIGIN` entry so `just dev` reaches Checkout with only the admin-entered keys. `StripeConfigResponse::from_env()` becomes `unconfigured()` reporting `source: "unconfigured"`, and `free_price_id_from_env()` is gone: `TierConfig` reads the column only. `StripeConfig.free_price_id` is now always `None`, so all four `StripeService::free_price_id()` call sites read the live `Arc<RwLock<TierConfig>>` instead (via the new `handlers::live_free_price_id` helper), which is always fresh after an admin tier-settings save rather than baked in at startup. Operator-facing text that named env vars (the startup webhook-secret warning, the webhook fail-closed message, and the web checkout error, which also matched on the literal secret-key variable name) now points at the admin Stripe page. The env surface is removed from `compose.dev.yml`, the two Stripe key secrets in `compose.yml`, `scripts/init-secrets.sh`, `.env.example`, `docs/stripe-test-mode.md`, and `docs/e2e.md`. `STRIPE_ENCRYPTION_KEY` and its `_FILE` variant stay everywhere: that is at-rest AES-256-GCM key material for the DB row (shared with `email_config`), not Stripe API information, and consolidating it is BUNYIP-483. `scripts/check-no-stripe-env.sh` gates the removal in CI and `just check`: any `STRIPE_*` name outside the encryption-key allowlist fails the build. Committed migrations are excluded because sqlx checksums them and an edit stops a deployed database from booting, so their historical SQL comments stay as written. #BUNYIP-482
feat(stripe): make the database the only source of Stripe config
All checks were successful
E2E PR gate / E2E / PR gate (pull_request) Successful in 24s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m39s
Create release / Create release from merged PR (pull_request) Has been skipped
0f24fe2990
Stripe configuration was readable from two places, and env won whenever the database row was empty or a field was NULL: `stripe_config_from_env()` built a runtime config from `STRIPE_SECRET_KEY` / `STRIPE_WEBHOOK_SECRET` / `STRIPE_SUCCESS_URL` / `STRIPE_CANCEL_URL` / `STRIPE_APP_TAG` / `STRIPE_FREE_PRICE_ID`, and `stripe_config_from_db_model()` used it as the per-field fallback for every NULL column, so an operator who cleared a field in the admin UI silently fell back to whatever the container was started with.

The `stripe_config` and `tier_config` rows already hold everything the runtime needs, so delete the env builders and make the DB the single source. `unconfigured_stripe_config()` replaces `stripe_config_from_env()` and keeps `is_configured()` false via the placeholder secrets; `success_url_from_env` / `cancel_url_from_env` become `default_success_url` / `default_cancel_url`, still derived from the first `CORS_ORIGIN` entry so `just dev` reaches Checkout with only the admin-entered keys. `StripeConfigResponse::from_env()` becomes `unconfigured()` reporting `source: "unconfigured"`, and `free_price_id_from_env()` is gone: `TierConfig` reads the column only.

`StripeConfig.free_price_id` is now always `None`, so all four `StripeService::free_price_id()` call sites read the live `Arc<RwLock<TierConfig>>` instead (via the new `handlers::live_free_price_id` helper), which is always fresh after an admin tier-settings save rather than baked in at startup.

Operator-facing text that named env vars (the startup webhook-secret warning, the webhook fail-closed message, and the web checkout error, which also matched on the literal secret-key variable name) now points at the admin Stripe page.

The env surface is removed from `compose.dev.yml`, the two Stripe key secrets in `compose.yml`, `scripts/init-secrets.sh`, `.env.example`, `docs/stripe-test-mode.md`, and `docs/e2e.md`. `STRIPE_ENCRYPTION_KEY` and its `_FILE` variant stay everywhere: that is at-rest AES-256-GCM key material for the DB row (shared with `email_config`), not Stripe API information, and consolidating it is BUNYIP-483.

`scripts/check-no-stripe-env.sh` gates the removal in CI and `just check`: any `STRIPE_*` name outside the encryption-key allowlist fails the build. Committed migrations are excluded because sqlx checksums them and an edit stops a deployed database from booting, so their historical SQL comments stay as written.

#BUNYIP-482
Claude-Run deleted branch chore/BUNYIP-482-remove-stripe-env-vars 2026-08-06 03:36:57 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!482
No description provided.