Hiding source maps from end users while keeping them debuggable
Server-side upload, signed URLs, and the deploy-script hooks that make this a one-time setup instead of a recurring leak risk.
You open Chrome DevTools on your production site, click a stack trace, and there it is: your entire clean, commented, original TypeScript source code, served directly to the world. It's a "feature" of modern build tools that quietly becomes a security liability. For a lot of teams the choice feels binary—either leave the maps public and risk code scraping, or delete them and spend hours debugging anonymous errors at line 1, column 54,021. You don't have to pick. You can have both security and visibility, and the setup is a one-time job.
The Hidden Cost of Public Source Maps
A source map is not just a debugging aid. When it includes the sourcesContent field—which most build tools embed by default—it is a complete, readable copy of your application's source, structure and comments intact. Leaving it on your CDN is, functionally, publishing an open-source project you never meant to release.
Intellectual property and code scraping
There are off-the-shelf tools that take a single //# sourceMappingURL= reference and reconstruct your whole frontend repo: directory layout, original filenames, every line of un-minified logic. A competitor doesn't need to reverse-engineer your minified bundle; they download the map and read your code like you wrote it.
Exposing vulnerabilities
Comments ship in the map. So does the structure attackers care about: the shape of your API calls, the names of your feature flags, that // TODO: re-enable the permission check before launch you wrote at midnight. Minified code hides this through friction; a source map removes the friction entirely.
The "security through obscurity" myth
Minification is sometimes treated as a security layer. It isn't—and it especially isn't when the map is one GET request away. e.id looks opaque until the map tells the reader it was currentUser.id. Obscurity that any browser can trivially undo is not protection. Our security docs go deeper on this distinction.
The "Generate, Upload, Delete" Workflow
The industry-standard architecture for private source maps has three steps, and the order is everything.
Step 1: Generate the maps during the build
You still need maps—you just don't need to serve them. Configure your bundler to emit them (ideally without the sourceMappingURL comment, so nothing in the public bundle even hints they exist). The maps land in ./dist as build artifacts.
Step 2: Upload to a secure endpoint
Push the .map files to your error tracker via its CLI or bundler plugin, tagged with a release identifier. Now the tracker holds your maps in private storage, version-locked to the deploy that produced them.
Step 3: Purge before deploy
Delete the .map files from ./dist before the folder ships to your CDN. The tracker has its copy; the public gets none. The whole flow is one line conceptually:
npm run build \
&& glitchreplay-cli sourcemaps upload --release "$GIT_SHA" ./dist \
&& rm -f ./dist/**/*.mapIf you delete before uploading, the tracker has nothing. If you upload but forget to delete, you've leaked. Chain them so neither can happen.
Framework-Specific Implementations
Next.js
Next.js does not serve browser source maps publicly by default—productionBrowserSourceMaps is false, and you should keep it that way. The maps still get generated for the build, which is exactly what you want: nothing public, but artifacts available for upload. Detailed setup for the App Router lives in error tracking on Next.js 15.
// next.config.js
module.exports = {
// Keep browser maps OFF the public output...
productionBrowserSourceMaps: false,
// ...the upload plugin still reads the generated maps internally.
};Vite
Set build.sourcemap: 'hidden'. This emits the .map file but strips the sourceMappingURL comment, so the bundle gives no hint the map exists. Pair it with the Sentry-compatible Vite plugin to handle the upload. The full three-config walkthrough is in our Vite + source maps guide.
// vite.config.ts
export default defineConfig({
build: { sourcemap: 'hidden' },
});Webpack
Use devtool: 'source-map' for accurate production maps, or reach for SourceMapDevToolPlugin when you need fine-grained control—it lets you set append: false to omit the URL comment and use devtoolModuleFilenameTemplate to normalize the paths recorded in the map. Just be careful with the loader chain; the subtle ways webpack maps go wrong are the subject of our source-map-loader postmortem.
// webpack.config.js
const { SourceMapDevToolPlugin } = require('webpack');
module.exports = {
devtool: false,
plugins: [
new SourceMapDevToolPlugin({
filename: '[file].map',
append: false, // no //# sourceMappingURL comment in the bundle
}),
],
};Automating the Upload in CI/CD
Done once in your pipeline, this becomes invisible.
GitHub Actions and GitLab CI
Run build, upload, and purge as sequential steps. Store the upload token as a CI secret—never in the repo—and consider marking the upload step non-blocking so a tracker outage can't stop a production hotfix.
# .github/workflows/deploy.yml
- run: npm run build
- name: Upload source maps
run: glitchreplay-cli sourcemaps upload --release "$GITHUB_SHA" ./dist
env:
GLITCHREPLAY_AUTH_TOKEN: ${{ secrets.GLITCHREPLAY_AUTH_TOKEN }}
continue-on-error: true
- name: Strip maps
run: rm -f ./dist/**/*.map
- run: npm run deployHandling versioning
The release string the SDK reports at runtime must match the release the maps were uploaded under—exactly. Derive both from the same source, almost always the git SHA. A mismatch here is the number-one reason a tracker has the map but still shows a minified trace.
Protecting your token
The upload token only needs map-upload scope. Scope it minimally, inject it via environment variable, and rotate it if it ever appears in a log. It should never be readable from the deployed bundle or the repo.
How GlitchReplay Resolves Private Maps
The ingestion flow
When an error arrives, GlitchReplay reads the release tag on the event, looks up the matching map in its private bucket, and reverses the minified coordinate back to your original source—all on ingest, before the error ever appears in your dashboard. No crawler reaches back into your infrastructure, and no map is ever exposed.
Stack trace reconstruction
The minified index-abc.js:1:54021 becomes PaymentForm.tsx:88 → submitPayment(), with original variable names restored from the map's names array. The how-it-works details are in our deminification deep dive.
Security of stored maps
Uploaded maps live in private storage, accessible only to your project for resolution. They are never served back to the public web, which is the entire point of moving them off your CDN in the first place.
Verifying Your Setup (and Common Gotchas)
The manual check
The fastest sanity check is a curl against production. You want a 404:
curl -I https://yourapp.com/assets/index-D7x9a2.js.map
# Expect: HTTP/2 404If that returns 200, your maps are public and the purge step isn't running. Our security headers checker can scan for related exposure issues too.
Handling multiple chunks
Code-split apps emit many chunks, each with its own map. Make sure your upload glob catches all of them (./dist/**/*.map, not just the entry), or some traces will resolve and others won't. The source map validator confirms a given map actually aligns with its bundle.
Source maps for edge workers
Cloudflare Workers and serverless functions have no file system, so don't inline maps into the bundle—it bloats your script and can blow past size limits. Generate, upload at build time, and purge, exactly as with browser bundles. We cover the edge-specific quirks in error tracking on Cloudflare Workers.
Stop Leaking, Start Debugging
Public source maps are an accidental open-source release with real security cost: scrapable code, exposed comments, and a blueprint for attackers. The fix is the generate-upload-delete workflow, wired once into CI, plus a curl check that confirms a 404 in production. Do that and you keep perfectly readable stack traces in your tracker while the public sees nothing but minified bytes.
GlitchReplay makes the private side painless: Sentry-SDK compatible uploads, on-ingest resolution from a private bucket, and flat-rate pricing so you can re-run the setup as many times as it takes without watching an event meter. Stop leaking your source code—upload your maps, purge your dist, and debug in private. The full configuration reference is in our source maps documentation.
GlitchReplay is Sentry-SDK compatible, includes session replay and security signals, and never charges per event. Free to start, five minutes to first event.