diff --git a/web/.storybook/main.js b/web/.storybook/main.js
index ac6c5eb840..3e8cfdf5be 100644
--- a/web/.storybook/main.js
+++ b/web/.storybook/main.js
@@ -3,13 +3,11 @@
  * @import { StorybookConfig } from "@storybook/web-components-vite";
  * @import { InlineConfig, Plugin } from "vite";
  */
-import { cwd } from "process";
+import { createBundleDefinitions } from "@goauthentik/web/scripts/esbuild/environment";
 import postcssLit from "rollup-plugin-postcss-lit";
 import tsconfigPaths from "vite-tsconfig-paths";
 
-const NODE_ENV = process.env.NODE_ENV || "development";
-
-const CSSImportPattern = /import [\w\$]+ from .+\.(css)/g;
+const CSSImportPattern = /import [\w$]+ from .+\.(css)/g;
 const JavaScriptFilePattern = /\.m?(js|ts|tsx)$/;
 
 /**
@@ -54,11 +52,7 @@ const config = {
          */
         const mergedConfig = {
             ...config,
-            define: {
-                "process.env.NODE_ENV": JSON.stringify(NODE_ENV),
-                "process.env.CWD": JSON.stringify(cwd()),
-                "process.env.AK_API_BASE_PATH": JSON.stringify(process.env.AK_API_BASE_PATH || ""),
-            },
+            define: createBundleDefinitions(),
             plugins: [inlineCSSPlugin, ...plugins, postcssLit(), tsconfigPaths()],
         };
 
diff --git a/web/packages/esbuild-plugin-live-reload/client/ESBuildObserver.js b/web/packages/esbuild-plugin-live-reload/client/ESBuildObserver.js
index 2f8ebe3ce2..8a6b0a3990 100644
--- a/web/packages/esbuild-plugin-live-reload/client/ESBuildObserver.js
+++ b/web/packages/esbuild-plugin-live-reload/client/ESBuildObserver.js
@@ -21,7 +21,7 @@ const log = console.debug.bind(console, logPrefix);
  * ESBuild may tree-shake it out of production builds.
  *
  * ```ts
- * if (process.env.NODE_ENV === "development") {
+ * if (import.meta.env.NODE_ENV=== "development") {
  *   await import("@goauthentik/esbuild-plugin-live-reload/client")
  *     .catch(() => console.warn("Failed to import watcher"))
  * }
diff --git a/web/packages/esbuild-plugin-live-reload/client/types.d.ts b/web/packages/esbuild-plugin-live-reload/client/types.d.ts
index c446181716..370d24a148 100644
--- a/web/packages/esbuild-plugin-live-reload/client/types.d.ts
+++ b/web/packages/esbuild-plugin-live-reload/client/types.d.ts
@@ -4,15 +4,20 @@
 
 export {};
 declare global {
+    /**
+     * Environment variables injected by ESBuild.
+     */
+    interface ImportMetaEnv {
+        /**
+         * The injected watcher URL for ESBuild.
+         * This is used for live reloading in development mode.
+         *
+         * @format url
+         */
+        readonly ESBUILD_WATCHER_URL?: string;
+    }
+
     interface ImportMeta {
-        readonly env: {
-            /**
-             * The injected watcher URL for ESBuild.
-             * This is used for live reloading in development mode.
-             *
-             * @format url
-             */
-            ESBUILD_WATCHER_URL: string;
-        };
+        readonly env: ImportMetaEnv;
     }
 }
diff --git a/web/packages/monorepo/constants.js b/web/packages/monorepo/constants.js
deleted file mode 100644
index 6abf55e8a5..0000000000
--- a/web/packages/monorepo/constants.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * @file Constants for JavaScript and TypeScript files.
- */
-
-/// 
-
-/**
- * The current Node.js environment, defaulting to "development" when not set.
- *
- * Note, this should only be used during the build process.
- *
- * If you need to check the environment at runtime, use `process.env.NODE_ENV` to
- * ensure that module tree-shaking works correctly.
- *
- */
-export const NodeEnvironment = process.env.NODE_ENV || "development";
diff --git a/web/packages/monorepo/build.js b/web/packages/monorepo/environment.js
similarity index 56%
rename from web/packages/monorepo/build.js
rename to web/packages/monorepo/environment.js
index 343d522cd6..062c1a4f4c 100644
--- a/web/packages/monorepo/build.js
+++ b/web/packages/monorepo/environment.js
@@ -1,6 +1,20 @@
 /**
- * @file Utility functions for building and copying files.
+ * @file Utility functions for working with environment variables.
  */
+/// 
+
+//#region Constants
+
+/**
+ * The current Node.js environment, defaulting to "development" when not set.
+ *
+ * Note, this should only be used during the build process.
+ *
+ * If you need to check the environment at runtime, use `process.env.NODE_ENV` to
+ * ensure that module tree-shaking works correctly.
+ *
+ */
+export const NodeEnvironment = process.env.NODE_ENV || "development";
 
 /**
  * A source environment variable, which can be a string, number, boolean, null, or undefined.
@@ -14,19 +28,26 @@
  * @typedef {T extends string ? `"${T}"` : T} JSONify
  */
 
+//#endregion
+
+//#region Utilities
+
 /**
  * Given an object of environment variables, returns a new object with the same keys and values, but
  * with the values serialized as strings.
  *
  * @template {Record} EnvRecord
- * @template {string} [Prefix='process.env.']
+ * @template {string} [Prefix='import.meta.env.']
  *
  * @param {EnvRecord} input
- * @param {Prefix} [prefix='process.env.']
+ * @param {Prefix} [prefix='import.meta.env.']
  *
  * @returns {{[K in keyof EnvRecord as `${Prefix}${K}`]: JSONify}}
  */
-export function serializeEnvironmentVars(input, prefix = /** @type {Prefix} */ ("process.env.")) {
+export function serializeEnvironmentVars(
+    input,
+    prefix = /** @type {Prefix} */ ("import.meta.env."),
+) {
     /**
      * @type {Record}
      */
@@ -40,3 +61,5 @@ export function serializeEnvironmentVars(input, prefix = /** @type {Prefix} */ (
 
     return /** @type {any} */ (env);
 }
+
+//#endregion
diff --git a/web/packages/monorepo/index.js b/web/packages/monorepo/index.js
index 284d2b5d2b..5eee5d4cef 100644
--- a/web/packages/monorepo/index.js
+++ b/web/packages/monorepo/index.js
@@ -1,7 +1,6 @@
 /// 
 
 export * from "./paths.js";
-export * from "./constants.js";
-export * from "./build.js";
+export * from "./environment.js";
 export * from "./version.js";
 export * from "./scripting.js";
diff --git a/web/scripts/build-web.mjs b/web/scripts/build-web.mjs
index 3b1f329e23..67394ec32b 100644
--- a/web/scripts/build-web.mjs
+++ b/web/scripts/build-web.mjs
@@ -1,3 +1,4 @@
+/// 
 /**
  * @file ESBuild script for building the authentik web UI.
  *
@@ -9,7 +10,6 @@ import {
     NodeEnvironment,
     readBuildIdentifier,
     resolvePackage,
-    serializeEnvironmentVars,
 } from "@goauthentik/monorepo";
 import { DistDirectory, DistDirectoryName, EntryPoint, PackageRoot } from "@goauthentik/web/paths";
 import { deepmerge } from "deepmerge-ts";
@@ -20,15 +20,10 @@ import * as fs from "node:fs/promises";
 import * as path from "node:path";
 
 import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs";
+import { createBundleDefinitions } from "./esbuild/environment.mjs";
 
 const logPrefix = "[Build]";
 
-const definitions = serializeEnvironmentVars({
-    NODE_ENV: NodeEnvironment,
-    CWD: process.cwd(),
-    AK_API_BASE_PATH: process.env.AK_API_BASE_PATH,
-});
-
 const patternflyPath = resolvePackage("@patternfly/patternfly");
 
 /**
@@ -86,7 +81,7 @@ const BASE_ESBUILD_OPTIONS = {
             root: MonoRepoRoot,
         }),
     ],
-    define: definitions,
+    define: createBundleDefinitions(),
     format: "esm",
     logOverride: {
         /**
diff --git a/web/scripts/esbuild/environment.mjs b/web/scripts/esbuild/environment.mjs
new file mode 100644
index 0000000000..2af7afcaba
--- /dev/null
+++ b/web/scripts/esbuild/environment.mjs
@@ -0,0 +1,29 @@
+/**
+ * @file ESBuild environment utilities.
+ */
+import { AuthentikVersion, NodeEnvironment, serializeEnvironmentVars } from "@goauthentik/monorepo";
+
+/**
+ * Creates a mapping of environment variables to their respective runtime constants.
+ */
+export function createBundleDefinitions() {
+    const SerializedNodeEnvironment = /** @type {`"development"` | `"production"`} */ (
+        JSON.stringify(NodeEnvironment)
+    );
+
+    /**
+     * @satisfies {Record}
+     */
+    const envRecord = {
+        AK_VERSION: AuthentikVersion,
+        AK_API_BASE_PATH: process.env.AK_API_BASE_PATH ?? "",
+    };
+
+    return {
+        ...serializeEnvironmentVars(envRecord),
+        // We need to explicitly set this for NPM packages that use `process`
+        // to determine their environment.
+        "process.env.NODE_ENV": SerializedNodeEnvironment,
+        "import.meta.env.NODE_ENV": SerializedNodeEnvironment,
+    };
+}
diff --git a/web/src/admin/AdminInterface/index.entrypoint.ts b/web/src/admin/AdminInterface/index.entrypoint.ts
index 9b6480078e..2797ddb6e1 100644
--- a/web/src/admin/AdminInterface/index.entrypoint.ts
+++ b/web/src/admin/AdminInterface/index.entrypoint.ts
@@ -43,7 +43,7 @@ import {
     renderSidebarItems,
 } from "./AdminSidebar.js";
 
-if (process.env.NODE_ENV === "development") {
+if (import.meta.env.NODE_ENV === "development") {
     await import("@goauthentik/esbuild-plugin-live-reload/client");
 }
 
diff --git a/web/src/common/stylesheets.ts b/web/src/common/stylesheets.ts
index 84a2459a56..094f4ec0bc 100644
--- a/web/src/common/stylesheets.ts
+++ b/web/src/common/stylesheets.ts
@@ -223,7 +223,7 @@ export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleS
     };
 }
 
-if (process.env.NODE_ENV === "development") {
+if (import.meta.env.NODE_ENV === "development") {
     Object.assign(window, {
         inspectStyleSheetTree,
         serializeStyleSheet,
diff --git a/web/src/flow/index.entrypoint.ts b/web/src/flow/index.entrypoint.ts
index 9f5cfff49c..759efa4b9f 100644
--- a/web/src/flow/index.entrypoint.ts
+++ b/web/src/flow/index.entrypoint.ts
@@ -14,6 +14,6 @@ import "@goauthentik/flow/stages/password/PasswordStage";
 
 // end of stage import
 
-if (process.env.NODE_ENV === "development") {
+if (import.meta.env.NODE_ENV === "development") {
     await import("@goauthentik/esbuild-plugin-live-reload/client");
 }
diff --git a/web/src/user/index.entrypoint.ts b/web/src/user/index.entrypoint.ts
index 5bc8721fe4..d295f8ad6a 100644
--- a/web/src/user/index.entrypoint.ts
+++ b/web/src/user/index.entrypoint.ts
@@ -43,7 +43,7 @@ import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
 
 import { CurrentBrand, EventsApi, SessionUser } from "@goauthentik/api";
 
-if (process.env.NODE_ENV === "development") {
+if (import.meta.env.NODE_ENV === "development") {
     await import("@goauthentik/esbuild-plugin-live-reload/client");
 }
 
diff --git a/web/types/esbuild.d.ts b/web/types/esbuild.d.ts
new file mode 100644
index 0000000000..c227f2ab67
--- /dev/null
+++ b/web/types/esbuild.d.ts
@@ -0,0 +1,39 @@
+/**
+ * @file Import meta environment variables available via ESBuild.
+ */
+
+export {};
+declare global {
+    interface ESBuildImportEnv {
+        /**
+         * The authentik version injected by ESBuild during build time.
+         *
+         * @format semver
+         */
+        readonly AK_VERSION: string;
+
+        /**
+         * @todo Determine where this is used and if it is needed,
+         * give it a better name.
+         * @deprecated
+         */
+        readonly AK_API_BASE_PATH: string;
+    }
+
+    type ESBuildImportEnvKey = keyof ESBuildImportEnv;
+
+    /**
+     * Environment variables injected by ESBuild.
+     */
+    interface ImportMetaEnv extends ESBuildImportEnv {
+        /**
+         * An environment variable used to determine
+         * whether Node.js is running in production mode.
+         */
+        readonly NODE_ENV: "development" | "production";
+    }
+
+    interface ImportMeta {
+        readonly env: ImportMetaEnv;
+    }
+}
diff --git a/web/types/global.d.ts b/web/types/node.d.ts
similarity index 70%
rename from web/types/global.d.ts
rename to web/types/node.d.ts
index 4418fb7e9f..9a9d8c56b8 100644
--- a/web/types/global.d.ts
+++ b/web/types/node.d.ts
@@ -1,5 +1,5 @@
 /**
- * @file Environment variables available via ESBuild.
+ * @file Global variables provided by Node.js
  */
 
 declare module "module" {
@@ -14,8 +14,8 @@ declare module "module" {
          * const relativeDirname = dirname(fileURLToPath(import.meta.url));
          * ```
          */
-        // eslint-disable-next-line no-var
-        var __dirname: string;
+
+        const __dirname: string;
     }
 }
 
@@ -23,13 +23,16 @@ declare module "process" {
     global {
         namespace NodeJS {
             interface ProcessEnv {
-                CWD: string;
+                /**
+                 * Node environment, if any.
+                 */
+                readonly NODE_ENV?: "development" | "production";
                 /**
                  * @todo Determine where this is used and if it is needed,
                  * give it a better name.
                  * @deprecated
                  */
-                AK_API_BASE_PATH: string;
+                readonly AK_API_BASE_PATH?: string;
             }
         }
     }
diff --git a/web/wdio.conf.ts b/web/wdio.conf.ts
index 8b41fdcd79..22bfbab91c 100644
--- a/web/wdio.conf.ts
+++ b/web/wdio.conf.ts
@@ -1,17 +1,15 @@
-import replace from "@rollup/plugin-replace";
+/// 
 import { browser } from "@wdio/globals";
 import type { Options } from "@wdio/types";
-import path from "path";
-import { cwd } from "process";
-import { fileURLToPath } from "url";
-import type { UserConfig } from "vite";
-import litCss from "vite-plugin-lit-css";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { createBundleDefinitions } from "scripts/esbuild/environment.mjs";
+import type { InlineConfig } from "vite";
+import litCSS from "vite-plugin-lit-css";
 import tsconfigPaths from "vite-tsconfig-paths";
 
 const __dirname = fileURLToPath(new URL(".", import.meta.url));
 
-const isProdBuild = process.env.NODE_ENV === "production";
-const apiBasePath = process.env.AK_API_BASE_PATH || "";
 const runHeadless = process.env.CI !== undefined;
 
 const testSafari = process.env.WDIO_TEST_SAFARI !== undefined;
@@ -72,21 +70,9 @@ export const config: Options.Testrunner = {
     runner: [
         "browser",
         {
-            viteConfig: (userConfig: UserConfig = { plugins: [] }) => ({
-                ...userConfig,
-                plugins: [
-                    litCss(),
-                    replace({
-                        "process.env.NODE_ENV": JSON.stringify(
-                            isProdBuild ? "production" : "development",
-                        ),
-                        "process.env.CWD": JSON.stringify(cwd()),
-                        "process.env.AK_API_BASE_PATH": JSON.stringify(apiBasePath),
-                        "preventAssignment": true,
-                    }),
-                    ...(userConfig?.plugins ?? []),
-                    tsconfigPaths(),
-                ],
+            viteConfig: {
+                define: createBundleDefinitions(),
+                plugins: [litCSS(), tsconfigPaths()],
                 resolve: {
                     alias: {
                         "@goauthentik/admin": path.resolve(__dirname, "src/admin"),
@@ -101,7 +87,7 @@ export const config: Options.Testrunner = {
                         "@goauthentik/user": path.resolve(__dirname, "src/user"),
                     },
                 },
-            }),
+            } satisfies InlineConfig,
         },
     ],