Netwarden
Back to Documentation
guidesv1.0

Deploy integrations: Vercel, Railway, Fly, and more

Drop-in recipes for uploading source maps and tagging releases as part of every deploy on the platforms vibecoders use.

Last updated: May 8, 2026
11 min read

Deploy integrations: Vercel, Railway, Fly, and more

The Netwarden CLI is great for ad-hoc uploads, but you do not want to remember to run it. The whole point of source-map upload is that it happens every time you ship, not the times you remember to. This page is a set of copy-paste recipes that wire upload-sourcemaps and releases create into the deploy pipelines of the platforms most teams ship to today.

Why this matters

When a runtime error fires in a browser, the stack trace points into your minified bundle: at e (main.4f8a2b.js:1:48211). That is unreadable. Source maps translate those positions back into the original file and line, so you can actually fix things.

Two things have to be true for that to work:

  1. The source map has to be uploaded to Netwarden.
  2. The release version the SDK reports at runtime has to match the release the source map was uploaded under.

If either is missing, you get raw minified frames in the dashboard. The SDK does not warn you about this — your build keeps succeeding, your deploys keep going out, and the day you actually need symbolication you find out you do not have it. The recipes below close that gap by making upload a deploy-time step.

What you need before starting

  • A DSN for the project, copied from /apps/[id]/settings. It looks like https://[email protected]/proj_xxx.
  • A release version scheme. Two good defaults: the short Git SHA (git rev-parse --short HEAD) or the version field from package.json. Whichever you pick, the SDK's init({ release }) call must produce the exact same string the CLI uploads under.
  • @netwarden/sdk installed in your project (npm install @netwarden/sdk). The recipes invoke the CLI via npx @netwarden/cli, which works whether the package is a runtime dep or a devDependency.

Two environment variables are referenced everywhere below: NETWARDEN_DSN and NETWARDEN_RELEASE. Set them once per platform; the recipe scripts read them.

Vercel

Vercel is the easiest case because if you are using Next.js, the bundler plugin does the right thing automatically — you just need to wire up two env vars.

1. Set environment variables

In the Vercel dashboard, go to Settings → Environment Variables for the project and add:

  • NETWARDEN_DSN — your project DSN, scoped to Production (and Preview if you want preview deploys symbolicated too).
  • NETWARDEN_RELEASE — leave this unset. Vercel exposes VERCEL_GIT_COMMIT_SHA to every build, and the recipes below use it. Setting it manually means every deploy uses the same release string, which defeats the purpose.

2. Path A: Next.js with the plugin (recommended)

If your app is Next.js, wrap your config with withNetwarden:

js
// next.config.js
const { withNetwarden } = require('@netwarden/sdk/plugins/next');

module.exports = withNetwarden({
  reactStrictMode: true,
  productionBrowserSourceMaps: true,
}, {
  dsn: process.env.NETWARDEN_DSN,
  release: process.env.VERCEL_GIT_COMMIT_SHA,
});

That is the entire integration. After every Vercel build, the plugin discovers .map files in .next/static/, uploads them, and registers the manifest. There is nothing to add to vercel.json.

Make sure the SDK's runtime init uses the same release string:

js
// app/layout.tsx (or wherever you bootstrap the SDK)
import { init } from '@netwarden/sdk/browser';

init({
  dsn: process.env.NEXT_PUBLIC_NETWARDEN_DSN,
  release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
});

The NEXT_PUBLIC_ prefix is what makes Vercel inline the value into the browser bundle.

3. Path B: non-Next builds with a script

For Vite, Astro, Remix, or any other framework on Vercel, run the recipe script as part of the build command. In package.json:

json
{
  "scripts": {
    "build": "vite build && bash ./scripts/vercel-build-script.sh"
  }
}

Drop vercel-build-script.sh into your repo (see recipes/vercel-build-script.sh in @netwarden/sdk). It reads NETWARDEN_DSN and VERCEL_GIT_COMMIT_SHA, then calls upload-sourcemaps and releases create against your built output.

Vercel's build environment is ephemeral but supports arbitrary post-build scripts. The build-output API is what powers the plugin path; the script path uses the older but equally reliable approach of running after vite build (or whatever your bundler is) finishes.

Railway

Railway uses Nixpacks by default, which honours package.json build scripts. The cleanest hook is the postbuild npm lifecycle script — Nixpacks runs it automatically after build.

1. Set environment variables

In the Railway dashboard, open Variables and add:

  • NETWARDEN_DSN — your project DSN.
  • RAILWAY_GIT_COMMIT_SHA — Railway exposes this automatically; you do not set it. The recipe reads it directly.

2. Path A: postbuild script (recommended)

In your package.json:

json
{
  "scripts": {
    "build": "vite build",
    "postbuild": "bash ./scripts/railway-postdeploy.sh"
  }
}

Drop the railway-postdeploy.sh recipe into ./scripts/. Nixpacks will invoke npm run build followed by npm run postbuild, and your source maps will upload as part of every successful deploy.

2. Path B: explicit railway.json

If you prefer to be explicit (or you are not using Nixpacks):

json
{
  "$schema": "https://railway.app/railway.schema.json",
  "build": {
    "builder": "NIXPACKS",
    "buildCommand": "npm run build && bash ./scripts/railway-postdeploy.sh"
  },
  "deploy": {
    "startCommand": "npm start"
  }
}

Both approaches end up doing the same thing. The postbuild lifecycle is slightly more portable because it survives if you switch builders later.

Notes

  • Railway runs the build inside the deploy container, so npx @netwarden/cli resolves against your project's node_modules. If @netwarden/sdk is a devDependency, set NIXPACKS_NO_DEV_DEPS_PRUNE=true so Railway does not strip it before the script runs.
  • Railway will redeploy on every push to your default branch by default. The recipe is idempotent — the platform dedups source maps by content hash, so re-running on the same commit is safe.

Fly.io

Fly's deploy model is Dockerfile-based. The natural place for source-map upload is after the image builds but before traffic shifts, which is exactly what [deploy] release_command is for.

1. Set secrets

bash
fly secrets set NETWARDEN_DSN=https://[email protected]/proj_xxx

Fly does not natively expose a commit SHA inside the container, so add it at build time:

dockerfile
# Dockerfile
ARG GIT_SHA
ENV NETWARDEN_RELEASE=$GIT_SHA

And pass it from CI:

bash
fly deploy --build-arg GIT_SHA=$(git rev-parse --short HEAD)

2. Wire up release_command

In fly.toml:

toml
[deploy]
  release_command = "bash /app/scripts/fly-release-command.sh"

Drop fly-release-command.sh into your repo at scripts/fly-release-command.sh and make sure your Dockerfile copies it into the image. The release_command runs in a one-off ephemeral machine that has access to your secrets and your built artifacts, then exits before the new release is rolled out to the live machines.

3. Multi-stage builds

Fly's standard pattern is a multi-stage Dockerfile: a builder stage that installs deps and runs the build, then a runtime stage that copies only the artifacts. The source-map upload needs the builder stage's node_modules and .map files, which the runtime stage usually does not carry.

The cleanest pattern is to upload during the build itself, before the artifact split. In your builder stage:

dockerfile
RUN npm run build
RUN bash /app/scripts/fly-release-command.sh

…and skip the [deploy] release_command entry. The trade-off: you need the DSN available at docker build time, which means passing it as a --build-secret rather than an env var. Pick whichever fits your secret-management story better.

Notes

  • release_command has a 5-minute timeout on most Fly plans. Source-map upload for a typical SPA finishes in under 30 seconds, but very large bundles (10+ MB of maps) on a slow link can brush against the limit. If you hit it, move the upload into the build step.
  • The release command runs once per deploy, not once per machine. You will not get duplicate uploads from Fly scaling out.

Cloudflare Pages

Cloudflare Pages is structurally similar to Vercel: ephemeral build environment, configurable build command, env vars set in the dashboard. The recipe is the same shape.

1. Set environment variables

In the Pages project settings, under Settings → Environment variables, add:

  • NETWARDEN_DSN — your project DSN.
  • Cloudflare exposes CF_PAGES_COMMIT_SHA automatically; the recipe reads it.

2. Build command

In Settings → Builds & deployments, set the build command to:

bash
npm run build && bash ./scripts/cloudflare-pages-build.sh

Drop cloudflare-pages-build.sh into the repo. It mirrors the Vercel script but reads CF_PAGES_COMMIT_SHA instead.

Caveats

  • Cloudflare Pages' build environment ships with Node 18 by default. If your build needs a newer version, set NODE_VERSION as a build env var. The CLI itself supports Node 18+.
  • Outbound HTTPS to your Netwarden host is allowed from the build sandbox; no allow-listing is required.
  • If you use Pages' Git integration, the script runs on every push to your watched branches. There is no separate "post-deploy" hook — everything happens during the build phase.

Netlify

Netlify supports two integration points: a postbuild npm script (same as Railway) or a Netlify Build plugin. The npm script is simpler and works for 95% of cases.

1. Set environment variables

In the Netlify dashboard, Site configuration → Environment variables, add:

  • NETWARDEN_DSN — your project DSN.
  • Netlify exposes COMMIT_REF automatically; the recipe reads it.

2. postbuild script

In package.json:

json
{
  "scripts": {
    "build": "vite build",
    "postbuild": "bash ./scripts/netlify-postbuild.sh"
  }
}

Or, if you do not want to touch package.json, set the build command in netlify.toml:

toml
[build]
  command = "npm run build && bash ./scripts/netlify-postbuild.sh"
  publish = "dist"

Drop netlify-postbuild.sh into the repo. Same shape as the Vercel and Cloudflare scripts.

Notes

  • Netlify has a separate Build plugins API if you need anything more sophisticated (running on specific events, gating by deploy context). For a single source-map upload step, it is overkill.
  • COMMIT_REF is the full SHA, not the short form. That is fine for the release string as long as your runtime SDK init uses the full SHA too.

Generic CI (GitHub Actions, GitLab CI)

For everything else — self-hosted CI, GitHub Actions runners, GitLab CI, CircleCI, Drone — the pattern is the same: build, then call the CLI, then deploy.

GitHub Actions

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci
      - run: npm run build

      - name: Upload source maps to Netwarden
        env:
          NETWARDEN_DSN: ${{ secrets.NETWARDEN_DSN }}
        run: |
          npx @netwarden/cli upload-sourcemaps \
            --release=$GITHUB_SHA \
            --path=./dist \
            --dsn=$NETWARDEN_DSN

      - name: Tag release
        env:
          NETWARDEN_DSN: ${{ secrets.NETWARDEN_DSN }}
        run: |
          npx @netwarden/cli releases create \
            --release=$GITHUB_SHA \
            --commit=$GITHUB_SHA \
            --dsn=$NETWARDEN_DSN

      - name: Deploy
        run: ./deploy.sh   # whatever your deploy step is

GitLab CI

yaml
# .gitlab-ci.yml
deploy:
  image: node:20
  script:
    - npm ci
    - npm run build
    - npx @netwarden/cli upload-sourcemaps --release=$CI_COMMIT_SHA --path=./dist --dsn=$NETWARDEN_DSN
    - npx @netwarden/cli releases create --release=$CI_COMMIT_SHA --commit=$CI_COMMIT_SHA --dsn=$NETWARDEN_DSN
    - ./deploy.sh
  only:
    - main

The pattern is portable: any CI runner that can run npx and reach your Netwarden host can do this.

Troubleshooting

The SDK is silent in production but builds succeed. The DSN is unset. The SDK fails open by design — it would rather drop events than crash your app. Verify NETWARDEN_DSN is set in the build environment and that the runtime SDK init has its own DSN (often the public-key half via NEXT_PUBLIC_NETWARDEN_DSN or equivalent).

Stack traces are still minified in the dashboard. The release string the SDK reports at runtime does not match what the CLI uploaded under. Open the affected event in the dashboard and check the release field; cross-check it against what upload-sourcemaps --release=... was given. They must be byte-for-byte identical, including any prefix like v1.2.3 vs 1.2.3.

npx @netwarden/cli upload-sourcemaps reports "no .map files found". Source maps are not being generated. Common fixes:

  • Next.js: set productionBrowserSourceMaps: true in next.config.js.
  • Vite: set build.sourcemap: true in vite.config.js.
  • webpack: set devtool: 'source-map' for production builds.

After rebuilding, confirm .map files exist in your output directory before re-running the CLI.

The --path is wrong. The CLI scans recursively from --path for .map files. If your output directory is .next/, dist/, or build/, point --path at the right one. Most build tools log the output directory at the end of the build — check the build log if in doubt.

The sign endpoint returns 401. The DSN's public key has been revoked or rotated. Generate a fresh DSN at /apps/[id]/settings and update the build env var. Old uploads under the previous DSN are unaffected.

Uploads succeed but the release does not appear in the dashboard. The manifest registration failed silently after the file uploads succeeded — re-run releases create --release=<v> --dsn=<dsn> to register the release explicitly. The CLI is idempotent; re-running is safe.

Was this page helpful?

Help us improve our documentation

Edit on GitHubReport an Issue