· via dev.to (home feed)
A misplaced 'use client' directive bundled a Stripe secret key into Next.js client JavaScript
A dev.to postmortem explains how defining a Next.js 15 Server Action inside a 'use client' file shipped a Stripe secret key to browsers, and how file separation, validation and CI audits prevent it.
How the leak happened
A post on dev.to by Elena Rostova recounts a production incident the author was asked to trace: a Next.js 15 application was delivering the team's Stripe secret key to browsers as part of its client JavaScript. The cause was not an exposed endpoint or a leaked environment file. It was a single 'use client' directive at the top of a file that also contained a Server Action.
The problematic module opened with 'use client', then declared an exported async function marked internally with 'use server'. That function imported the project's Stripe client — the object initialized with the secret key. According to the author, Next.js will happily serialize a function reference like this, but because the enclosing module sits on the client side of the boundary, the bundler follows the module's imports, and the credential gets compiled into the static chunks that any visitor can download and read.
The fix is architectural, not a patch
The corrected layout in the post is simple: the payment action lives in its own file whose first line is 'use server' and which contains no client directive anywhere. The client component — a checkout button — imports that action and invokes it inside a React transition, but never imports the Stripe client itself. Once no import chain connects client code to the secret, the bundler has nothing sensitive to follow.
Treating Server Actions like public endpoints
Rostova rewrites the action so it behaves less like a trusted internal function and more like an API route. It first checks for an authenticated session and rejects anonymous callers. It then validates raw input with a Zod schema before anything reaches Stripe: the amount must be positive and capped, the currency restricted to an allowlist, and the idempotency key required to be a UUID. That key is forwarded to Stripe so a network retry cannot produce a duplicate charge. Failures are returned to the caller as structured objects rather than raw exceptions, which keeps stack traces and internal details on the server.
A second trap: serial awaits
The same post describes a performance pitfall that often coexists with this kind of code: sequential awaits in a server component — fetch the user, then the posts, then the comments — block rendering in order. Wrapping independent sections in separate Suspense boundaries lets Next.js 15 send the shell HTML immediately, start the independent fetches in parallel, and patch in each section as its data arrives. The author reports LCP improvements of 40–60% from this restructure, though that figure comes from her own experience rather than a controlled benchmark.
Making the regression check automatic
After the Stripe incident, the team added a CI step: build the app, then grep the generated static chunks for the sk_live_ prefix that marks live Stripe keys, and fail the build on any match. According to the post, this scan has since caught three further leaks before they reached production. Worth noting: a prefix scan only catches credentials with recognizable formats, so pairing it with broader secret-scanning tools would widen the net.
Why it matters
Server Actions make the client–server boundary feel like an ordinary function call, which is exactly why it is easy to get wrong. Directive placement is not a style preference: 'use client' on the wrong file effectively reclassifies everything the module imports as client code, and anything in a client bundle should be assumed public. Rostova's incident is a useful reminder of three cheap habits — keep server-only logic in dedicated 'use server' files, validate and authenticate inside every action as if it were a public endpoint, and verify in CI that what you ship to browsers contains no credentials. Next.js 15 supplies the primitives; wiring them safely, the post argues, is still the developer's responsibility.
- #next-js
- #server-actions
- #security
- #stripe
- #react