mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
[Docs] First-class TanStack Start in AI setup prompts + clearer env-var guidance (#1438)
## Summary
Two related improvements to Stack Auth's AI setup story, both driven by
`packages/stack-shared/src/ai/prompts.ts`:
### 1. Clearer env-var guidance in the cloud-project flow (existing
commit)
The previous wording suggested `STACK_PROJECT_ID` should be prefixed via
a generic _"if available, prefix with your framework's convention"_
comment, and the backend section additionally listed
`NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY` — which the SDK does not
actually read in the cloud-project setup. Agents would dutifully
fabricate that third variable.
This is now spelled out:
- The exact prefix per framework (Next.js →
`NEXT_PUBLIC_STACK_PROJECT_ID`, Vite → `VITE_STACK_PROJECT_ID`, etc.) is
given inline.
- A note clarifies that on the client, **only** the project ID is read —
there is no separate publishable / client key.
- A note clarifies that the backend setup reads exactly two variables
(`STACK_PROJECT_ID` + `STACK_SECRET_SERVER_KEY`); a third slot in
`.env.local` is wrong.
### 2. First-class TanStack Start support (new commit)
Until now `mainType: "tanstack-start"` was silently routed through
`@stackframe/react` and inherited the React-only setup steps. Agents had
to guess at the TanStack-specific bits (where to mount `StackProvider`,
what to do with `routeTree.gen.ts`, how `useUser()` behaves under SSR,
where the handler route lives).
`prompts.ts` now:
- Recognizes TanStack Start as its own `mainType` and routes the install
to `@stackframe/tanstack-start`.
- Lists TanStack Start alongside Next.js / React in the
supported-frameworks list and the package table.
- Adds three TanStack-specific steps that don't apply to vanilla React:
1. Mount `StackProvider` / `StackTheme` inside the root route's
`component` (the inner React tree), keeping `shellComponent` as the
document shell.
2. Wrap `<Outlet />` in `Suspense` inside `RootComponent`.
3. Register the Stack handler splat at `src/routes/handler/\$.tsx` with
`ssr: false`.
- Surfaces the two notes that aren't obvious from the React docs:
`routeTree.gen.ts` is generated and shouldn't be hand-edited, and
`useUser()` resolves the SSR user from TanStack Start's request cookies
for free as long as `tokenStore: \"cookie\"` is set.
The auto-generated outputs
(`docs-mintlify/guides/getting-started/setup.mdx`,
`docs-mintlify/snippets/home-prompt-island.jsx`) are regenerated from
the prompt.
### 3. tanstack-start-demo SSR-vs-client examples
Two paired routes (`/ssr` and `/client`) render the same `AuthDemoCard`
so the SSR-vs-\`ssr: false\` tradeoff is observable side-by-side. The
new \`AuthDemoCard\` shows the resolved Stack Auth user (or sign-in/up
buttons) plus the snippet that produced it. The
\`ClientMountedUserButton\` workaround in the header is dropped now that
SSR cookie reading just works, and the empty \`Suspense
fallback={null}\` in \`__root.tsx\` is replaced with a
\`RouteLoadingState\` skeleton.
## Test plan
- [ ] \`pnpm typecheck\` and \`pnpm lint\` both pass on the touched
packages (\`stack-shared\`, \`tanstack-start-demo\`).
- [ ] \`docs-mintlify/guides/getting-started/setup.mdx\` and
\`docs-mintlify/snippets/home-prompt-island.jsx\` are byte-identical to
a fresh \`scripts/generate-setup-prompt-docs.ts\` run.
- [ ] In \`tanstack-start-demo\`, \`/ssr\` renders the user card during
the server response (no flash from signed-out → signed-in), and
\`/client\` renders the empty card on first paint, then resolves to the
user after hydration.
- [ ] \`/handler/sign-in\`, \`/handler/sign-up\`, OAuth callbacks, and
password reset all render correctly through the new splat route.
- [ ] Following the new TanStack Start prompt steps from scratch in an
empty \`npm create @tanstack/start@latest\` project produces a working
sign-in flow without any extra changes.
Made with [Cursor](https://cursor.com)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added TanStack Start support, provider/theme wiring, SSR and
client-only demo pages, plus an Auth demo card component.
* **Documentation**
* Updated setup guides and snippets across frameworks; clarified env-var
guidance (client reads only project ID; secret is server-only) and
removed misleading publishable-key example.
* Clarified OAuth callback and hosted-domain behavior.
* **Improvements**
* Added loading skeleton UI, refined demo navigation, and tightened
setup wording.
<!-- review_stack_entry_start -->
[](https://app.coderabbit.ai/change-stack/hexclave/stack-auth/pull/1438?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)
<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a62702354b
commit
07af46944b
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,61 @@
|
||||
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { UserAvatar, useStackApp } from "@stackframe/tanstack-start";
|
||||
import type { CurrentUser } from "@stackframe/tanstack-start";
|
||||
|
||||
type AuthDemoCardProps = {
|
||||
title: string,
|
||||
eyebrow: string,
|
||||
description: string,
|
||||
user: CurrentUser | null,
|
||||
code: string,
|
||||
};
|
||||
|
||||
export function AuthDemoCard(props: AuthDemoCardProps) {
|
||||
const app = useStackApp();
|
||||
const userLabel = props.user?.displayName ?? props.user?.primaryEmail ?? props.user?.id;
|
||||
|
||||
return (
|
||||
<section className="grid w-full gap-6">
|
||||
<div className="rounded-lg border border-zinc-200 bg-white p-8 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<p className="mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400">{props.eyebrow}</p>
|
||||
<h1 className="text-3xl font-semibold tracking-tight">{props.title}</h1>
|
||||
<p className="mt-4 max-w-2xl text-zinc-600 dark:text-zinc-300">{props.description}</p>
|
||||
|
||||
<div className="mt-8 rounded-lg border border-zinc-200 p-5 dark:border-zinc-800">
|
||||
{props.user ? (
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||
<UserAvatar user={props.user} size={64} />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400">Resolved Stack Auth user</p>
|
||||
<p className="truncate text-xl font-semibold">{userLabel}</p>
|
||||
<p className="mt-1 break-all font-mono text-sm text-zinc-500 dark:text-zinc-400">{props.user.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="text-lg font-semibold">No signed-in user</p>
|
||||
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
This route rendered the signed-out branch from Stack Auth.
|
||||
</p>
|
||||
<div className="mt-4 flex flex-wrap gap-3">
|
||||
<button className="rounded-md bg-zinc-950 px-4 py-2 text-sm font-medium text-white transition-colors hover:transition-none dark:bg-white dark:text-zinc-950" onClick={() => runAsynchronouslyWithAlert(app.redirectToSignIn())}>
|
||||
Sign in
|
||||
</button>
|
||||
<button className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium transition-colors hover:bg-zinc-100 hover:transition-none dark:border-zinc-700 dark:hover:bg-zinc-800" onClick={() => runAsynchronouslyWithAlert(app.redirectToSignUp())}>
|
||||
Sign up
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-950 p-5 text-zinc-100 shadow-sm dark:border-zinc-800">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<h2 className="text-sm font-semibold uppercase text-zinc-400">Usage snippet</h2>
|
||||
</div>
|
||||
<pre className="overflow-x-auto text-sm leading-6"><code>{props.code}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { UserButton } from "@stackframe/tanstack-start";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
@ -11,23 +10,20 @@ export function Header() {
|
||||
<Link to="/" className="font-semibold tracking-tight">
|
||||
Stack 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
|
||||
</Link>
|
||||
<Link to="/client" className="text-sm text-zinc-600 hover:text-zinc-950 hover:transition-none dark:text-zinc-300 dark:hover:text-white">
|
||||
Client
|
||||
</Link>
|
||||
<Link to="/protected" className="text-sm text-zinc-600 hover:text-zinc-950 hover:transition-none dark:text-zinc-300 dark:hover:text-white">
|
||||
Protected
|
||||
</Link>
|
||||
</nav>
|
||||
<ClientMountedUserButton />
|
||||
<UserButton />
|
||||
</div>
|
||||
</header>
|
||||
<div className="h-14" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ClientMountedUserButton() {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
return isMounted ? <UserButton /> : <div className="h-9 w-9" />;
|
||||
}
|
||||
|
||||
@ -9,15 +9,27 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as SsrRouteImport } from './routes/ssr'
|
||||
import { Route as ProtectedRouteImport } from './routes/protected'
|
||||
import { Route as ClientRouteImport } from './routes/client'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as HandlerSplatRouteImport } from './routes/handler/$'
|
||||
|
||||
const SsrRoute = SsrRouteImport.update({
|
||||
id: '/ssr',
|
||||
path: '/ssr',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ProtectedRoute = ProtectedRouteImport.update({
|
||||
id: '/protected',
|
||||
path: '/protected',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const ClientRoute = ClientRouteImport.update({
|
||||
id: '/client',
|
||||
path: '/client',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
@ -31,36 +43,51 @@ const HandlerSplatRoute = HandlerSplatRouteImport.update({
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/client': typeof ClientRoute
|
||||
'/protected': typeof ProtectedRoute
|
||||
'/ssr': typeof SsrRoute
|
||||
'/handler/$': typeof HandlerSplatRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/client': typeof ClientRoute
|
||||
'/protected': typeof ProtectedRoute
|
||||
'/ssr': typeof SsrRoute
|
||||
'/handler/$': typeof HandlerSplatRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/client': typeof ClientRoute
|
||||
'/protected': typeof ProtectedRoute
|
||||
'/ssr': typeof SsrRoute
|
||||
'/handler/$': typeof HandlerSplatRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths: '/' | '/protected' | '/handler/$'
|
||||
fullPaths: '/' | '/client' | '/protected' | '/ssr' | '/handler/$'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/protected' | '/handler/$'
|
||||
id: '__root__' | '/' | '/protected' | '/handler/$'
|
||||
to: '/' | '/client' | '/protected' | '/ssr' | '/handler/$'
|
||||
id: '__root__' | '/' | '/client' | '/protected' | '/ssr' | '/handler/$'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
ClientRoute: typeof ClientRoute
|
||||
ProtectedRoute: typeof ProtectedRoute
|
||||
SsrRoute: typeof SsrRoute
|
||||
HandlerSplatRoute: typeof HandlerSplatRoute
|
||||
}
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/ssr': {
|
||||
id: '/ssr'
|
||||
path: '/ssr'
|
||||
fullPath: '/ssr'
|
||||
preLoaderRoute: typeof SsrRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/protected': {
|
||||
id: '/protected'
|
||||
path: '/protected'
|
||||
@ -68,6 +95,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ProtectedRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/client': {
|
||||
id: '/client'
|
||||
path: '/client'
|
||||
fullPath: '/client'
|
||||
preLoaderRoute: typeof ClientRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/': {
|
||||
id: '/'
|
||||
path: '/'
|
||||
@ -87,7 +121,9 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
ClientRoute: ClientRoute,
|
||||
ProtectedRoute: ProtectedRoute,
|
||||
SsrRoute: SsrRoute,
|
||||
HandlerSplatRoute: HandlerSplatRoute,
|
||||
}
|
||||
export const routeTree = rootRouteImport
|
||||
|
||||
@ -44,15 +44,50 @@ function RootComponent() {
|
||||
return (
|
||||
<StackProvider app={stackApp}>
|
||||
<StackTheme>
|
||||
<div className="min-h-screen bg-zinc-100 text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50">
|
||||
<Header />
|
||||
<main className="mx-auto flex min-h-[calc(100vh-3.5rem)] max-w-5xl px-4 py-8">
|
||||
<Suspense fallback={null}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
<AppShell>
|
||||
<Suspense fallback={<RouteLoadingState />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</AppShell>
|
||||
</StackTheme>
|
||||
</StackProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppShell({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-100 text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50">
|
||||
<Header />
|
||||
<main className="mx-auto flex min-h-[calc(100vh-3.5rem)] max-w-5xl px-4 py-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RouteLoadingState() {
|
||||
return (
|
||||
<section className="grid w-full place-items-center">
|
||||
<div className="w-full max-w-2xl rounded-lg border border-zinc-200 bg-white p-8 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<div className="flex flex-col gap-5 sm:flex-row sm:items-center">
|
||||
<div className="h-24 w-24 shrink-0 rounded-full bg-zinc-200 dark:bg-zinc-800" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="h-4 w-24 rounded bg-zinc-200 dark:bg-zinc-800" />
|
||||
<div className="mt-3 h-9 w-full max-w-md rounded bg-zinc-200 dark:bg-zinc-800" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 grid gap-3 text-sm">
|
||||
<div className="grid gap-1 sm:grid-cols-[8rem_1fr]">
|
||||
<div className="h-5 w-16 rounded bg-zinc-200 dark:bg-zinc-800" />
|
||||
<div className="h-5 w-full rounded bg-zinc-200 dark:bg-zinc-800" />
|
||||
</div>
|
||||
<div className="grid gap-1 sm:grid-cols-[8rem_1fr]">
|
||||
<div className="h-5 w-20 rounded bg-zinc-200 dark:bg-zinc-800" />
|
||||
<div className="h-5 w-12 rounded bg-zinc-200 dark:bg-zinc-800" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 h-9 w-20 rounded-md bg-zinc-200 dark:bg-zinc-800" />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
38
examples/tanstack-start-demo/src/routes/client.tsx
Normal file
38
examples/tanstack-start-demo/src/routes/client.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { useUser } from "@stackframe/tanstack-start";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { AuthDemoCard } from "~/components/auth-demo-card";
|
||||
|
||||
export const Route = createFileRoute("/client")({
|
||||
ssr: false,
|
||||
component: ClientAuthDemoPage,
|
||||
});
|
||||
|
||||
const clientSnippet = `import { useUser } from "@stackframe/tanstack-start";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/client")({
|
||||
ssr: false,
|
||||
component: ClientAuthDemoPage,
|
||||
});
|
||||
|
||||
function ClientAuthDemoPage() {
|
||||
// This route is skipped during SSR. The user is resolved
|
||||
// in the browser from the client token store.
|
||||
const user = useUser({ includeRestricted: true });
|
||||
|
||||
return <div>{user?.displayName ?? "Signed out"}</div>;
|
||||
}`;
|
||||
|
||||
function ClientAuthDemoPage() {
|
||||
const user = useUser({ includeRestricted: true });
|
||||
|
||||
return (
|
||||
<AuthDemoCard
|
||||
eyebrow="Client-only route"
|
||||
title="Stack Auth user fetched in the browser"
|
||||
description="This route opts out of SSR with ssr: false. The UI is rendered on the client, and Stack Auth resolves the current user from the browser token store."
|
||||
user={user}
|
||||
code={clientSnippet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { UserAvatar, useStackApp, useUser } from "@stackframe/tanstack-start";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Link, createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: HomePage,
|
||||
@ -19,6 +19,14 @@ function HomePage() {
|
||||
<p className="mt-4 text-zinc-600 dark:text-zinc-300">
|
||||
This example uses <code className="rounded bg-zinc-100 px-1.5 py-0.5 text-sm dark:bg-zinc-800">@stackframe/tanstack-start</code> with file-based routes and Stack Auth handler pages.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<Link to="/ssr" className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium transition-colors hover:bg-zinc-100 hover:transition-none dark:border-zinc-700 dark:hover:bg-zinc-800">
|
||||
SSR demo
|
||||
</Link>
|
||||
<Link to="/client" className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium transition-colors hover:bg-zinc-100 hover:transition-none dark:border-zinc-700 dark:hover:bg-zinc-800">
|
||||
Client demo
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<button className="rounded-md bg-zinc-950 px-4 py-2 text-sm font-medium text-white transition-colors hover:transition-none dark:bg-white dark:text-zinc-950" onClick={() => runAsynchronouslyWithAlert(app.redirectToSignIn())}>
|
||||
Sign in
|
||||
@ -66,6 +74,12 @@ function HomePage() {
|
||||
</dl>
|
||||
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
<Link to="/ssr" className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium transition-colors hover:bg-zinc-100 hover:transition-none dark:border-zinc-700 dark:hover:bg-zinc-800">
|
||||
SSR demo
|
||||
</Link>
|
||||
<Link to="/client" className="rounded-md border border-zinc-300 px-4 py-2 text-sm font-medium transition-colors hover:bg-zinc-100 hover:transition-none dark:border-zinc-700 dark:hover:bg-zinc-800">
|
||||
Client demo
|
||||
</Link>
|
||||
<button className="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 hover:transition-none" onClick={() => runAsynchronouslyWithAlert(app.redirectToSignOut())}>
|
||||
Sign out
|
||||
</button>
|
||||
|
||||
36
examples/tanstack-start-demo/src/routes/ssr.tsx
Normal file
36
examples/tanstack-start-demo/src/routes/ssr.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { useUser } from "@stackframe/tanstack-start";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { AuthDemoCard } from "~/components/auth-demo-card";
|
||||
|
||||
export const Route = createFileRoute("/ssr")({
|
||||
component: SsrAuthDemoPage,
|
||||
});
|
||||
|
||||
const ssrSnippet = `import { useUser } from "@stackframe/tanstack-start";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/ssr")({
|
||||
component: SsrAuthDemoPage,
|
||||
});
|
||||
|
||||
function SsrAuthDemoPage() {
|
||||
// This hook can suspend during SSR while Stack Auth reads
|
||||
// the TanStack Start request cookies and fetches the user.
|
||||
const user = useUser({ includeRestricted: true });
|
||||
|
||||
return <div>{user?.displayName ?? "Signed out"}</div>;
|
||||
}`;
|
||||
|
||||
function SsrAuthDemoPage() {
|
||||
const user = useUser({ includeRestricted: true });
|
||||
|
||||
return (
|
||||
<AuthDemoCard
|
||||
eyebrow="SSR route"
|
||||
title="Stack Auth user fetched during server render"
|
||||
description="This route keeps SSR enabled. The Stack Auth hook can resolve the current user from TanStack Start request cookies while React renders the route on the server."
|
||||
user={user}
|
||||
code={ssrSnippet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -166,8 +166,10 @@ export const supabaseSetupPrompt = deindent`
|
||||
Also add the Stack Auth environment variables:
|
||||
|
||||
\`\`\`.env .env.local
|
||||
# The project ID is the only client-exposed Stack Auth variable; in Next.js it must
|
||||
# be prefixed with NEXT_PUBLIC_. STACK_SECRET_SERVER_KEY is server-only and must
|
||||
# NEVER be prefixed or exposed to the client.
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=<your-stack-project-id>
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=<your-publishable-client-key>
|
||||
STACK_SECRET_SERVER_KEY=<your-secret-server-key>
|
||||
\`\`\`
|
||||
</Step>
|
||||
@ -369,6 +371,10 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
|
||||
const isMaybeReact = isDefinitelyReact || mainType === "ai-prompt";
|
||||
const isDefinitelyNextjs = mainType === "nextjs";
|
||||
const isMaybeNextjs = isDefinitelyNextjs || mainType === "ai-prompt";
|
||||
const isDefinitelyTanstackStart = mainType === "tanstack-start";
|
||||
const isMaybeTanstackStart = isDefinitelyTanstackStart || mainType === "ai-prompt";
|
||||
const isDefinitelyVanillaReact = mainType === "react";
|
||||
const isMaybeVanillaReact = isDefinitelyVanillaReact || mainType === "ai-prompt";
|
||||
|
||||
const isDefinitelyBackend = mainType === "nodejs" || mainType === "bun" || mainType === "nextjs";
|
||||
const isMaybeBackend = isDefinitelyBackend || mainType === "js" || mainType === "ai-prompt";
|
||||
@ -391,7 +397,7 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
|
||||
nextjs: "@stackframe/stack",
|
||||
react: "@stackframe/react",
|
||||
js: "@stackframe/js",
|
||||
"tanstack-start": "@stackframe/react",
|
||||
"tanstack-start": "@stackframe/tanstack-start",
|
||||
nodejs: "@stackframe/js",
|
||||
bun: "@stackframe/js",
|
||||
}[mainType];
|
||||
@ -408,6 +414,7 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
|
||||
|
||||
- Next.js
|
||||
- React
|
||||
- TanStack Start
|
||||
- Other JS & TS (both frontend and backend)
|
||||
` : ""}
|
||||
|
||||
@ -422,6 +429,7 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
|
||||
|
||||
- Next.js: \`@stackframe/stack\`
|
||||
- React: \`@stackframe/react\`
|
||||
- TanStack Start: \`@stackframe/tanstack-start\`
|
||||
- Other & vanilla JS: \`@stackframe/js\`
|
||||
|
||||
You can install the correct JavaScript Stack Auth SDK into your project by running the following command:
|
||||
@ -541,9 +549,15 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
|
||||
Go to your project's dashboard on [app.stack-auth.com](https://app.stack-auth.com) and get the project ID. You can find it in the URL after the \`/projects/\` part. Copy-paste it into your \`.env.local\` file (or wherever your environment variables are stored):
|
||||
|
||||
\`\`\`.env .env.local
|
||||
STACK_PROJECT_ID=<your-project-id> # if available, prefix with your framework's convention for client-exposed variables (e.g. NEXT_PUBLIC_, VITE_, etc.)
|
||||
# Prefix the variable name with your framework's convention for client-exposed
|
||||
# variables. For Next.js use NEXT_PUBLIC_STACK_PROJECT_ID, for Vite use
|
||||
# VITE_STACK_PROJECT_ID, etc. If your framework has no such convention, use
|
||||
# STACK_PROJECT_ID as-is.
|
||||
STACK_PROJECT_ID=<your-project-id>
|
||||
\`\`\`
|
||||
|
||||
This is the **only** environment variable the client SDK reads in the cloud-project setup. Do not invent or add any other Stack Auth env vars on the client (in particular, there is **no** separate publishable / client key — the project ID alone is sufficient on the client).
|
||||
|
||||
Alternatively, you can also just set the project ID in the \`stack/client.ts\` file:
|
||||
|
||||
\`\`\`ts src/stack/client.ts
|
||||
@ -561,10 +575,19 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
|
||||
Then, copy-paste them into your \`.env.local\` file (or wherever your environment variables are stored):
|
||||
|
||||
\`\`\`.env .env.local
|
||||
STACK_PROJECT_ID=<your-project-id> # if desired, prefix with your framework's convention for client-exposed variables (e.g. NEXT_PUBLIC_, VITE_, etc.)
|
||||
# Server SDKs (StackServerApp, Node, etc.) read the UNPREFIXED names:
|
||||
STACK_PROJECT_ID=<your-project-id>
|
||||
STACK_SECRET_SERVER_KEY=<your-secret-server-key>
|
||||
|
||||
# Client-side frameworks must use their own publishable prefix for the
|
||||
# project ID so the bundler exposes it to the browser — for example:
|
||||
# Next.js: NEXT_PUBLIC_STACK_PROJECT_ID=<your-project-id>
|
||||
# Vite: VITE_STACK_PROJECT_ID=<your-project-id>
|
||||
# STACK_SECRET_SERVER_KEY must NEVER be prefixed or exposed to the client.
|
||||
\`\`\`
|
||||
|
||||
These two values (project ID + secret server key) are the **complete** set the SDK reads in the cloud-project setup. Do not add any additional Stack Auth env vars (in particular, there is **no** separate publishable / client key — the project ID alone is sufficient on the client). The dashboard "Project Keys" page exposes exactly these two values; if a third slot is present in any \`.env.local\` you write, it is wrong.
|
||||
|
||||
They'll automatically be picked up by the \`StackServerApp\` constructor.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@ -574,7 +597,7 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
|
||||
<Step title="${!isDefinitelyReact ? "React: " : ""}Creating a <StackProvider /> and <StackTheme />">
|
||||
In React frameworks, Stack Auth provides \`StackProvider\` and \`StackTheme\` components that should wrap your entire app at the root level.
|
||||
|
||||
${!isDefinitelyNextjs ? deindent`
|
||||
${isMaybeVanillaReact && !isDefinitelyNextjs && !isDefinitelyTanstackStart ? deindent`
|
||||
For example, if you have an \`App.tsx\` file, update it as follows:
|
||||
|
||||
\`\`\`tsx src/App.tsx
|
||||
@ -612,6 +635,48 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
|
||||
}
|
||||
\`\`\`
|
||||
` : ""}
|
||||
|
||||
${isMaybeTanstackStart ? deindent`
|
||||
${!isDefinitelyTanstackStart ? "For TanStack Start specifically: " : ""}TanStack Start uses file-based routes. The provider goes inside the root route's \`component\` (the inner React tree), while the document shell stays in \`shellComponent\`. Update \`src/routes/__root.tsx\`:
|
||||
|
||||
\`\`\`tsx src/routes/__root.tsx
|
||||
import { StackProvider, StackTheme } from "${isDefinitelyTanstackStart ? packageName : "@stackframe/tanstack-start"}";
|
||||
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
|
||||
import type { ReactNode } from "react";
|
||||
import { stackClientApp } from "../stack/client";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
shellComponent: RootDocument,
|
||||
component: RootComponent,
|
||||
});
|
||||
|
||||
function RootDocument({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<StackProvider app={stackClientApp}>
|
||||
<StackTheme>
|
||||
<Outlet />
|
||||
</StackTheme>
|
||||
</StackProvider>
|
||||
);
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
Do not edit \`src/routeTree.gen.ts\` — it is regenerated automatically by the TanStack Start router from the files under \`src/routes/\`.
|
||||
` : ""}
|
||||
</Step>
|
||||
|
||||
<Step title="${!isDefinitelyReact ? "React: " : ""}Add Suspense boundary">
|
||||
@ -619,7 +684,7 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
|
||||
|
||||
To support the suspension, you need to add a suspense boundary around your app.
|
||||
|
||||
${!isDefinitelyNextjs ? deindent`
|
||||
${isMaybeVanillaReact && !isDefinitelyNextjs && !isDefinitelyTanstackStart ? deindent`
|
||||
The easiest way to do this is to just wrap your entire app in a \`Suspense\` component:
|
||||
|
||||
\`\`\`tsx src/App.tsx
|
||||
@ -651,10 +716,57 @@ export function getSdkSetupPrompt(mainType: "ai-prompt" | "nextjs" | "react" | "
|
||||
\`\`\`
|
||||
` : ""}
|
||||
|
||||
${isMaybeTanstackStart ? deindent`
|
||||
${!isDefinitelyTanstackStart ? "In TanStack Start: " : ""}wrap the \`<Outlet />\` in your root route with a \`Suspense\` boundary so the document shell can stream while child routes wait on Stack Auth. Update \`RootComponent\` in \`src/routes/__root.tsx\`:
|
||||
|
||||
\`\`\`tsx src/routes/__root.tsx
|
||||
import { Suspense } from "react";
|
||||
// ...other imports...
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<StackProvider app={stackClientApp}>
|
||||
<StackTheme>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</StackTheme>
|
||||
</StackProvider>
|
||||
);
|
||||
}
|
||||
\`\`\`
|
||||
` : ""}
|
||||
|
||||
${isAiPrompt ? deindent`
|
||||
Note: If you are an AI agent, make sure to make the loading indicator visually appealing and match the design of your app, instead of the example that just says "Loading...".
|
||||
` : ""}
|
||||
</Step>
|
||||
|
||||
${isMaybeTanstackStart ? deindent`
|
||||
<Step title="${!isDefinitelyTanstackStart ? "TanStack Start: " : ""}Add the Stack handler route">
|
||||
Stack Auth's auth flows (sign-in, sign-up, OAuth callbacks, password reset, etc.) are rendered by a single \`StackHandler\` component mounted at \`/handler/*\`. In TanStack Start, expose it as a splat file route at \`src/routes/handler/$.tsx\`:
|
||||
|
||||
\`\`\`tsx src/routes/handler/$.tsx
|
||||
import { StackHandler } from "${isDefinitelyTanstackStart ? packageName : "@stackframe/tanstack-start"}";
|
||||
import { createFileRoute, useLocation } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/handler/$")({
|
||||
ssr: false,
|
||||
component: HandlerPage,
|
||||
});
|
||||
|
||||
function HandlerPage() {
|
||||
const { pathname } = useLocation();
|
||||
return <StackHandler fullPage location={pathname} />;
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
Two TanStack-specific notes:
|
||||
|
||||
- The route is opted out of SSR with \`ssr: false\`. The handler runs browser-only auth flows (cookies, redirects, popups), so rendering it on the server provides no benefit and can fight with hydration. Other routes can opt into or out of SSR per-route the same way.
|
||||
- Stack Auth resolves the current user during SSR by reading TanStack Start's request cookies through \`@stackframe/tanstack-start\`'s server context. No extra wiring is required — \`useUser()\` "just works" on both server and client routes as long as \`tokenStore: "cookie"\` is set on \`StackClientApp\`.
|
||||
</Step>
|
||||
` : ""}
|
||||
` : ""}
|
||||
|
||||
${isMaybeBackend && !isDefinitelyNextjs ? deindent`
|
||||
|
||||
@ -41,7 +41,7 @@ const stackAuthReminders = deindent`
|
||||
- Take extra care to always have great error handling and loading states whenever necessary (including in button onClick handlers; Stack Auth's code examples often use a special onClick class which handles loading states, but your own button may not). Stack Auth's SDK tends to return errors that need to be handled explicitly in its return types.
|
||||
- Language, framework, and library-specific details:
|
||||
- JavaScript & TypeScript:
|
||||
- Stack Auth has different SDK packages for different frameworks and languages. As of the time of writing these reminders, they are: @stackframe/js (JavaScript/TypeScript), @stackframe/stack (Next.js), @stackframe/react (React). You can find all of these on npm. They are all versioned together, meaning that vX.Y.Z of one SDK was released at the same time as vX.Y.Z of another SDK. For the most part, they are the same, although each has platform-specific features and differences.
|
||||
- Stack Auth has different SDK packages for different frameworks and languages. As of the time of writing these reminders, they are: @stackframe/js (JavaScript/TypeScript), @stackframe/stack (Next.js), @stackframe/react (React), @stackframe/tanstack-start (TanStack Start). You can find all of these on npm. They are all versioned together, meaning that vX.Y.Z of one SDK was released at the same time as vX.Y.Z of another SDK. For the most part, they are the same, although each has platform-specific features and differences.
|
||||
- The \`Result<T, E>\` type is \`{ status: "ok", data: T } | { status: "error", error: E }\`.
|
||||
- \`KnownErrors[KNOWN_ERROR_CODE]\` refers to a specific known error type. Each KnownError may have its own properties, but they all inherit from \`Error & { statusCode: number, humanReadableMessage: string, details?: Json }\`.
|
||||
- React & Next.js:
|
||||
@ -104,6 +104,20 @@ function createCustomPagePrompt(options: {
|
||||
},
|
||||
\`\`\`
|
||||
|
||||
**Important — overriding one URL target does NOT override the others.** Every handler URL is independent and falls back to \`urls.default\`. If \`default\` is \`{ type: "hosted" }\`, customizing only this page will cause the browser to **visibly redirect through \`<projectId>.built-with-stack-auth.com\`** during OAuth, magic-link, sign-out, email-verification, password-reset, and similar flows — even though your sign-in / sign-up pages render locally. That hosted subdomain must also be on the project's Trusted Domains list, or the API rejects the redirect with \`REDIRECT_URL_NOT_WHITELISTED\`.
|
||||
|
||||
To keep auth flows entirely on your own origin, override every related URL target. The complete set of handler URLs and the SDK call each custom page must invoke:
|
||||
|
||||
| URL target | What the custom page must do |
|
||||
|---|---|
|
||||
| \`signIn\`, \`signUp\` | Render the forms described in this prompt (or its sign-in / sign-up counterpart). |
|
||||
| \`oauthCallback\` | On mount, call \`await stackApp.callOAuthCallback()\`. The SDK exchanges the \`code\`/\`state\` query params for tokens and then redirects to \`afterSignIn\`. |
|
||||
| \`signOut\` | Read the current user, then sign them out via \`use(cacheSignOut(user))\` (which calls \`user.signOut()\` inside a cached function so the page stays idempotent on refresh). Show a confirmation state with a "Go home" button that calls \`stackApp.redirectToHome()\`. |
|
||||
| \`magicLinkCallback\` | Complete the magic-link exchange when the link is opened directly (separate from the OTP flow inside the sign-in page). Call \`await stackApp.signInWithMagicLink(code)\` with the \`code\` query param. |
|
||||
| \`forgotPassword\`, \`passwordReset\`, \`emailVerification\`, \`accountSettings\`, \`teamInvitation\`, \`mfa\`, \`error\`, \`onboarding\`, \`cliAuthConfirm\` | Each is its own URL target; customize as needed. |
|
||||
|
||||
Any URL target you do NOT customize will keep bouncing through the hosted domain — that may be intentional, but it should be a deliberate choice, not an accident. Always whitelist every origin you redirect to (your app's origin in production, \`http://localhost:<port>\` in dev, and the \`<projectId>.built-with-stack-auth.com\` host if you keep any handlers on hosted). In development you can also flip the "Allow localhost callbacks" toggle on the Trusted Domains page.
|
||||
|
||||
${stackAuthReminders}
|
||||
`;
|
||||
const versions = {
|
||||
@ -155,7 +169,7 @@ function createAuthPagePrompt(type: AuthPagePromptType): CustomPagePrompt {
|
||||
? "- If sign-ups are enabled (\\`project = await stackApp.getProject(); project.config.signUpEnabled\\`), show a link to the sign-up page."
|
||||
: "- If sign-ups are disabled (\\`project = await stackApp.getProject(); !project.config.signUpEnabled\\`), show a message that sign-up is disabled."}
|
||||
- Show a ${authVerb} screen. The auth methods that should render:
|
||||
- For each OAuth provider (\`project.config.oauthProviders: { readonly id: string }[]\`), render an OAuth button. Clicking the button calls \`await stackApp.signInWithOAuth("<providerId>")\`.
|
||||
- For each OAuth provider (\`project.config.oauthProviders: { readonly id: string }[]\`), render an OAuth button. Clicking the button calls \`await stackApp.signInWithOAuth("<providerId>")\`. Note: this triggers a redirect through \`urls.oauthCallback\`. If that target is hosted (the default unless you override it), the browser will visibly visit \`<projectId>.built-with-stack-auth.com\` before returning. To keep the flow on your origin, also customize \`urls.oauthCallback\` with a page that calls \`stackApp.callOAuthCallback()\` on mount — see the URL-config note at the end of this prompt.
|
||||
${isSignIn ? "- If \\`project.config.passkeyEnabled\\`, render a passkey button. Clicking the button calls \\`await stackApp.signInWithPasskey()\\`." : ""}
|
||||
- If \`project.config.credentialEnabled\`, render a credential ${authVerb} form:
|
||||
- Email + password${isSignIn ? "" : " + repeat password"}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user