Local Stripe Sandbox Setup
Use this workflow for local Movie Renamer Ultimate checkout and webhook testing in WSL.
Local URL
- Run the app at
http://localhost:3000 - Set
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Required .env.local values
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PRICE_ID=price_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_BASE_URL=http://localhost:3000
DISABLE_SENTRY=1
NEXT_PUBLIC_DISABLE_SENTRY=1
NEXT_TELEMETRY_DISABLED=1
v1_KV_REST_API_URL=https://...
v1_KV_REST_API_TOKEN=...
EMAIL_PREVIEW_ENABLED=1
EMAIL_TEST_ENABLED=1
EMAIL_TEST_TOKEN=replace-with-long-random-string
# Optional for real email sending. Omit during initial sandbox work.
RESEND_API_KEY=re_...
# Optional for local download-proxy failure testing. Defaults to production.
DOWNLOAD_SERVER_BASE_URL=https://downloads.joshlehman.ca/download/file-renamer
For local privacy-first testing, keep Sentry and Next.js telemetry disabled with the three flags shown above.
DISABLE_SENTRY=1 must be present at build time, not just runtime. This repo's
next.config.ts only skips withSentryConfig(...) when that flag is set during
the build, which prevents Sentry's build plugin from attempting source map
upload or project validation.
Redis variable naming
Use v1_KV_REST_API_URL and v1_KV_REST_API_TOKEN.
The v1_ prefix is intentional. This repo should keep those exact names so
local setup matches deployment.
Stripe CLI listener
Forward sandbox webhook events to the local webhook route with:
stripe listen --forward-to http://localhost:3000/api/webhook/stripe
Copy the printed whsec_... value into STRIPE_WEBHOOK_SECRET for the current
local session.
For this repo, you can automate that with:
npm run stripe:listen:start
Useful companion commands:
npm run stripe:listen:status
npm run stripe:listen:stop
The start command:
- fetches a fresh listener secret with Stripe CLI
- updates
STRIPE_WEBHOOK_SECRETin.env.local - starts
stripe listenin the background - writes listener state under
.tmp/stripe-listener/
Restart the app server after npm run stripe:listen:start so the new webhook
secret is loaded into the running Next.js process.
Stripe CLI authentication still expires periodically. When that happens, rerun
stripe login manually in WSL, then resume using the automation commands.
Restart server rule
Restart the local Next.js dev server before each Stripe E2E or CLI-triggered verification run. Treat stale-process cleanup as part of the normal server-start routine for this repo, not as a one-off recovery step.
This keeps env-backed behavior, webhook handling, and local state predictable between attempts and avoids chasing stale server state.
If npm run build is run from inside a restricted sandbox, Next.js 16 with
Turbopack can fail with an internal error mentioning creating new process and
binding to a port. That is a sandbox limitation, not an application build
failure. Rerun the build with unrestricted execution and wait for the build
process to exit with code 0.
For Redis-backed checkout verification, the local app server also needs network
access to Upstash. If checkout creation fails before redirecting to Stripe and
the server log shows fetch failed or ENOTFOUND, restart npm run dev
outside the restricted sandbox before retrying the flow.
For Playwright runs, this repo now enforces the stricter production-server
workflow automatically through the shared scripts/start-fresh-prod-server.sh
entry point:
- stops stale listeners on test-related ports
- deletes
.next/ - rebuilds the app
- starts a fresh production server
This is the default local E2E behavior for this project; do not rely on a previously running app server for Playwright.
The npm Playwright entry points also run npm run cleanup:test-services first
so Playwright does not fail early on an already-bound local app port or stale
Chrome debug port.
Failure-mode checks
The final local storefront pass should explicitly verify these behaviors:
- wrong
STRIPE_WEBHOOK_SECRETreturns400fromPOST /api/webhook/stripe - stopped Stripe listener means the app receives no forwarded webhook at all
- Redis-unavailable fulfillment still returns
200to Stripe and logs a post-payment warning instead of triggering retries - invalid or expired
/download/<orderId>shows the fallback guidance page
To force a live download-proxy failure without changing source, start the
production server with a bad DOWNLOAD_SERVER_BASE_URL, then hit a real
tokenized /api/download/<token>?platform=... URL. The route should return
500 with:
{"error":"Failed to download file. Please try again or contact support."}
Docker-backed ZAP workflow
The preferred local security-scan setup for this repo is Docker Desktop on Windows with WSL integration enabled for the Ubuntu distro used by the project.
Why this is the preferred path:
- it avoids installing a separate Docker daemon inside the distro
- it matches how future WSL storefront projects should run local ZAP
- it lets the repo's
scripts/security-scan.shuse the official ZAP container
Required host setup:
- Docker Desktop installed on Windows
- Docker Desktop running
- WSL integration enabled for the active Ubuntu distro
Useful host checks from WSL:
/mnt/c/Program\ Files/Docker/Docker/resources/bin/docker.exe version
docker --version
If docker.exe version reports that it cannot connect to
dockerDesktopLinuxEngine, Docker Desktop is installed but not running yet.
Once Docker Desktop is running and WSL integration is active, the local ZAP scan can be run with:
npm run test:security
That command uses the repo's Docker-based ZAP wrapper and writes reports under
zap-reports/.
For this repo's current WSL setup, Docker Desktop does not reach the app
through host.docker.internal. The working target is the Ubuntu distro's own
WSL IP, and scripts/security-scan.sh now resolves that automatically with
hostname -I.
The wrapper also gives ZAP its own writable temporary work directory. That
avoids Automation Framework permission errors when the container writes
zap.yaml during the baseline run.
Run ZAP against a production server, not next dev:
npm run test:security
For the default local case, npm run test:security now manages the local
server lifecycle itself:
- stops stale listeners on test-related ports
- deletes
.next/ - rebuilds the app
- starts a fresh production server
- runs the Docker-based ZAP baseline
- shuts the temporary server down when the scan finishes
If the scan reports ZAP failed to access the target, confirm that:
- the app is running on port
3000 - the production server is bound before starting ZAP
- Docker Desktop is running with WSL integration enabled
- the distro IP from
hostname -Iis still current for this session
Current local baseline status:
- the Docker-based ZAP baseline now runs successfully through
npm run test:security - reports are written under
zap-reports/ - the tuned local policy now finishes with no fails and no warns
- remaining findings are informational only:
Content-Type Header Missingon the308redirect for/api/CSP: style-src unsafe-inlineCross-Origin-Embedder-Policy Header Missing or InvalidApplication Error Disclosureon the Stripe sandbox docs page because the page content discusses failure cases
First validation target
The first local sandbox milestone is:
- create a checkout session locally
- complete a Stripe sandbox payment
- confirm
checkout.session.completedreaches the webhook - confirm Redis token creation succeeds
- inspect the local fulfillment output without depending on live email delivery
Email preview workflow
For internal template review, enable the gated preview route with:
EMAIL_PREVIEW_ENABLED=1
Then open:
http://localhost:3000/email-preview
Optional query params let you preview realistic values without sending mail:
http://localhost:3000/email-preview?email=buyer@example.com&downloadUrl=http://localhost:3000/download/cs_test_123
Live email verification flow
For controlled real-email delivery tests, enable the internal route with:
EMAIL_TEST_ENABLED=1
EMAIL_TEST_TOKEN=replace-with-long-random-string
EMAIL_DELIVERY_MODE=send
Then send a request with the shared token:
curl -X POST http://localhost:3000/api/internal/email-test/purchase \
-H "content-type: application/json" \
-H "x-email-test-token: $EMAIL_TEST_TOKEN" \
-d '{
"email": "your-controlled-inbox@example.com",
"downloadUrl": "http://localhost:3000/download/cs_test_email_preview"
}'
For this repo, the repeatable command is:
npm run email:test:purchase
If that command exits with Missing recipient, set EMAIL_TEST_RECIPIENT in
.env.local or pass the recipient address directly to the script.