Source Maps in Production for TypeScript: Debugging Without the Security Risk
AI generated
<T>
type
TypeScript · Source Maps · Error Tracking · Security
Source Maps in Production for TypeScript
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.

16 min read hidden source maps · sourceMappingURL · Sentry · CDN protection TypeScript 5.x · Webpack · Vite · esbuild

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.

11. FAQ: Source Maps in Production for TypeScript

1Skip source maps entirely?
No, then stack traces are unreadable. Hidden source maps with tracking upload are the right path.
2What is a hidden source map?
The map exists, but the sourceMappingURL comment is missing from the shipped bundle, so browsers never load it.
3Most important tsconfig option?
inlineSources, since it embeds the full source code into the map and should be used with caution.
4How does the Sentry upload work?
A CLI tool uploads maps release-bound to a private area, never making them public.
5What if the upload fails?
The build should fail instead of continuing silently, otherwise later errors remain unresolvable.
6Protection without direct upload?
WAF rules or Nginx configuration restricting *.map access to internal IP ranges.
7Why doesn't url-prefix sometimes match?
If the browser delivery path differs from the build path, the service won't find the map despite a successful upload.
8How much slower is the build?
15 to 30 percent on large projects, depending on the build tool and devtool precision.
9Own release identifier per micro-frontend?
Yes, otherwise the error-tracking service can't find the right map collection for a specific package.
10Same devtool option for dev and prod?
No, development uses a faster, coarse-grained option, production the precise hidden-source-map variant.