diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index c7fb18d0553..068c26bf13f 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -18,6 +18,7 @@ jobs: build: name: Build - macOS (${{ matrix.arch }}) runs-on: macos-latest + environment: production strategy: fail-fast: false @@ -54,6 +55,9 @@ jobs: - name: Compile app with electron-vite working-directory: apps/desktop + env: + NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }} + NEXT_PUBLIC_POSTHOG_HOST: ${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }} run: bun run compile:app # Build the Electron app for macOS diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index 8644e787fae..3c982a6f925 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -106,8 +106,17 @@ export default defineConfig({ renderer: { define: { + // Core env vars - Vite replaces these at build time "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV), "process.platform": JSON.stringify(process.platform), + // API URLs - available in renderer if needed + "process.env.NEXT_PUBLIC_API_URL": JSON.stringify( + process.env.NEXT_PUBLIC_API_URL || "https://api.superset.sh", + ), + "process.env.NEXT_PUBLIC_WEB_URL": JSON.stringify( + process.env.NEXT_PUBLIC_WEB_URL || "https://app.superset.sh", + ), + // Custom env vars "import.meta.env.DEV_SERVER_PORT": JSON.stringify(DEV_SERVER_PORT), "import.meta.env.NEXT_PUBLIC_POSTHOG_KEY": JSON.stringify( process.env.NEXT_PUBLIC_POSTHOG_KEY, diff --git a/apps/desktop/src/lib/electron-app/factories/app/setup.ts b/apps/desktop/src/lib/electron-app/factories/app/setup.ts index 28112c40ffa..579fa340392 100644 --- a/apps/desktop/src/lib/electron-app/factories/app/setup.ts +++ b/apps/desktop/src/lib/electron-app/factories/app/setup.ts @@ -4,9 +4,9 @@ import { installExtension, REACT_DEVELOPER_TOOLS, } from "electron-extension-installer"; +import { env } from "main/env.main"; import { PLATFORM } from "shared/constants"; import { makeAppId } from "shared/utils"; -import { env } from "../../../../env"; import { ignoreConsoleWarnings } from "../../utils/ignore-console-warnings"; ignoreConsoleWarnings(["Manifest version 2 is deprecated"]); diff --git a/apps/desktop/src/lib/electron-router-dom.ts b/apps/desktop/src/lib/electron-router-dom.ts index 6af34dbdb51..cc2115093a2 100644 --- a/apps/desktop/src/lib/electron-router-dom.ts +++ b/apps/desktop/src/lib/electron-router-dom.ts @@ -1,7 +1,7 @@ import type { BrowserWindow } from "electron"; import { createElectronRouter } from "electron-router-dom"; import { PORTS } from "shared/constants"; -import { env } from "../env"; +import { env } from "shared/env.shared"; const electronRouter = createElectronRouter({ port: PORTS.VITE_DEV_SERVER, diff --git a/apps/desktop/src/env.ts b/apps/desktop/src/main/env.main.ts similarity index 56% rename from apps/desktop/src/env.ts rename to apps/desktop/src/main/env.main.ts index 7f9a3dc46c5..207e32d81f4 100644 --- a/apps/desktop/src/env.ts +++ b/apps/desktop/src/main/env.main.ts @@ -1,3 +1,11 @@ +/** + * Environment variables for the MAIN PROCESS (Node.js context). + * + * This file uses t3-env with process.env which works at runtime in Node.js. + * Only import this file in src/main/ code - never in renderer or shared code. + * + * For renderer process env vars, use src/renderer/env.renderer.ts instead. + */ import { createEnv } from "@t3-oss/env-core"; import { z } from "zod/v4"; @@ -12,11 +20,10 @@ export const env = createEnv({ runtimeEnv: { ...process.env, - // Vite's define replaces this at build time, ensuring correct env in packaged apps NODE_ENV: process.env.NODE_ENV, }, emptyStringAsUndefined: true, - // Electron runs in a trusted environment - treat renderer as server context + // Main process runs in trusted Node.js environment isServer: true, }); diff --git a/apps/desktop/src/main/lib/api-client.ts b/apps/desktop/src/main/lib/api-client.ts index c3c6aa9ea2a..c8b24b647be 100644 --- a/apps/desktop/src/main/lib/api-client.ts +++ b/apps/desktop/src/main/lib/api-client.ts @@ -1,7 +1,7 @@ import type { AppRouter } from "@superset/trpc"; import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import { env } from "main/env.main"; import superjson from "superjson"; -import { env } from "../../env"; import { authService } from "./auth"; /** diff --git a/apps/desktop/src/main/lib/auth/auth-service.ts b/apps/desktop/src/main/lib/auth/auth-service.ts index 274c498ba06..cb850aac41c 100644 --- a/apps/desktop/src/main/lib/auth/auth-service.ts +++ b/apps/desktop/src/main/lib/auth/auth-service.ts @@ -1,8 +1,8 @@ import { EventEmitter } from "node:events"; import { TOKEN_CONFIG } from "@superset/shared/constants"; import { type BrowserWindow, shell } from "electron"; +import { env } from "main/env.main"; import type { AuthProvider, AuthSession, SignInResult } from "shared/auth"; -import { env } from "../../../env"; import { pkceStore } from "./pkce"; import { tokenStorage } from "./token-storage"; diff --git a/apps/desktop/src/main/lib/auth/deep-link-handler.ts b/apps/desktop/src/main/lib/auth/deep-link-handler.ts index 3af0368fa7b..08f84517cab 100644 --- a/apps/desktop/src/main/lib/auth/deep-link-handler.ts +++ b/apps/desktop/src/main/lib/auth/deep-link-handler.ts @@ -1,6 +1,6 @@ +import { env } from "main/env.main"; import type { AuthSession } from "shared/auth"; import { PROTOCOL_SCHEMES } from "shared/constants"; -import { env } from "../../../env"; import { pkceStore } from "./pkce"; /** diff --git a/apps/desktop/src/main/lib/auto-updater.ts b/apps/desktop/src/main/lib/auto-updater.ts index a77ac78487b..ddd2867e126 100644 --- a/apps/desktop/src/main/lib/auto-updater.ts +++ b/apps/desktop/src/main/lib/auto-updater.ts @@ -1,7 +1,7 @@ import { app, type BrowserWindow, dialog } from "electron"; import { autoUpdater } from "electron-updater"; +import { env } from "main/env.main"; import { PLATFORM } from "shared/constants"; -import { env } from "../../env"; const UPDATE_CHECK_INTERVAL_MS = 1000 * 60 * 60 * 4; // 4 hours const UPDATE_FEED_URL = diff --git a/apps/desktop/src/main/lib/sound-paths.ts b/apps/desktop/src/main/lib/sound-paths.ts index e914829d1aa..5ae2218dfd2 100644 --- a/apps/desktop/src/main/lib/sound-paths.ts +++ b/apps/desktop/src/main/lib/sound-paths.ts @@ -1,7 +1,7 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; import { app } from "electron"; -import { env } from "../../env"; +import { env } from "main/env.main"; /** * Gets the path to a ringtone sound file. diff --git a/apps/desktop/src/main/lib/terminal-history.ts b/apps/desktop/src/main/lib/terminal-history.ts index 69e74a6a28c..4ad95e5aace 100644 --- a/apps/desktop/src/main/lib/terminal-history.ts +++ b/apps/desktop/src/main/lib/terminal-history.ts @@ -1,7 +1,7 @@ import { createWriteStream, promises as fs, type WriteStream } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { env } from "../../env"; +import { env } from "main/env.main"; import { SUPERSET_HOME_DIR } from "./app-environment"; export interface SessionMetadata { diff --git a/apps/desktop/src/renderer/env.renderer.ts b/apps/desktop/src/renderer/env.renderer.ts new file mode 100644 index 00000000000..e2fe439c95f --- /dev/null +++ b/apps/desktop/src/renderer/env.renderer.ts @@ -0,0 +1,43 @@ +/** + * Environment variables for the RENDERER PROCESS (browser context). + * + * These values are injected at BUILD TIME by Vite's `define` in electron.vite.config.ts. + * They are NOT read from process.env at runtime - Vite replaces the references with + * literal strings during compilation. + * + * Only import this file in src/renderer/ code - never in main or shared code. + * + * For main process env vars, use src/main/env.main.ts instead. + */ +import { z } from "zod/v4"; + +const envSchema = z.object({ + NODE_ENV: z + .enum(["development", "production", "test"]) + .default("development"), + NEXT_PUBLIC_API_URL: z.url().default("https://api.superset.sh"), + NEXT_PUBLIC_WEB_URL: z.url().default("https://app.superset.sh"), + NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), + NEXT_PUBLIC_POSTHOG_HOST: z.string().default("https://us.i.posthog.com"), +}); + +/** + * Build-time environment variables. + * + * Vite replaces these process.env.* and import.meta.env.* references at build time. + * The values are baked into the bundle as string literals. + */ +const rawEnv = { + // These are replaced by Vite's define at build time + NODE_ENV: process.env.NODE_ENV, + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, + NEXT_PUBLIC_WEB_URL: process.env.NEXT_PUBLIC_WEB_URL, + NEXT_PUBLIC_POSTHOG_KEY: import.meta.env.NEXT_PUBLIC_POSTHOG_KEY as + | string + | undefined, + NEXT_PUBLIC_POSTHOG_HOST: import.meta.env.NEXT_PUBLIC_POSTHOG_HOST as + | string + | undefined, +}; + +export const env = envSchema.parse(rawEnv); diff --git a/apps/desktop/src/renderer/lib/posthog.ts b/apps/desktop/src/renderer/lib/posthog.ts index a1b604786fe..9666d86dafa 100644 --- a/apps/desktop/src/renderer/lib/posthog.ts +++ b/apps/desktop/src/renderer/lib/posthog.ts @@ -1,18 +1,14 @@ import posthog from "posthog-js/dist/module.full.no-external"; - -const POSTHOG_KEY = import.meta.env.NEXT_PUBLIC_POSTHOG_KEY as - | string - | undefined; +import { env } from "../env.renderer"; export function initPostHog() { - if (!POSTHOG_KEY) { + if (!env.NEXT_PUBLIC_POSTHOG_KEY) { console.log("[posthog] No key configured, skipping"); return; } - posthog.init(POSTHOG_KEY, { - api_host: "https://us.i.posthog.com", - ui_host: "https://us.posthog.com", + posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, { + api_host: env.NEXT_PUBLIC_POSTHOG_HOST, defaults: "2025-11-30", capture_pageview: false, capture_pageleave: false, diff --git a/apps/desktop/src/shared/constants.ts b/apps/desktop/src/shared/constants.ts index 66f2cc98fd4..0d6e6de8aed 100644 --- a/apps/desktop/src/shared/constants.ts +++ b/apps/desktop/src/shared/constants.ts @@ -1,4 +1,4 @@ -import { env } from "../env"; +import { env } from "./env.shared"; export const PLATFORM = { IS_MAC: process.platform === "darwin", diff --git a/apps/desktop/src/shared/env.shared.ts b/apps/desktop/src/shared/env.shared.ts new file mode 100644 index 00000000000..bf9b9448631 --- /dev/null +++ b/apps/desktop/src/shared/env.shared.ts @@ -0,0 +1,30 @@ +/** + * Environment variables safe for SHARED CODE (main + renderer). + * + * This file only accesses individual process.env properties that are: + * 1. Defined in Vite's `define` block (replaced at build time for renderer) + * 2. Available in main process via actual process.env + * + * DO NOT spread ...process.env here - that only works in main process. + * + * For main-process-only env vars (API URLs, etc.), use src/main/env.main.ts + * For renderer-only env vars (PostHog, etc.), use src/renderer/env.renderer.ts + */ +import { z } from "zod/v4"; + +const envSchema = z.object({ + NODE_ENV: z + .enum(["development", "production", "test"]) + .default("development"), +}); + +/** + * Shared environment variables. + * + * These work in both main and renderer because Vite's `define` replaces + * process.env.NODE_ENV at build time for renderer, while main process + * reads the actual value at runtime. + */ +export const env = envSchema.parse({ + NODE_ENV: process.env.NODE_ENV, +});