diff --git a/.github/assets/hexclave-rebrand-modal.png b/.github/assets/hexclave-rebrand-modal.png
new file mode 100644
index 0000000000..4079615689
Binary files /dev/null and b/.github/assets/hexclave-rebrand-modal.png differ
diff --git a/apps/dashboard/public/hexclave-icon.svg b/apps/dashboard/public/hexclave-icon.svg
new file mode 100644
index 0000000000..6954a8f3fe
--- /dev/null
+++ b/apps/dashboard/public/hexclave-icon.svg
@@ -0,0 +1,26 @@
+
diff --git a/apps/dashboard/src/app/(main)/(protected)/layout-client.tsx b/apps/dashboard/src/app/(main)/(protected)/layout-client.tsx
index de0da7ce84..19c1b51f0e 100644
--- a/apps/dashboard/src/app/(main)/(protected)/layout-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/layout-client.tsx
@@ -3,6 +3,7 @@
import Loading from "@/app/loading";
import { CursorBlastEffect } from "@stackframe/dashboard-ui-components";
import { ConfigUpdateDialogProvider } from "@/lib/config-update";
+import { HexclaveRebrandModal } from "@/components/hexclave-rebrand-modal";
import { getPublicEnvVar } from '@/lib/env';
import { useStackApp, useUser } from "@stackframe/stack";
import { LOCAL_EMULATOR_ADMIN_EMAIL, LOCAL_EMULATOR_ADMIN_PASSWORD } from "@stackframe/stack-shared/dist/local-emulator";
@@ -60,6 +61,7 @@ export default function LayoutClient({ children }: { children: React.ReactNode }
return (
+
{children}
);
diff --git a/apps/dashboard/src/components/hexclave-rebrand-modal.tsx b/apps/dashboard/src/components/hexclave-rebrand-modal.tsx
new file mode 100644
index 0000000000..b96ff01bc5
--- /dev/null
+++ b/apps/dashboard/src/components/hexclave-rebrand-modal.tsx
@@ -0,0 +1,207 @@
+"use client";
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { getPublicEnvVar } from "@/lib/env";
+import { useUser } from "@stackframe/stack";
+import Image from "next/image";
+import { useEffect, useState } from "react";
+
+// Per-user dismissal flag. Keyed by user.id so a shared browser (e.g. a
+// machine where two teammates each log into their own accounts) tracks the
+// dismissal separately for each account — otherwise one teammate dismissing
+// would silently hide the announcement from the other.
+const STORAGE_KEY_PREFIX = "hexclave-rebrand-modal-dismissed:";
+const MIGRATION_DOCS_URL = "https://docs.hexclave.com/migration";
+
+// Users who signed up before this instant predate the Stack Auth → Hexclave
+// rebrand and are the only ones who benefit from the announcement. Anyone
+// signing up after this already lands on a Hexclave-branded experience and
+// has no "Stack Auth" mental model to update — no point telling them.
+const REBRAND_CUTOFF = new Date("2026-05-27T00:00:00.000Z");
+
+/**
+ * One-time informational modal announcing the Stack Auth → Hexclave rebrand.
+ *
+ * Skipped entirely in preview / local-emulator / remote-development environments
+ * — those auto-create throwaway users or seed a fixture admin, so the rebrand
+ * notice would be friction for developers and meaningless for preview visitors
+ * who never used "Stack Auth" in the first place.
+ *
+ * For real customers: only renders for a logged-in user who signed up before
+ * {@link REBRAND_CUTOFF}. On any dismissal (confirm button, close button,
+ * overlay click, or Escape) writes `${STORAGE_KEY_PREFIX}${user.id}` to
+ * localStorage so the modal never re-appears for that account on that browser.
+ */
+export function HexclaveRebrandModal() {
+ // Skip in dev/preview environments — same flags the protected layout already
+ // gates on. Read at top so we can short-circuit before any hook runs the
+ // useEffect or computes the user-based gate.
+ const isDevEnvironment =
+ getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true"
+ || getPublicEnvVar("NEXT_PUBLIC_STACK_IS_REMOTE_DEVELOPMENT_ENVIRONMENT") === "true"
+ || getPublicEnvVar("NEXT_PUBLIC_STACK_IS_PREVIEW") === "true";
+
+ // `or: "return-null"` keeps this from triggering the sign-in redirect when
+ // it's rendered above the auth boundary — we simply opt out for guests.
+ const user = useUser({ or: "return-null" });
+ const isPreRebrandUser =
+ !isDevEnvironment && user != null && user.signedUpAt < REBRAND_CUTOFF;
+ const [open, setOpen] = useState(false);
+
+ // Per-user storage key. `null` when there's no user; the gates below
+ // ensure we never try to read/write it in that case.
+ const storageKey = user ? `${STORAGE_KEY_PREFIX}${user.id}` : null;
+
+ // Read localStorage after hydration to avoid SSR mismatch — render closed
+ // on the server and only open if we know this user hasn't dismissed it.
+ useEffect(() => {
+ if (!isPreRebrandUser || !storageKey) return;
+ try {
+ const dismissed = localStorage.getItem(storageKey);
+ if (dismissed !== "true") {
+ setOpen(true);
+ }
+ } catch {
+ // localStorage can throw in private-mode / sandboxed iframes; treat
+ // unavailable storage as "already dismissed" so we don't spam users
+ // who can't persist the dismissal anyway.
+ }
+ }, [isPreRebrandUser, storageKey]);
+
+ const dismiss = () => {
+ if (storageKey) {
+ try {
+ localStorage.setItem(storageKey, "true");
+ } catch {
+ // see above — best-effort write
+ }
+ }
+ setOpen(false);
+ };
+
+ if (!isPreRebrandUser) return null;
+
+ return (
+
+ );
+}
+
+/**
+ * Stack Auth mark (faded) → arrow → Hexclave benzene mark. Both logos are
+ * served from `/public` so they match the canonical brand assets.
+ */
+function RebrandIllustration() {
+ return (
+
+ {/* Stack Auth: served light & dark variants depending on theme */}
+
+
+
+ {/* Arrow — bridge between the two marks */}
+
+
+ {/* Hexclave benzene mark — gradient + glow filter, theme-agnostic */}
+
+