# How do we keep preview environments from emailing real customers or charging real cards?

> Contain preview side effects with sandbox credentials, email sinks, restricted egress, and configuration that blocks unsafe sends or charges.

Canonical URL: https://www.devobs.io/articles/qa-preview-environment-external-side-effects/
By: Samira Haddad
Published: 2023-09-11T04:26:18.148Z
Updated: 2026-09-06T10:18:15.722Z
Section: Architecture

Preview environments should be unable to reach real side-effecting systems by default. The reliable pattern is layered: use provider sandboxes and test keys, replace customer-facing destinations with sinks, restrict outbound network paths, and make unsafe configuration fail closed so a missing variable stops the action instead of falling back to production. This adds some setup work, but it is the difference between a safe disposable environment and one bad secret away from charging cards or emailing users.

## What should be blocked by default?

Start by inventorying every outbound action that changes state outside the preview environment: payment capture, email, SMS, webhooks, CRM writes, analytics events, push notifications, and background jobs that call third-party APIs. Treat each one as unsafe until you can prove it points to a non-production target.

For payments, prefer the provider’s sandbox instead of mocking the happy path. Stripe explicitly documents that you should “test your integration in a sandbox” and that “these transactions don’t move funds,” using test API keys in all API calls and test payment methods such as `pm_card_visa` in code rather than real card numbers ([Stripe testing](https://docs.stripe.com/testing)). That gives you realistic integration coverage without risking a charge.

For email, the equivalent control is a sink. In preview, the app should deliver to a mailbox capture service, fake SMTP server, or internal API stub that records messages for inspection and never relays externally. Do not rely on “developers will remember not to invite real users.” The environment should make that impossible.

## How do we stop a production credential mistake?

Assume someone will eventually wire the wrong secret. Your safety model should still hold.

First, make environment identity explicit. Every deployment should expose `ENVIRONMENT=preview` and every side-effecting code path should check it before sending. Second, require preview-specific secrets names such as `STRIPE_SECRET_KEY_PREVIEW`; never let preview reuse a generic `STRIPE_SECRET_KEY`. Third, validate the credential mode before enabling payments: accept only explicitly approved test or sandbox credentials and reject live or unrecognized credentials at startup. A variable name is not proof of its value. If the application cannot enforce this policy reliably, send preview payment requests through a broker that holds only sandbox credentials and prevents callers from supplying their own keys. Missing or invalid configuration must disable the action.

Then add network containment. Kubernetes documents that pods are non-isolated for egress by default, and that once egress isolation applies, only connections allowed by policy may leave the pod ([Kubernetes NetworkPolicies](https://kubernetes.io/docs/concepts/services-networking/network-policies/)). In practice, put preview workloads in namespaces with default-deny egress and allow only the destinations they truly need, such as your test payment endpoint, fake mail service, source control callback, and package registry. This only works if your cluster networking supports NetworkPolicy enforcement, which Kubernetes also calls out. Egress rules restrict network destinations, not a provider’s account or credential mode. Stripe test and live requests can use the same API host, so allowing that host does not enforce test mode; credential validation or an enforcing broker must provide that boundary.

## What does a safe preview setup look like?

Use this decision checklist:

1. Label the namespace and workloads as preview.
2. Apply default-deny egress for preview pods.
3. Allowlist only approved preview destinations.
4. Inject sandbox credentials and reject live or unknown credential modes before enabling payments.
5. Point email and SMS to sinks, not real delivery providers.
6. Disable scheduled jobs that are hard to sandbox.
7. Make unsafe config abort sends and charges.
8. Add tests that prove a payment uses test mode, a deliberately supplied live credential is rejected before any provider request, and an email lands in the sink.

Worked example: a [pull request](https://www.devobs.io/articles/small-pull-request-dependency-stacks/) environment for `checkout-service` can talk to its isolated database, a preview payment broker, and a mail sink. The broker holds only approved Stripe sandbox credentials and refuses caller-supplied credentials. It cannot reach your production SMTP host or unrestricted internet egress. If `STRIPE_SECRET_KEY_PREVIEW` is absent, the payment endpoint returns `503 preview payments disabled` instead of trying any fallback key.

## How do we still test real integrations?

Yes—use the real provider’s test environment where available. Stripe’s test mode is better than a mock for payment flows because it exercises authentication, declines, refunds, and dispute scenarios without moving funds ([Stripe testing](https://docs.stripe.com/testing)). Reserve mocks for dependencies that do not offer a meaningful sandbox.

## What if a tool cannot be sandboxed?

Quarantine it. Either disable that feature in preview, or route it through an internal broker that enforces non-production policy centrally. If you cannot make the action preview-safe, the correct default is not to run it.

Next step: pick one preview environment this week, list every outbound side effect it can trigger, and enforce one default-deny egress policy plus one sinked destination before adding anything else.

Reviewed: 2026-09-05

## Source references

- <https://docs.stripe.com/testing>
- <https://kubernetes.io/docs/concepts/services-networking/network-policies/>
