Merge branch 'cl/romantic-mendel-5a2c25' into cl/hexclave-pr3

This commit is contained in:
BilalG1 2026-05-26 18:16:36 -07:00 committed by GitHub
commit 0a85a49772
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 451 additions and 41 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -0,0 +1,26 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="neon-grad" x1="4" y1="4" x2="44" y2="44" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#00FFFF" />
<stop offset="50%" stop-color="#3B82F6" />
<stop offset="100%" stop-color="#8B5CF6" />
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="1.5" result="blur1" />
<feGaussianBlur stdDeviation="3.5" result="blur2" />
<feMerge>
<feMergeNode in="blur2" />
<feMergeNode in="blur1" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<g id="benzene-mark">
<path d="M 24 4 L 41.32 14 L 41.32 34 L 24 44 L 6.68 34 L 6.68 14 Z" fill="none" stroke="url(#neon-grad)" stroke-width="3" stroke-linejoin="miter" />
<path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="url(#neon-grad)" />
<path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="url(#neon-grad)" transform="rotate(120 24 24)" />
<path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="url(#neon-grad)" transform="rotate(240 24 24)" />
</g>
</defs>
<use href="#benzene-mark" filter="url(#glow)" opacity="0.75" />
<use href="#benzene-mark" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,3 +1,26 @@
<svg width="131" height="156" viewBox="0 0 131 156" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M124.042 28.6459L69.7327 1.75616C66.9416 0.374284 63.666 0.372197 60.8735 1.75051L0.335449 31.6281V87.6369L65.3045 119.91L117.154 93.675V112.414L65.3045 138.44L0.335449 106.584V119.655C0.335449 122.359 1.87599 124.827 4.30545 126.015L61.8765 154.161C64.6911 155.538 67.9883 155.515 70.7832 154.099L130.065 124.074V79.7105C130.065 74.8003 124.934 71.5769 120.51 73.7077L79.0476 93.675V75.9771L130.065 50.1589V38.3485C130.065 34.2325 127.731 30.4724 124.042 28.6459Z" fill="white"/>
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Hexclave">
<defs>
<linearGradient id="neon-grad" x1="4" y1="4" x2="44" y2="44" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#00FFFF" />
<stop offset="50%" stop-color="#3B82F6" />
<stop offset="100%" stop-color="#8B5CF6" />
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="1.5" result="blur1" />
<feGaussianBlur stdDeviation="3.5" result="blur2" />
<feMerge>
<feMergeNode in="blur2" />
<feMergeNode in="blur1" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<g id="benzene-mark">
<path d="M 24 4 L 41.32 14 L 41.32 34 L 24 44 L 6.68 34 L 6.68 14 Z" fill="none" stroke="url(#neon-grad)" stroke-width="3" stroke-linejoin="miter" />
<path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="url(#neon-grad)" />
<path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="url(#neon-grad)" transform="rotate(120 24 24)" />
<path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="url(#neon-grad)" transform="rotate(240 24 24)" />
</g>
</defs>
<use href="#benzene-mark" filter="url(#glow)" opacity="0.75" />
<use href="#benzene-mark" />
</svg>

Before

Width:  |  Height:  |  Size: 597 B

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,3 +1,26 @@
<svg width="131" height="156" viewBox="0 0 131 156" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M124.447 28.6459L70.1382 1.75616C67.3472 0.374284 64.0715 0.372197 61.279 1.75051L0.740967 31.6281V87.6369L65.7101 119.91L117.56 93.675V112.414L65.7101 138.44L0.740967 106.584V119.655C0.740967 122.359 2.28151 124.827 4.71097 126.015L62.282 154.161C65.0966 155.538 68.3938 155.515 71.1888 154.099L130.47 124.074V79.7105C130.47 74.8003 125.34 71.5769 120.915 73.7077L79.4531 93.675V75.9771L130.47 50.1589V38.3485C130.47 34.2325 128.137 30.4724 124.447 28.6459Z" fill="black"/>
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Hexclave">
<defs>
<linearGradient id="neon-grad" x1="4" y1="4" x2="44" y2="44" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#00FFFF" />
<stop offset="50%" stop-color="#3B82F6" />
<stop offset="100%" stop-color="#8B5CF6" />
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="1.5" result="blur1" />
<feGaussianBlur stdDeviation="3.5" result="blur2" />
<feMerge>
<feMergeNode in="blur2" />
<feMergeNode in="blur1" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<g id="benzene-mark">
<path d="M 24 4 L 41.32 14 L 41.32 34 L 24 44 L 6.68 34 L 6.68 14 Z" fill="none" stroke="url(#neon-grad)" stroke-width="3" stroke-linejoin="miter" />
<path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="url(#neon-grad)" />
<path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="url(#neon-grad)" transform="rotate(120 24 24)" />
<path d="M 11 16.87 L 14 15.13 L 14 32.87 L 11 31.13 Z" fill="url(#neon-grad)" transform="rotate(240 24 24)" />
</g>
</defs>
<use href="#benzene-mark" filter="url(#glow)" opacity="0.75" />
<use href="#benzene-mark" />
</svg>

Before

Width:  |  Height:  |  Size: 591 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 148 KiB

View File

@ -3,6 +3,7 @@
import Loading from "@/app/loading";
import { CursorBlastEffect } from "@hexclave/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 "@hexclave/next";
import { LOCAL_EMULATOR_ADMIN_EMAIL, LOCAL_EMULATOR_ADMIN_PASSWORD } from "@hexclave/shared/dist/local-emulator";
@ -60,6 +61,7 @@ export default function LayoutClient({ children }: { children: React.ReactNode }
return (
<ConfigUpdateDialogProvider>
<CursorBlastEffect />
<HexclaveRebrandModal />
{children}
</ConfigUpdateDialogProvider>
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -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 (
<Dialog
open={open}
onOpenChange={(next) => {
if (!next) dismiss();
}}
>
<DialogContent className="max-w-md">
<DialogHeader>
<RebrandIllustration />
<DialogTitle className="text-center text-xl pt-2">
Stack Auth is now Hexclave
</DialogTitle>
<DialogDescription className="text-center">
We&apos;re rebranding! Same product, same team, new home at{" "}
<a
href="https://app.hexclave.com"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-2 hover:text-foreground"
>
app.hexclave.com
</a>
. To update your project, rename all{" "}
<code className="font-mono text-xs">@stackframe/*</code> imports to{" "}
<code className="font-mono text-xs">@hexclave/*</code> the only
exception is{" "}
<code className="font-mono text-xs">@stackframe/stack</code>, which
becomes <code className="font-mono text-xs">@hexclave/next</code>.
See the{" "}
<a
href={MIGRATION_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 hover:text-foreground"
>
migration guide
</a>{" "}
for full details.
</DialogDescription>
</DialogHeader>
<DialogFooter className="sm:justify-center pt-6">
<Button onClick={dismiss} className="min-w-32">
Got it
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
/**
* Stack Auth mark (faded) arrow Hexclave benzene mark. Both logos are
* served from `/public` so they match the canonical brand assets.
*/
function RebrandIllustration() {
return (
<div
className="flex justify-center items-center gap-4 pb-2"
aria-hidden="true"
>
{/* Stack Auth: served light & dark variants depending on theme */}
<Image
src="/logo.svg"
alt=""
width={48}
height={48}
aria-hidden
className="h-12 w-auto opacity-50 block dark:hidden"
/>
<Image
src="/logo-bright.svg"
alt=""
width={48}
height={48}
aria-hidden
className="h-12 w-auto opacity-60 hidden dark:block"
/>
{/* Arrow — bridge between the two marks */}
<svg
width="40"
height="14"
viewBox="0 0 40 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-muted-foreground"
>
<path
d="M2 7 L34 7"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M28 1 L34 7 L28 13"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</svg>
{/* Hexclave benzene mark — gradient + glow filter, theme-agnostic */}
<Image
src="/hexclave-icon.svg"
alt=""
width={56}
height={56}
aria-hidden
className="h-14 w-14"
/>
</div>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -154,7 +154,7 @@
{
"group": "Tutorials",
"pages": [
"guides/other/tutorials/build-a-saas-with-stack-auth",
"guides/other/tutorials/build-a-saas-with-hexclave",
"guides/other/tutorials/build-a-team-based-app",
"guides/other/tutorials/ship-production-ready-auth"
]

View File

@ -5,7 +5,7 @@ description: Model B2B tenants as teams, wire team selection and deep links, enf
This tutorial walks through a **multi-tenant** product where a **team** is the customer boundary (workspace, organization, account). You get membership-scoped team access, permission checks, team selection UX, and invitations.
If you have not installed Stack yet (handler routes, `HexclaveProvider`, environment variables), start with [Build a SaaS with Hexclave](/guides/other/tutorials/build-a-saas-with-stack-auth), then continue here.
If you have not installed Stack yet (handler routes, `HexclaveProvider`, environment variables), start with [Build a SaaS with Hexclave](/guides/other/tutorials/build-a-saas-with-hexclave), then continue here.
## What you will have at the end
@ -17,7 +17,7 @@ If you have not installed Stack yet (handler routes, `HexclaveProvider`, environ
## Prerequisites
- Hexclave installed as in [Build a SaaS with Hexclave](/guides/other/tutorials/build-a-saas-with-stack-auth) (Next.js App Router examples below assume `stackServerApp` in `stack/server.ts` and `@hexclave/next` in the app).
- Hexclave installed as in [Build a SaaS with Hexclave](/guides/other/tutorials/build-a-saas-with-hexclave) (Next.js App Router examples below assume `stackServerApp` in `stack/server.ts` and `@hexclave/next` in the app).
- A project in the [dashboard](https://app.hexclave.com/projects) where you can edit **Teams** and **Team permissions**.
<Note>
@ -356,7 +356,7 @@ Teams support `clientMetadata`, `serverMetadata`, and `clientReadOnlyMetadata` o
| Topic | Guide |
|--------|--------|
| Auth bootstrap and route protection | [Build a SaaS with Hexclave](/guides/other/tutorials/build-a-saas-with-stack-auth) |
| Auth bootstrap and route protection | [Build a SaaS with Hexclave](/guides/other/tutorials/build-a-saas-with-hexclave) |
| Teams API reference | [Teams](/guides/apps/teams/overview) |
| `SelectedTeamSwitcher` and URL strategies | [Team selection](/guides/apps/teams/team-selection) |
| Permission modeling and nesting | [RBAC](/guides/apps/rbac/overview) |

View File

@ -5,7 +5,7 @@ description: Lock down page and API access, handle secrets and environments, the
Going live is not only “turning on production mode.” This guide focuses on **what must be true** so only signed-in users reach protected surfaces, **secrets stay server-only**, and Stacks dev-friendly defaults are replaced with **your** domains, OAuth apps, and email.
If you are still wiring Stack into your app, complete [Build a SaaS with Hexclave](/guides/other/tutorials/build-a-saas-with-stack-auth) first. For team RBAC before launch, see [Build a team-based app](/guides/other/tutorials/build-a-team-based-app).
If you are still wiring Stack into your app, complete [Build a SaaS with Hexclave](/guides/other/tutorials/build-a-saas-with-hexclave) first. For team RBAC before launch, see [Build a team-based app](/guides/other/tutorials/build-a-team-based-app).
## What you will have at the end
@ -153,7 +153,7 @@ If you consume Stack webhooks, **verify every payload** (for example with Svix a
| Topic | Guide |
|--------|--------|
| First integration | [Build a SaaS with Hexclave](/guides/other/tutorials/build-a-saas-with-stack-auth) |
| First integration | [Build a SaaS with Hexclave](/guides/other/tutorials/build-a-saas-with-hexclave) |
| Page protection details | [User fundamentals](/guides/getting-started/user-fundamentals) |
| Domains, OAuth, email, prod mode | [Launch checklist](/guides/apps/launch-checklist/overview) |
| `HexclaveServerApp` and keys | [Stack App](/guides/going-further/stack-app) |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -7,8 +7,8 @@ import { stackServerApp } from "src/stack";
import './global.css';
export const metadata: Metadata = {
title: 'Stack Demo',
description: 'Example of using Stack as your authentication system.',
title: 'Hexclave Demo',
description: 'Example of using Hexclave as your authentication system.',
};
export default function RootLayout({

View File

@ -17,7 +17,6 @@ export default function Header() {
alt="Hexclave Logo"
width={64}
height={64}
className="dark:invert"
/>
Demo
</Link>

View File

@ -8,8 +8,8 @@ import './global.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Stack Demo',
description: 'Example of using Stack as your authentication system.',
title: 'Hexclave Docs Example',
description: 'Example of using Hexclave as your authentication system.',
};
export default function RootLayout({

View File

@ -7,7 +7,7 @@ const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Hexclave Middleware Demo",
description: "A demo of Stack's middleware capabilities.",
description: "A demo of Hexclave's middleware capabilities.",
};
export default function RootLayout({

View File

@ -8,7 +8,7 @@ export function Header() {
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between gap-4">
<nav className="flex items-center gap-4">
<Link to="/" className="font-semibold tracking-tight">
Stack TanStack Demo
Hexclave TanStack Demo
</Link>
<Link to="/ssr" className="text-sm text-zinc-600 hover:text-zinc-950 hover:transition-none dark:text-zinc-300 dark:hover:text-white">
SSR