readable stack traces without exposing your source
Minified JavaScript without source maps turns every production error into a guessing game over cryptic line numbers. But shipping source maps publicly right next to the bundle exposes your commented TypeScript source to every visitor. Hidden source maps combined with a controlled upload to an error-tracking service solve this dilemma cleanly.
Table of Contents
- 1. The dilemma: debugging without exposing your source code
- 2. How source maps technically work
- 3. Source map options in tsconfig.json in detail
- 4. Hidden source maps: the safe default approach
- 5. Automatic upload to error-tracking services
- 6. Access protection at CDN and server level
- 7. Source maps in monorepos and micro-frontends
- 8. Build performance: source maps and CI time
- 9. Source map strategies compared
- 10. Summary
- 11. FAQ
1. The dilemma: debugging without exposing your source code
Every modern TypeScript frontend gets minified and bundled for production, which strips variable names, removes line breaks and merges multiple modules into a single, nearly unreadable file. A runtime error in this bundle produces a stack trace pointing to line 1, column 48213, which is practically worthless for debugging. Source maps solve this by mapping every position in the compiled JavaScript exactly back to the original TypeScript line, including original variable names and file path.
The problem arises the moment teams ship source maps publicly next to the bundle without thinking it through, simply because the build tool generates them by default and the deployment process copies them along. In that case, any website visitor can use browser DevTools to view the complete, commented TypeScript source code, including internal API endpoints, business logic and sometimes even commented-out debug secrets. The right answer is not to skip source maps entirely, but to use them in a controlled, non-public way.
2. How source maps technically work
A source map is a JSON file with a .map suffix that contains a so-called VLQ-encoded mappings table: for every position in the generated JavaScript, a position in one of the referenced original source files is stored. At the end of the compiled JavaScript file sits a comment of the form //# sourceMappingURL=main.js.map, which browser DevTools automatically evaluate to show the original TypeScript file instead of the minified code while debugging.
It is important that a source map itself either embeds the original source code inline as a base64-encoded string in the sourcesContent field, or merely references the original file paths without shipping the content. In the first case, access to the .map file alone is enough to reconstruct the entire source code. In the second case, an attacker would additionally need access to the referenced original files, though in practice this rarely provides effective protection, since build pipelines usually enable sourcesContent by default for convenience.
{
"version": 3,
"file": "main.js",
"sourceRoot": "",
"sources": ["../src/main.ts", "../src/order-service.ts"],
"sourcesContent": [
"// Full original TypeScript source embedded here if inlineSources is true"
],
"names": ["OrderService", "calculateTotal", "customerId"],
"mappings": "AAAA,SAASA;AACT,MAAMC..."
}
3. Source map options in tsconfig.json in detail
TypeScript offers several granular options in tsconfig.json that together determine how and whether source maps are generated. "sourceMap": true generates a separate .js.map file for each compiled file, with the classic sourceMappingURL comment at the end of the file. "inlineSourceMap": true instead embeds the map as a base64 data URI directly into the JavaScript file, which is convenient for individual Node.js scripts but unnecessarily inflates bundle size in a browser context.
The option that matters most for production scenarios is "inlineSources": true combined with sourceMap: it embeds the full original source code into the map. This exact combination should be deliberately avoided in production builds, or at least paired with strict access protection, since it is the mechanism through which a publicly reachable map exposes the entire source code. For libraries published as npm packages, declarationMap: true is additionally useful so consumers can jump directly to the TypeScript definition while debugging, without this clashing with the production security question.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"sourceMap": true,
"inlineSourceMap": false,
"inlineSources": false,
"declaration": true,
"declarationMap": true,
"outDir": "dist"
},
"exclude": ["**/*.test.ts", "dist"]
}
4. Hidden source maps: the safe default approach
The approach recommended by most error-tracking providers is called hidden source maps: the .map files are generated during the build, but the sourceMappingURL comment is stripped from the final JavaScript bundle before it ships. A regular visitor's browser therefore finds no hint that a source map even exists, and consequently never loads one. At the same time, the .map files continue to exist as build artifacts and can be deliberately uploaded to an internal error-tracking system.
In Webpack, devtool: 'hidden-source-map' enables exactly this behavior. In Vite and Rollup, the same effect can be achieved by setting build.sourcemap: true and then stripping the sourceMappingURL comment from the shipped files in a post-build step before deployment begins. The decisive advantage: a development team retains full access to readable stack traces in the error-tracking dashboard, while a regular website visitor never has any way to reach the original TypeScript source code.
// webpack.config.js — hidden source maps for production
const isProd = process.env.NODE_ENV === 'production';
module.exports = {
mode: isProd ? 'production' : 'development',
// Generates .map files but strips the sourceMappingURL comment
// from the shipped bundle — maps stay build artifacts only.
devtool: isProd ? 'hidden-source-map' : 'eval-source-map',
output: {
filename: '[name].[contenthash].js',
path: __dirname + '/dist',
},
};
5. Automatic upload to error-tracking services
Services like Sentry, Bugsnag or Rollbar provide CLI tools that upload source maps directly during the CI build to a private storage area, tied to a unique release version. When an error occurs in live operation, the client only sends the minified stack trace along with the release identifier. The error-tracking service resolves the minified position server-side against the previously uploaded, never publicly reachable source map, and shows the development team the original TypeScript line with variable names.
Decisive for a reliable workflow is that the release version injected into the client during the build exactly matches the release version under which the maps were uploaded. A common source of failure: the CI build produces a new version, but the map upload fails due to an expired auth token, and the deployment still goes through. The result is weeks of unresolvable stack traces until someone notices the failed upload step in the build logs. A build should therefore fail if the source map upload was not successful, instead of silently ignoring the problem.
#!/usr/bin/env bash
# ci-deploy.sh — build, upload source maps, then strip them from the bundle
set -euo pipefail
RELEASE="myapp@$(git rev-parse --short HEAD)"
npm run build
# Create the release and associate commits for better stack traces
npx sentry-cli releases new "$RELEASE"
npx sentry-cli releases set-commits "$RELEASE" --auto
# Upload source maps to Sentry — fails the pipeline on error, no silent skip
npx sentry-cli sourcemaps upload \
--release="$RELEASE" \
--url-prefix '~/static/js' \
./dist
# Finalize release only after a successful upload
npx sentry-cli releases finalize "$RELEASE"
# Strip .map files from the deployable bundle — they must never be public
find ./dist -name '*.map' -delete
6. Access protection at CDN and server level
Where hidden source maps cannot be implemented immediately for organizational reasons, for example because an external monitoring tool needs direct HTTP access to the .map file, the next best protection is explicit access control at server or CDN level. On an Nginx server, access to *.map files can be restricted to specific IP ranges or a basic-auth password, while the actual JavaScript bundle continues to be served publicly. On a CDN like Cloudflare, the same result is achieved through a WAF rule that blocks requests to paths ending in .map unless they originate from an internal IP range.
An additional, often underestimated protection mechanism is to never place source maps in the public deployment directory at all, but rather in a separate, non-routable S3 bucket or artifact store that only the CI process and the error-tracking tool can access. This approach avoids the cat-and-mouse game with access rules entirely, because the files are physically never located anywhere a website visitor could reach in the first place. For TypeScript projects with strict compliance requirements, this is the most robust approach.
# nginx.conf — restrict *.map access to an internal IP range,
# while the regular JavaScript bundle stays publicly served
location ~ \.map$ {
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
add_header Cache-Control "no-store";
}
location ~* \.js$ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
7. Source maps in monorepos and micro-frontends
In a TypeScript monorepo with multiple micro-frontends or independently deployable packages, source map handling quickly becomes confusing, since each sub-project has its own build artifacts and its own release cycle. The proven approach is to give each package its own unique release identifier that combines the package name with the commit hash or version number, for example checkout-frontend@2.14.3. Only this way can an error-tracking service select the correct map collection when an error occurs in a specific micro-frontend, instead of accidentally using maps from another package.
With server-side rendering and chunks shared between multiple micro-frontends, it is additionally important that the url-prefix used during the source map upload matches exactly the actual delivery path in the browser. If the path in the build differs from the path in production, for example due to a different CDN subdomain or an extra version folder, the error-tracking service will not find the matching map, even though it was uploaded correctly. This detail is the most common cause of uploads that appear to succeed but still fail to produce resolved stack traces.
8. Build performance: source maps and CI time
Generating source maps is not free: it measurably extends build time, because the compiler or bundler has to compute a mapping to the original position for every line in the output code and serialize it into its own file. On large TypeScript projects with several thousand files, enabling full source maps can increase compilation time by 15 to 30 percent, depending on the build tool and the level of detail of the chosen devtool option.
A pragmatic compromise for faster local development builds is devtool: 'eval-cheap-module-source-map' in Webpack, which forgoes column-level precision in favor of line-level precision and thus builds incrementally much faster, while the CI production build continues to use the full, column-accurate variant with hidden-source-map. This separation between a fast development configuration and a precise but slower production configuration is standard practice in nearly every production-grade TypeScript build setup.
9. Source map strategies compared
Choosing the right source map strategy depends on security requirements, team setup and the error-tracking infrastructure in use. The following table compares the common approaches for TypeScript projects in production.
| Strategy | Source code publicly visible | Readable stack traces | Recommendation |
|---|---|---|---|
| No source maps | No | No | Debugging practically impossible |
| Public maps next to the bundle | Yes, fully | Yes | Security risk, avoid |
| Hidden source maps + tracking upload | No | Yes, in the dashboard | Recommended default |
| Access-protected maps (WAF/auth) | Only for authorized IPs | Yes | Fallback if upload is not possible |
| Maps in separate, non-routable storage | No | Yes, via internal access | Best choice for compliance requirements |
For the vast majority of TypeScript projects, the combination of hidden source maps and automatic upload to an error-tracking service is the right middle ground between debugging comfort and security. Access-protected maps or a separate storage location only come into play when organizational constraints prevent direct upload or regulatory requirements demand strict physical separation.
Mironsoft
TypeScript observability, error tracking and secure build pipelines
Readable stack traces without exposed source code?
We set up hidden source maps, automated uploads to your error-tracking solution and clean access protection for your TypeScript builds, so debugging in production stays secure and efficient.
Source map audit
Checking whether your maps are accidentally publicly reachable
CI integration
Automated upload to Sentry, Bugsnag or your monitoring solution
Access protection
Setting up WAF rules, separate storage and release versioning
10. Summary
Source maps in production are not an optional extra but a prerequisite for effective debugging of TypeScript applications in live operation. The solution to the security dilemma is hidden source maps: the sourceMappingURL comment is removed from the publicly shipped bundle, while the maps themselves continue to exist as a build artifact and are deliberately uploaded to an error-tracking service.
Reliable release versioning is essential for operations, so that uploaded maps and shipped bundles match exactly, along with a build process that blocks deployment when a map upload fails instead of letting it pass silently. Where direct upload is not organizationally possible, access protection at the server level or a separate, non-routable storage location replaces the security risk of publicly reachable maps.
Source Maps in Production for TypeScript — Key Takeaways
Hidden source maps
Remove sourceMappingURL from the public bundle, keep maps as an internal build artifact.
Automatic upload
CLI tools from Sentry, Bugsnag and others upload maps release-bound to a private storage area.
Access protection
WAF rules or separate, non-routable storage as a fallback without direct upload.
Build separation
Fast, coarse-grained maps in development, precise hidden source maps in the production build.