From 04130c0fe2fdcf44add63d60c7909e74f722082e Mon Sep 17 00:00:00 2001
From: Kiet Ho
Date: Fri, 15 May 2026 18:25:47 -0700
Subject: [PATCH 01/14] feat: local dev without third-party credentials
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A fresh clone can now boot the full dev stack (web + api + desktop +
electric + electric-proxy + caddy) against a local Postgres in Docker,
with no Neon, OAuth, Stripe, Resend, or other cloud-service keys required.
Key changes:
- packages/db/src/client.ts: driver swap to drizzle-orm/node-postgres for
non-Neon DATABASE_URLs (@neondatabase/serverless only speaks Neon's
HTTP/WS protocol, not vanilla Postgres TCP).
- packages/auth/src/server.ts: emailAndPassword auth enabled + autoSignIn.
- apps/desktop/src/main/lib/dev-auto-sign-in.ts: during app.whenReady(),
if SKIP_ENV_VALIDATION=1 and no token on disk, auto-signs-in as
admin@local.test and persists the token via the existing saveToken().
Renderer hydrates like a real OAuth user — no special renderer code.
- 17 renderer files: flip MOCK_ORG_ID priority — prefer real session's
activeOrganizationId, fall back to MOCK_ORG_ID only without a session.
Fixes the "Host service not available" toast that fired because the
renderer was looking up host-service connections by the fake org id
while host-service spawned for the real admin@local.test org.
- Lazy-init guards for Stripe (Proxy), Resend (Proxy), and PostHog (no-op
surface) so they don't crash module load when keys are missing.
- apps/web: dev-only email/password form on sign-in/sign-up, gated on
NODE_ENV !== "production".
- packages/db: bun db:seed:dev script to create admin@local.test;
refuses prod + non-localhost DATABASE_URL.
- turbo.jsonc: SKIP_ENV_VALIDATION added to globalPassThroughEnv.
- docs/LOCAL_DEVELOPMENT.md: contributor-facing setup guide.
- plans/20260515-oss-local-setup.md: full record of what shipped + what's
deferred (env vars .optional(), docker-compose.dev.yml, bun setup
orchestrator, CI fresh-clone smoke test).
Verified end-to-end via Chrome DevTools Protocol (port 9333) — the
renderer's LocalHostServiceContext.activeHostUrl resolves to a live
host-service URL, so the import gate is open.
---
README.md | 49 ++----
apps/api/src/lib/analytics.ts | 33 +++-
apps/desktop/src/main/index.ts | 13 ++
apps/desktop/src/main/lib/dev-auto-sign-in.ts | 96 +++++++++++
.../OpenInWorkspaceV2/OpenInWorkspaceV2.tsx | 6 +-
.../RunInWorkspacePopoverV2.tsx | 6 +-
.../RunIssuesInWorkspacePopover.tsx | 6 +-
.../useAccessibleV2Workspaces.ts | 6 +-
.../useWorkspaceHostOptions.ts | 6 +-
.../DashboardNewWorkspaceModalContent.tsx | 6 +-
.../V1ImportModal/V1ImportModal.tsx | 6 +-
.../renderer/routes/_authenticated/layout.tsx | 6 +-
.../CollectionsProvider.tsx | 6 +-
.../LocalHostServiceProvider.tsx | 6 +-
.../HostsSettingsSidebar.tsx | 6 +-
.../_authenticated/settings/hosts/page.tsx | 6 +-
.../settings/projects/$projectId/page.tsx | 6 +-
.../ProjectsSettingsSidebar.tsx | 6 +-
.../_authenticated/settings/projects/page.tsx | 6 +-
.../_authenticated/setup/project/page.tsx | 6 +-
.../src/renderer/routes/sign-in/page.tsx | 4 +-
.../components/DevAuthForm/DevAuthForm.tsx | 103 ++++++++++++
.../(auth)/components/DevAuthForm/index.ts | 1 +
.../(auth)/sign-in/[[...sign-in]]/page.tsx | 4 +
.../(auth)/sign-up/[[...sign-up]]/page.tsx | 6 +
bun.lock | 6 +-
docs/LOCAL_DEVELOPMENT.md | 154 ++++++++++++++++++
package.json | 1 +
packages/auth/src/lib/resend.ts | 54 +++++-
packages/auth/src/server.ts | 4 +
packages/auth/src/stripe.ts | 16 +-
packages/db/package.json | 3 +
packages/db/src/client.ts | 48 ++++--
packages/db/src/seed-dev.ts | 73 +++++++++
packages/trpc/src/lib/analytics.ts | 32 +++-
packages/trpc/src/router/support/support.ts | 19 ++-
plans/20260515-oss-local-setup.md | 119 ++++++++++++++
turbo.jsonc | 3 +-
38 files changed, 822 insertions(+), 115 deletions(-)
create mode 100644 apps/desktop/src/main/lib/dev-auto-sign-in.ts
create mode 100644 apps/web/src/app/(auth)/components/DevAuthForm/DevAuthForm.tsx
create mode 100644 apps/web/src/app/(auth)/components/DevAuthForm/index.ts
create mode 100644 docs/LOCAL_DEVELOPMENT.md
create mode 100644 packages/db/src/seed-dev.ts
create mode 100644 plans/20260515-oss-local-setup.md
diff --git a/README.md b/README.md
index e1d935232ec..e2758e59ecf 100644
--- a/README.md
+++ b/README.md
@@ -85,57 +85,32 @@ If it runs in a terminal, it runs on Superset
### Build from Source
-
-Click to expand build instructions
+For a complete contributor workflow that boots a fresh clone with no third-party credentials (Neon / OAuth / Stripe / Resend keys are all optional), follow **[Local Development](docs/LOCAL_DEVELOPMENT.md)**.
-**1. Clone the repository**
+Short version:
```bash
git clone https://github.com/superset-sh/superset.git
cd superset
-```
-
-**2. Set up environment variables** (choose one):
-
-Option A: Full setup
-```bash
-cp .env.example .env
-# Edit .env and fill in the values
-```
-
-Option B: Skip env validation (for quick local testing)
-```bash
-cp .env.example .env
-echo 'SKIP_ENV_VALIDATION=1' >> .env
-```
-
-**3. Set up Caddy** (reverse proxy for Electric SQL streams):
-
-```bash
-# Install caddy: brew install caddy (macOS) or see https://caddyserver.com/docs/install
-cp Caddyfile.example Caddyfile
-
-# Without this, Chromium rejects https://localhost:* with ERR_CERT_AUTHORITY_INVALID.
-# Prompts for sudo once.
-caddy trust
-```
-
-**4. Install dependencies and run**
-
-```bash
bun install
-bun run dev
+docker run -d --name superset-pg \
+ -e POSTGRES_USER=superset -e POSTGRES_PASSWORD=superset -e POSTGRES_DB=superset \
+ -p 5433:5432 postgres:16 -c wal_level=logical
+cp .env.example .env # then edit DATABASE_URL + BETTER_AUTH_SECRET
+bun run db:migrate
+cp Caddyfile.example Caddyfile && caddy trust
+SKIP_ENV_VALIDATION=1 bun dev
```
-**5. Build the desktop app**
+The desktop window opens auto-signed-in as a seed admin (`admin@local.test`). See [Local Development](docs/LOCAL_DEVELOPMENT.md) for details, troubleshooting, and what's stubbed without integration keys.
+
+To build a distributable desktop app:
```bash
bun run build
open apps/desktop/release
```
-
-
## Keyboard Shortcuts
All shortcuts are customizable via **Settings > Keyboard Shortcuts** (`⌘/`). See [full documentation](https://docs.superset.sh/keyboard-shortcuts).
diff --git a/apps/api/src/lib/analytics.ts b/apps/api/src/lib/analytics.ts
index aa8d66fce76..8f5536ff5d3 100644
--- a/apps/api/src/lib/analytics.ts
+++ b/apps/api/src/lib/analytics.ts
@@ -1,8 +1,33 @@
import { PostHog } from "posthog-node";
import { env } from "@/env";
-export const posthog = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
- host: env.NEXT_PUBLIC_POSTHOG_HOST,
- flushAt: 1,
- flushInterval: 0,
+let client: PostHog | null = null;
+
+// Disabled stub — accepts any args, returns the right shape for each method.
+const disabled = {
+ capture: (..._args: unknown[]) => {},
+ identify: (..._args: unknown[]) => {},
+ alias: (..._args: unknown[]) => {},
+ groupIdentify: (..._args: unknown[]) => {},
+ shutdown: (..._args: unknown[]) => Promise.resolve(),
+ flush: (..._args: unknown[]) => Promise.resolve(),
+ getFeatureFlag: (..._args: unknown[]) => Promise.resolve(undefined),
+ getFeatureFlagPayload: (..._args: unknown[]) => Promise.resolve(undefined),
+ isFeatureEnabled: (..._args: unknown[]) => Promise.resolve(undefined),
+} as unknown as PostHog;
+
+// Lazy-init: if NEXT_PUBLIC_POSTHOG_KEY is missing, return a no-op surface
+// so analytics calls don't crash the API at module load.
+export const posthog = new Proxy({} as PostHog, {
+ get(_target, prop) {
+ if (!env.NEXT_PUBLIC_POSTHOG_KEY) return Reflect.get(disabled, prop);
+ if (!client) {
+ client = new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
+ host: env.NEXT_PUBLIC_POSTHOG_HOST,
+ flushAt: 1,
+ flushInterval: 0,
+ });
+ }
+ return Reflect.get(client, prop);
+ },
});
diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts
index 03879a6d9ea..eca914a7d3b 100644
--- a/apps/desktop/src/main/index.ts
+++ b/apps/desktop/src/main/index.ts
@@ -28,6 +28,7 @@ import { initAppState } from "./lib/app-state";
import { requestAppleEventsAccess } from "./lib/apple-events-permission";
import { setupAutoUpdater } from "./lib/auto-updater";
import { installBundledCliShim } from "./lib/bundled-cli";
+import { ensureDevAuthToken } from "./lib/dev-auto-sign-in";
import { resolveDevWorkspaceName } from "./lib/dev-workspace-name";
import { setWorkspaceDockIcon } from "./lib/dock-icon";
import { loadWebviewBrowserExtension } from "./lib/extensions";
@@ -55,6 +56,12 @@ import { MainWindow } from "./windows/main";
console.log("[main] Local database ready:", !!localDb);
const IS_DEV = process.env.NODE_ENV === "development";
+// Dev: expose Chrome DevTools Protocol for headless testing (e.g. import/host-service checks)
+if (IS_DEV && process.env.SKIP_ENV_VALIDATION) {
+ app.commandLine.appendSwitch("remote-debugging-port", "9333");
+ app.commandLine.appendSwitch("remote-allow-origins", "*");
+}
+
void applyShellEnvToProcess().catch((error) => {
console.error("[main] Failed to apply shell environment:", error);
});
@@ -416,6 +423,12 @@ if (!gotTheLock) {
console.error("[main] Failed to install bundled CLI shim:", error);
}
+ // Dev-only: auto-sign-in as the seed admin if SKIP_ENV_VALIDATION is
+ // set and no token is on disk. Lets fresh-clone contributors boot the
+ // desktop without OAuth. Runs before host-service discovery so the
+ // renderer's AuthProvider hydrates with a valid token on first paint.
+ await ensureDevAuthToken();
+
// Discover and adopt host-services that survived a previous quit
// before the tray initializes, so it shows accurate status immediately.
await getHostServiceCoordinator().discoverAll();
diff --git a/apps/desktop/src/main/lib/dev-auto-sign-in.ts b/apps/desktop/src/main/lib/dev-auto-sign-in.ts
new file mode 100644
index 00000000000..e5f1f5a0650
--- /dev/null
+++ b/apps/desktop/src/main/lib/dev-auto-sign-in.ts
@@ -0,0 +1,96 @@
+import { env as mainEnv } from "main/env.main";
+import {
+ loadToken,
+ saveToken,
+} from "../../lib/trpc/routers/auth/utils/auth-functions";
+
+const DEV_EMAIL = "admin@local.test";
+const DEV_PASSWORD = "supersetdev";
+const DEV_NAME = "Local Admin";
+const TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 30;
+
+interface SignInResponse {
+ token?: string;
+ user?: { id: string };
+}
+
+interface AuthErrorBody {
+ code?: string;
+ message?: string;
+}
+
+async function postAuth(
+ path: string,
+ body: Record,
+): Promise<{ ok: boolean; status: number; data: T | AuthErrorBody }> {
+ const res = await fetch(`${mainEnv.NEXT_PUBLIC_API_URL}${path}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Origin: mainEnv.NEXT_PUBLIC_API_URL,
+ },
+ body: JSON.stringify(body),
+ });
+ const data = (await res.json().catch(() => ({}))) as T | AuthErrorBody;
+ return { ok: res.ok, status: res.status, data };
+}
+
+/**
+ * Dev-only: if SKIP_ENV_VALIDATION is set and no usable token is on disk,
+ * sign in (or sign up) as the seed admin user and persist the token so
+ * the renderer's AuthProvider can hydrate normally — no special renderer code.
+ * Best-effort: failure is logged but doesn't crash boot.
+ */
+export async function ensureDevAuthToken(): Promise {
+ if (
+ process.env.NODE_ENV !== "development" ||
+ !process.env.SKIP_ENV_VALIDATION
+ )
+ return;
+
+ const stored = await loadToken();
+ if (stored.token && stored.expiresAt) {
+ const isExpired = new Date(stored.expiresAt) < new Date();
+ if (!isExpired) return;
+ }
+
+ try {
+ let signIn = await postAuth("/api/auth/sign-in/email", {
+ email: DEV_EMAIL,
+ password: DEV_PASSWORD,
+ });
+
+ const errBody = signIn.data as AuthErrorBody;
+ if (!signIn.ok && errBody.code === "INVALID_EMAIL_OR_PASSWORD") {
+ const signUp = await postAuth("/api/auth/sign-up/email", {
+ email: DEV_EMAIL,
+ password: DEV_PASSWORD,
+ name: DEV_NAME,
+ });
+ if (!signUp.ok) {
+ const e = signUp.data as AuthErrorBody;
+ throw new Error(`dev sign-up failed (${signUp.status}): ${e.message}`);
+ }
+ signIn = await postAuth("/api/auth/sign-in/email", {
+ email: DEV_EMAIL,
+ password: DEV_PASSWORD,
+ });
+ }
+
+ if (!signIn.ok) {
+ const e = signIn.data as AuthErrorBody;
+ throw new Error(`dev sign-in failed (${signIn.status}): ${e.message}`);
+ }
+ const token = (signIn.data as SignInResponse).token;
+ if (!token) throw new Error("dev sign-in: no token in response");
+
+ const expiresAt = new Date(Date.now() + TOKEN_TTL_MS).toISOString();
+ await saveToken({ token, expiresAt });
+ console.log(`[dev-auto-sign-in] signed in as ${DEV_EMAIL}`);
+ } catch (err) {
+ console.warn(
+ `[dev-auto-sign-in] failed (is the API up at ${mainEnv.NEXT_PUBLIC_API_URL}?):`,
+ err,
+ );
+ }
+}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx
index d853bf926d9..8b8b74465b2 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx
@@ -54,9 +54,9 @@ export function OpenInWorkspaceV2({ task }: OpenInWorkspaceV2Props) {
const { machineId, activeHostUrl } = useLocalHostService();
const { otherHosts } = useWorkspaceHostOptions();
const { data: session } = authClient.useSession();
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const { submit } = useWorkspaceCreates();
const lastProjectId = useV2WorkspaceCreateDefaultsStore(
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx
index 6759919c96a..75843cbaf59 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx
@@ -59,9 +59,9 @@ export function RunInWorkspacePopoverV2({
const collections = useCollections();
const { machineId, activeHostUrl } = useLocalHostService();
const { data: session } = authClient.useSession();
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const { otherHosts } = useWorkspaceHostOptions();
const { submit } = useWorkspaceCreates();
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunIssuesInWorkspacePopover/RunIssuesInWorkspacePopover.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunIssuesInWorkspacePopover/RunIssuesInWorkspacePopover.tsx
index a05788c30f9..0c877cfed52 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunIssuesInWorkspacePopover/RunIssuesInWorkspacePopover.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunIssuesInWorkspacePopover/RunIssuesInWorkspacePopover.tsx
@@ -63,9 +63,9 @@ export function RunIssuesInWorkspacePopover({
const collections = useCollections();
const { machineId, activeHostUrl } = useLocalHostService();
const { data: session } = authClient.useSession();
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const { otherHosts } = useWorkspaceHostOptions();
const { submit } = useWorkspaceCreates();
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts
index eb904571c63..6a95ea646b5 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts
@@ -221,9 +221,9 @@ export function useAccessibleV2Workspaces(
const collections = useCollections();
const { machineId } = useLocalHostService();
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const currentUserId = session?.user?.id ?? null;
const { data: rows = [] } = useLiveQuery(
diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts
index b96c7534db5..6014b9ee911 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts
+++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts
@@ -26,9 +26,9 @@ export function useWorkspaceHostOptions(): UseWorkspaceHostOptionsResult {
const collections = useCollections();
const { machineId, activeHostUrl } = useLocalHostService();
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const currentUserId = session?.user?.id ?? null;
const { data: accessibleHosts = [] } = useLiveQuery(
diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/DashboardNewWorkspaceModalContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/DashboardNewWorkspaceModalContent.tsx
index da8399adba2..da6b96ba19d 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/DashboardNewWorkspaceModalContent.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceModalContent/DashboardNewWorkspaceModalContent.tsx
@@ -32,9 +32,9 @@ export function DashboardNewWorkspaceModalContent({
);
const collections = useCollections();
const { data: session } = authClient.useSession();
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const { data: v2Projects } = useLiveQuery(
(q) =>
diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/V1ImportModal.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/V1ImportModal.tsx
index bd292fd0e90..8b127423c10 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/V1ImportModal.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/V1ImportModal.tsx
@@ -27,9 +27,9 @@ export function V1ImportModal() {
const close = useCloseV1ImportModal();
const { data: session } = authClient.useSession();
const { activeHostUrl } = useLocalHostService();
- const organizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const organizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
if (!organizationId) return null;
diff --git a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx
index dbc28401f70..4717948ec01 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx
@@ -62,9 +62,9 @@ function AuthenticatedLayout() {
const isV2CloudEnabled = useIsV2CloudEnabled();
const isSignedIn = env.SKIP_ENV_VALIDATION || !!session?.user;
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : session?.session?.activeOrganizationId;
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
useAgentHookListener();
useUpdateListener();
diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx
index f5562295086..518fb6b4495 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx
@@ -33,9 +33,9 @@ export function preloadActiveOrganizationCollections(
export function CollectionsProvider({ children }: { children: ReactNode }) {
const { data: session, refetch: refetchSession } = authClient.useSession();
const [isSwitching, setIsSwitching] = useState(false);
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : session?.session?.activeOrganizationId;
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const switchOrganization = useCallback(
async (organizationId: string) => {
diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/LocalHostServiceProvider/LocalHostServiceProvider.tsx b/apps/desktop/src/renderer/routes/_authenticated/providers/LocalHostServiceProvider/LocalHostServiceProvider.tsx
index 5ef6fdfd5cc..75276559f9d 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/providers/LocalHostServiceProvider/LocalHostServiceProvider.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/providers/LocalHostServiceProvider/LocalHostServiceProvider.tsx
@@ -31,9 +31,9 @@ export function LocalHostServiceProvider({
const { mutate: startHostService } =
electronTrpc.hostServiceCoordinator.start.useMutation();
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const { data: organizations } = useLiveQuery(
(q) => q.from({ organizations: collections.organizations }),
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx
index 0b4567a52e4..5a0c0454b7d 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx
@@ -30,9 +30,9 @@ export function HostsSettingsSidebar({
const collections = useCollections();
const { data: session } = authClient.useSession();
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const { data: hosts = [] } = useLiveQuery(
(q) =>
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx
index 643c8e8adbd..3c50905ddd5 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx
@@ -16,9 +16,9 @@ function HostsIndexPage() {
const { data: session } = authClient.useSession();
const navigate = useNavigate();
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const { data: hosts = [] } = useLiveQuery(
(q) =>
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsx
index c93c0d2e7ad..7f11dda2af9 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/$projectId/page.tsx
@@ -25,9 +25,9 @@ function ProjectDetailPage() {
const { data: session } = authClient.useSession();
const searchQuery = useSettingsSearchQuery();
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const { data: v2Match = [] } = useLiveQuery(
(q) =>
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx
index ce7d751ecc4..bedd6a8612b 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx
@@ -31,9 +31,9 @@ export function ProjectsSettingsSidebar({
const collections = useCollections();
const { data: session } = authClient.useSession();
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const { data: groups = [] } =
electronTrpc.workspaces.getAllGrouped.useQuery();
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx
index 5db0608732d..60605248b49 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx
@@ -17,9 +17,9 @@ function ProjectsIndexPage() {
const { data: session } = authClient.useSession();
const navigate = useNavigate();
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const { data: groups = [] } =
electronTrpc.workspaces.getAllGrouped.useQuery();
diff --git a/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx
index fe5b99a03f3..10626d1ce1f 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/setup/project/page.tsx
@@ -26,9 +26,9 @@ function OnboardingProjectPage() {
const markSkipped = useOnboardingStore((s) => s.markSkipped);
const { data: session } = authClient.useSession();
- const activeOrganizationId = env.SKIP_ENV_VALIDATION
- ? MOCK_ORG_ID
- : (session?.session?.activeOrganizationId ?? null);
+ const activeOrganizationId =
+ session?.session?.activeOrganizationId ??
+ (env.SKIP_ENV_VALIDATION ? MOCK_ORG_ID : null);
const { data: projects = [], isLoading } = useQuery({
queryKey: ["onboarding", "v2Projects", activeOrganizationId],
diff --git a/apps/desktop/src/renderer/routes/sign-in/page.tsx b/apps/desktop/src/renderer/routes/sign-in/page.tsx
index ddfc693a961..aa08c51ad5d 100644
--- a/apps/desktop/src/renderer/routes/sign-in/page.tsx
+++ b/apps/desktop/src/renderer/routes/sign-in/page.tsx
@@ -18,8 +18,8 @@ function SignInPage() {
const signInMutation = electronTrpc.auth.signIn.useMutation();
const { hasLocalToken, isPending, session } = useSessionRecovery();
- // Dev bypass: skip sign-in entirely
- if (env.SKIP_ENV_VALIDATION) {
+ // Dev bypass: AuthProvider handles auto-sign-in; if session lands, redirect
+ if (env.SKIP_ENV_VALIDATION && session?.user) {
return ;
}
diff --git a/apps/web/src/app/(auth)/components/DevAuthForm/DevAuthForm.tsx b/apps/web/src/app/(auth)/components/DevAuthForm/DevAuthForm.tsx
new file mode 100644
index 00000000000..d07f9a35f6a
--- /dev/null
+++ b/apps/web/src/app/(auth)/components/DevAuthForm/DevAuthForm.tsx
@@ -0,0 +1,103 @@
+"use client";
+
+import { authClient } from "@superset/auth/client";
+import { Button } from "@superset/ui/button";
+import { Input } from "@superset/ui/input";
+import { Label } from "@superset/ui/label";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+
+interface DevAuthFormProps {
+ mode: "sign-in" | "sign-up";
+ callbackURL: string;
+}
+
+export function DevAuthForm({ mode, callbackURL }: DevAuthFormProps) {
+ const router = useRouter();
+ const [email, setEmail] = useState(
+ mode === "sign-up" ? "" : "admin@local.test",
+ );
+ const [password, setPassword] = useState(
+ mode === "sign-up" ? "" : "supersetdev",
+ );
+ const [name, setName] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+
+ const onSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setSubmitting(true);
+ setError(null);
+
+ try {
+ if (mode === "sign-up") {
+ const res = await authClient.signUp.email({
+ email,
+ password,
+ name: name || email.split("@")[0] || "Dev User",
+ });
+ if (res.error) throw new Error(res.error.message);
+ } else {
+ const res = await authClient.signIn.email({ email, password });
+ if (res.error) throw new Error(res.error.message);
+ }
+ router.push(callbackURL);
+ } catch (err) {
+ const message =
+ err instanceof Error ? err.message : "Authentication failed";
+ setError(message);
+ setSubmitting(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(auth)/components/DevAuthForm/index.ts b/apps/web/src/app/(auth)/components/DevAuthForm/index.ts
new file mode 100644
index 00000000000..eddbcfa938b
--- /dev/null
+++ b/apps/web/src/app/(auth)/components/DevAuthForm/index.ts
@@ -0,0 +1 @@
+export { DevAuthForm } from "./DevAuthForm";
diff --git a/apps/web/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx b/apps/web/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx
index b27142e33f0..63049a01b27 100644
--- a/apps/web/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx
+++ b/apps/web/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx
@@ -8,6 +8,9 @@ import { useState } from "react";
import { FaGithub } from "react-icons/fa";
import { FcGoogle } from "react-icons/fc";
import { env } from "@/env";
+import { DevAuthForm } from "../../components/DevAuthForm";
+
+const isDev = process.env.NODE_ENV !== "production";
export default function SignInPage() {
const searchParams = useSearchParams();
@@ -66,6 +69,7 @@ export default function SignInPage() {
{error && (
{error}
)}
+ {isDev && }
)}
+ {isDev && (
+
+ )}