Enchanted forest workshop scene where gnome engineers transition from a rusty orange-gold clockwork machine (old Gulp system) to a crystalline teal magical forge (new Heft system), with glowing environment variable orbs floating across a transformation bridge

Environment Variables with SPFx 1.22+ and Heft

An opinionated guide to implementing environment variables in SPFx 1.22+ with the new Heft build system, covering four approaches with code examples and recommendations.

Nello D'Andrea
Nello D'Andrea
6 minutes to read

I use environment variables extensively for allowing multi-tenancy configurations. With the release of SPFx 1.22, there is a new guy in town: Heft! Heft offers plenty of options for implementing environment variables. This post explains four possible approaches to use environment variables with Heft.

Table of Contents

Heft: The New Build System

With SPFx 1.22, Microsoft introduced Heft as the new build orchestrator, replacing the gulp-based toolchain. Heft is a config-driven task orchestrator from Rush Stack that provides better customization, improved performance, and active maintenance.

Why did Microsoft make this change?

  • The old gulp toolchain had accumulated technical debt and security warnings
  • Community feedback since 2020 requested more flexibility and transparency
  • Aligning internal Microsoft teams with external developers accelerates feature delivery
  • A true plugin architecture enables sustainable, maintainable customizations

For the full details, see Microsoft’s official documentation: SharePoint Framework Toolchain: Rush Stack Heft .

Do I Need to Migrate?

Short answer: Not yet, but eventually yes.

Microsoft has established a phased transition timeline:

SPFx Version New Projects Existing Projects Gulp Support
v1.22 Default to Heft (can use --use-gulp) Keep using gulp Both supported
v1.23 Heft only Keep using gulp Both supported (critical fixes only)
v1.24+ Heft only Can use gulp but… Officially unsupported

Projects that upgrade their package.json to reference SPFx v1.22 or later can continue using gulp and existing customizations without breaking changes. The new toolchain only affects newly generated projects and those that choose to migrate.

So if you have an existing project with environment variables working via gulpfile.js, you can keep it running. But if you’re starting a new project, or want to benefit from Heft’s improvements, read on.

Understanding the Build Pipeline Change

If you used environment variables with gulp, your code probably looked like this:

// The gulp way - gulpfile.js (pre-SPFx 1.22)
const webpack = require('webpack');
const dotenv = require('dotenv');

build.configureWebpack.mergeConfig({
  additionalConfiguration: (generatedConfig) => {
    const envConfig = dotenv.parse(fs.readFileSync('./env/dev.env'));
    generatedConfig.plugins.push(
      new webpack.DefinePlugin({
        'process.env.SPFX_API_URL': JSON.stringify(envConfig.SPFX_API_URL),
        'process.env.SPFX_TENANT_ID': JSON.stringify(envConfig.SPFX_TENANT_ID)
      })
    );
    return generatedConfig;
  }
});

With Heft, the configuration moves from JavaScript-based gulp tasks to JSON configuration files:

flowchart LR classDef default fill:#CFA63D,stroke:#c7a42b,stroke-width:1px; classDef heft fill:#0aa8a7,stroke:#087f7e,stroke-width:2px; subgraph Gulp[Gulp Toolchain] G1[gulpfile.js] --> G2[build.configureWebpack] G2 --> G3[DefinePlugin] end subgraph Heft[Heft Toolchain] H1[webpack-patch.json]:::heft --> H2[webpack-patch/*.js]:::heft H2 --> H3[DefinePlugin]:::heft end G3 --> R[process.env.VAR works] H3 --> R linkStyle 0,1,2,3,4 stroke-width:2px,stroke:#0aa8a7

The key differences:

Aspect Gulp Heft
Config file gulpfile.js webpack-patch.json
Webpack hook build.configureWebpack Patch function
Task runner Gulp Heft
Orchestration Sequential Parallel phases

The Four Approaches

I show here four ways to implement environment variables with Heft.

Approach 1: Pre-Build Script (Type-Safe)

Generate TypeScript constants before each build.

This approach provides full type safety and doesn’t require any webpack configuration knowledge. Choose this if type safety is your priority.

// scripts/generate-env-config.js
const fs = require('fs');
const dotenv = require('dotenv');

const NODE_ENV = process.env.NODE_ENV || 'dev';
const envConfig = dotenv.parse(fs.readFileSync(`./env/${NODE_ENV}.env`));

const spfxVars = Object.entries(envConfig)
  .filter(([key]) => key.startsWith('SPFX_'))
  .map(([key, value]) => `  ${key}: '${value}'`)
  .join(',\n');

const output = `// AUTO-GENERATED - Do not edit
export const ENV_CONFIG = {
${spfxVars}
} as const;
`;

fs.writeFileSync('src/utils/envConfig.generated.ts', output);

Usage in your component:

import { ENV_CONFIG } from '../utils/envConfig.generated';

// Full type safety and autocomplete!
const apiUrl = ENV_CONFIG.SPFX_API_BASE_URL;
const timeout = ENV_CONFIG.SPFX_API_TIMEOUT;

package.json scripts:

{
  "scripts": {
    "generate-env": "node scripts/generate-env-config.js",
    "start:dev": "cross-env NODE_ENV=dev npm run generate-env && heft start",
    "start:prod": "cross-env NODE_ENV=prod npm run generate-env && heft start"
  }
}

Use Microsoft’s official Webpack Patch Plugin.

This is my recommended approach - it’s the Microsoft-supported way to customize webpack in SPFx 1.22+ and keeps the familiar process.env.* pattern.

// config/webpack-patch.json
{
  "$schema": "https://developer.microsoft.com/json-schemas/spfx-build/webpack-patch.schema.json",
  "patchFiles": ["./config/webpack-patch/env-inject.js"]
}
// config/webpack-patch/env-inject.js
// IMPORTANT: Use webpack from the SPFx rig to ensure compatibility!
// Using require('webpack') directly loads a different webpack instance
// and causes "tap" errors in SPFx 1.22+
const webpack = require('@microsoft/spfx-web-build-rig/node_modules/webpack');
const dotenv = require('dotenv');
const fs = require('fs');
const path = require('path');

module.exports = function(webpackConfig) {
  const NODE_ENV = process.env.NODE_ENV || 'dev';
  const envPath = path.resolve(__dirname, '../../env', `${NODE_ENV}.env`);

  if (!fs.existsSync(envPath)) {
    console.warn(`Warning: ${envPath} not found`);
    return webpackConfig;
  }

  const envConfig = dotenv.parse(fs.readFileSync(envPath));
  const definitions = {};

  Object.entries(envConfig)
    .filter(([key]) => key.startsWith('SPFX_'))
    .forEach(([key, value]) => {
      definitions[`process.env.${key}`] = JSON.stringify(value);
    });

  webpackConfig.plugins.push(new webpack.DefinePlugin(definitions));
  return webpackConfig;
};

Always import webpack from @microsoft/spfx-web-build-rig/node_modules/webpack in SPFx 1.22+. Using require('webpack') directly will load a different webpack instance and cause cryptic “Cannot read properties of undefined (reading ’tap’)” errors.

Usage remains the same as the old gulp approach:

const apiUrl = process.env.SPFX_API_BASE_URL;

Approach 3: dotenv-webpack (Community)

Use the popular dotenv-webpack package.

This is the industry-standard approach used in React and Node.js projects.

The dotenv-webpack package may have compatibility issues with SPFx 1.22+ Heft builds because it internally uses require('webpack'). For SPFx 1.22+, Approach 2 (DefinePlugin with proper webpack import) is more reliable.

npm install dotenv-webpack --save-dev
// config/webpack-patch/dotenv-inject.js
const Dotenv = require('dotenv-webpack');
const path = require('path');

module.exports = function(webpackConfig) {
  const NODE_ENV = process.env.NODE_ENV || 'dev';

  webpackConfig.plugins.push(new Dotenv({
    path: path.resolve(__dirname, `../../env/${NODE_ENV}.env`),
    safe: false,
    systemvars: false
  }));

  return webpackConfig;
};

Approach 4: Heft Run Script (Advanced)

Integrate directly into Heft’s build phases.

This is the most powerful approach but also the most complex. Use it for enterprise scenarios requiring full control over the build pipeline.

// heft.json
{
  "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json",
  "extends": "./node_modules/@microsoft/spfx-heft-plugins/heft-defaults.json",
  "phasesByName": {
    "build": {
      "tasksByName": {
        "generate-env-config": {
          "taskPlugin": {
            "pluginPackage": "@rushstack/heft",
            "pluginName": "run-script-plugin",
            "options": {
              "scriptPath": "./scripts/heft-env-task.js"
            }
          }
        }
      }
    }
  }
}

Comparison Matrix

Approach Type Safety Complexity MS Support Best For
Pre-Build Script Full Simple Neutral TypeScript projects
Webpack Patch None Medium Official Most projects
dotenv-webpack None Medium Community* React/Node familiarity
Heft Run Script Full Complex Official Enterprise builds

Recommended - Microsoft’s official approach

*dotenv-webpack may have compatibility issues with SPFx 1.22+ due to webpack instance conflicts.

Full Code Repository

I have created a comprehensive sample: one unified SPFx solution containing 4 web parts, each demonstrating a different approach. This allows you to compare all methods side by side on a single SharePoint page.

View on GitHub: spfx-env-config-heft

The sample includes:

  • 1 SPFx solution with 4 web parts (one per approach)
  • Shared React component displaying environment variables
  • Complete configuration files for all approaches
  • Dev and Prod environment examples

Conclusion

Heft is the new build system for SPFx, but migration is not mandatory yet. Here’s my recommendation:

If you have existing gulp-based projects: You can keep them running without changes. Consider migrating when you have time or when starting new features.

If you’re starting a new project or choosing to migrate:

  1. For most projects: Use Approach 2 (Webpack Patch) - it’s Microsoft’s official approach and keeps the familiar process.env.* pattern
  2. For type safety: Use Approach 1 (Pre-Build Script) for full TypeScript type safety with IDE autocomplete
  3. For enterprise builds: Consider Approach 4 (Heft Run Script) for maximum control over the build pipeline

Heft is here to stay. Understanding these approaches will help you implement environment-specific configuration in your SPFx solutions, whether you migrate now or later.


Have questions or found an issue? Open an issue on GitHub .