Skip to content

Commit 07af469

Browse files
[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 --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](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>
1 parent a627023 commit 07af469

11 files changed

Lines changed: 569 additions & 73 deletions

File tree

docs-mintlify/guides/getting-started/setup.mdx

Lines changed: 195 additions & 41 deletions
Large diffs are not rendered by default.

docs-mintlify/snippets/home-prompt-island.jsx

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
2+
import { UserAvatar, useStackApp } from "@stackframe/tanstack-start";
3+
import type { CurrentUser } from "@stackframe/tanstack-start";
4+
5+
type AuthDemoCardProps = {
6+
title: string,
7+
eyebrow: string,
8+
description: string,
9+
user: CurrentUser | null,
10+
code: string,
11+
};
12+
13+
export function AuthDemoCard(props: AuthDemoCardProps) {
14+
const app = useStackApp();
15+
const userLabel = props.user?.displayName ?? props.user?.primaryEmail ?? props.user?.id;
16+
17+
return (
18+
<section className="grid w-full gap-6">
19+
<div className="rounded-lg border border-zinc-200 bg-white p-8 shadow-sm dark:border-zinc-800 dark:bg-zinc-900">
20+
<p className="mb-2 text-sm font-medium text-zinc-500 dark:text-zinc-400">{props.eyebrow}</p>
21+
<h1 className="text-3xl font-semibold tracking-tight">{props.title}</h1>
22+
<p className="mt-4 max-w-2xl text-zinc-600 dark:text-zinc-300">{props.description}</p>
23+
24+
<div className="mt-8 rounded-lg border border-zinc-200 p-5 dark:border-zinc-800">
25+
{props.user ? (
26+
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
27+
<UserAvatar user={props.user} size={64} />
28+
<div className="min-w-0">
29+
<p className="text-sm text-zinc-500 dark:text-zinc-400">Resolved Stack Auth user</p>
30+
<p className="truncate text-xl font-semibold">{userLabel}</p>
31+
<p className="mt-1 break-all font-mono text-sm text-zinc-500 dark:text-zinc-400">{props.user.id}</p>
32+
</div>
33+
</div>
34+
) : (
35+
<div>
36+
<p className="text-lg font-semibold">No signed-in user</p>
37+
<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
38+
This route rendered the signed-out branch from Stack Auth.
39+
</p>
40+
<div className="mt-4 flex flex-wrap gap-3">
41+
<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())}>
42+
Sign in
43+
</button>
44+
<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())}>
45+
Sign up
46+
</button>
47+
</div>
48+
</div>
49+
)}
50+
</div>
51+
</div>
52+
53+
<div className="rounded-lg border border-zinc-200 bg-zinc-950 p-5 text-zinc-100 shadow-sm dark:border-zinc-800">
54+
<div className="mb-3 flex items-center justify-between gap-3">
55+
<h2 className="text-sm font-semibold uppercase text-zinc-400">Usage snippet</h2>
56+
</div>
57+
<pre className="overflow-x-auto text-sm leading-6"><code>{props.code}</code></pre>
58+
</div>
59+
</section>
60+
);
61+
}
Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Link } from "@tanstack/react-router";
22
import { UserButton } from "@stackframe/tanstack-start";
3-
import { useEffect, useState } from "react";
43

54
export function Header() {
65
return (
@@ -11,23 +10,20 @@ export function Header() {
1110
<Link to="/" className="font-semibold tracking-tight">
1211
Stack TanStack Demo
1312
</Link>
13+
<Link to="/ssr" className="text-sm text-zinc-600 hover:text-zinc-950 hover:transition-none dark:text-zinc-300 dark:hover:text-white">
14+
SSR
15+
</Link>
16+
<Link to="/client" className="text-sm text-zinc-600 hover:text-zinc-950 hover:transition-none dark:text-zinc-300 dark:hover:text-white">
17+
Client
18+
</Link>
1419
<Link to="/protected" className="text-sm text-zinc-600 hover:text-zinc-950 hover:transition-none dark:text-zinc-300 dark:hover:text-white">
1520
Protected
1621
</Link>
1722
</nav>
18-
<ClientMountedUserButton />
23+
<UserButton />
1924
</div>
2025
</header>
2126
<div className="h-14" />
2227
</>
2328
);
2429
}
25-
26-
function ClientMountedUserButton() {
27-
const [isMounted, setIsMounted] = useState(false);
28-
useEffect(() => {
29-
setIsMounted(true);
30-
}, []);
31-
32-
return isMounted ? <UserButton /> : <div className="h-9 w-9" />;
33-
}

examples/tanstack-start-demo/src/routeTree.gen.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,27 @@
99
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
1010

1111
import { Route as rootRouteImport } from './routes/__root'
12+
import { Route as SsrRouteImport } from './routes/ssr'
1213
import { Route as ProtectedRouteImport } from './routes/protected'
14+
import { Route as ClientRouteImport } from './routes/client'
1315
import { Route as IndexRouteImport } from './routes/index'
1416
import { Route as HandlerSplatRouteImport } from './routes/handler/$'
1517

18+
const SsrRoute = SsrRouteImport.update({
19+
id: '/ssr',
20+
path: '/ssr',
21+
getParentRoute: () => rootRouteImport,
22+
} as any)
1623
const ProtectedRoute = ProtectedRouteImport.update({
1724
id: '/protected',
1825
path: '/protected',
1926
getParentRoute: () => rootRouteImport,
2027
} as any)
28+
const ClientRoute = ClientRouteImport.update({
29+
id: '/client',
30+
path: '/client',
31+
getParentRoute: () => rootRouteImport,
32+
} as any)
2133
const IndexRoute = IndexRouteImport.update({
2234
id: '/',
2335
path: '/',
@@ -31,43 +43,65 @@ const HandlerSplatRoute = HandlerSplatRouteImport.update({
3143

3244
export interface FileRoutesByFullPath {
3345
'/': typeof IndexRoute
46+
'/client': typeof ClientRoute
3447
'/protected': typeof ProtectedRoute
48+
'/ssr': typeof SsrRoute
3549
'/handler/$': typeof HandlerSplatRoute
3650
}
3751
export interface FileRoutesByTo {
3852
'/': typeof IndexRoute
53+
'/client': typeof ClientRoute
3954
'/protected': typeof ProtectedRoute
55+
'/ssr': typeof SsrRoute
4056
'/handler/$': typeof HandlerSplatRoute
4157
}
4258
export interface FileRoutesById {
4359
__root__: typeof rootRouteImport
4460
'/': typeof IndexRoute
61+
'/client': typeof ClientRoute
4562
'/protected': typeof ProtectedRoute
63+
'/ssr': typeof SsrRoute
4664
'/handler/$': typeof HandlerSplatRoute
4765
}
4866
export interface FileRouteTypes {
4967
fileRoutesByFullPath: FileRoutesByFullPath
50-
fullPaths: '/' | '/protected' | '/handler/$'
68+
fullPaths: '/' | '/client' | '/protected' | '/ssr' | '/handler/$'
5169
fileRoutesByTo: FileRoutesByTo
52-
to: '/' | '/protected' | '/handler/$'
53-
id: '__root__' | '/' | '/protected' | '/handler/$'
70+
to: '/' | '/client' | '/protected' | '/ssr' | '/handler/$'
71+
id: '__root__' | '/' | '/client' | '/protected' | '/ssr' | '/handler/$'
5472
fileRoutesById: FileRoutesById
5573
}
5674
export interface RootRouteChildren {
5775
IndexRoute: typeof IndexRoute
76+
ClientRoute: typeof ClientRoute
5877
ProtectedRoute: typeof ProtectedRoute
78+
SsrRoute: typeof SsrRoute
5979
HandlerSplatRoute: typeof HandlerSplatRoute
6080
}
6181

6282
declare module '@tanstack/react-router' {
6383
interface FileRoutesByPath {
84+
'/ssr': {
85+
id: '/ssr'
86+
path: '/ssr'
87+
fullPath: '/ssr'
88+
preLoaderRoute: typeof SsrRouteImport
89+
parentRoute: typeof rootRouteImport
90+
}
6491
'/protected': {
6592
id: '/protected'
6693
path: '/protected'
6794
fullPath: '/protected'
6895
preLoaderRoute: typeof ProtectedRouteImport
6996
parentRoute: typeof rootRouteImport
7097
}
98+
'/client': {
99+
id: '/client'
100+
path: '/client'
101+
fullPath: '/client'
102+
preLoaderRoute: typeof ClientRouteImport
103+
parentRoute: typeof rootRouteImport
104+
}
71105
'/': {
72106
id: '/'
73107
path: '/'
@@ -87,7 +121,9 @@ declare module '@tanstack/react-router' {
87121

88122
const rootRouteChildren: RootRouteChildren = {
89123
IndexRoute: IndexRoute,
124+
ClientRoute: ClientRoute,
90125
ProtectedRoute: ProtectedRoute,
126+
SsrRoute: SsrRoute,
91127
HandlerSplatRoute: HandlerSplatRoute,
92128
}
93129
export const routeTree = rootRouteImport

examples/tanstack-start-demo/src/routes/__root.tsx

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,50 @@ function RootComponent() {
4444
return (
4545
<StackProvider app={stackApp}>
4646
<StackTheme>
47-
<div className="min-h-screen bg-zinc-100 text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50">
48-
<Header />
49-
<main className="mx-auto flex min-h-[calc(100vh-3.5rem)] max-w-5xl px-4 py-8">
50-
<Suspense fallback={null}>
51-
<Outlet />
52-
</Suspense>
53-
</main>
54-
</div>
47+
<AppShell>
48+
<Suspense fallback={<RouteLoadingState />}>
49+
<Outlet />
50+
</Suspense>
51+
</AppShell>
5552
</StackTheme>
5653
</StackProvider>
5754
);
5855
}
56+
57+
function AppShell({ children }: { children: ReactNode }) {
58+
return (
59+
<div className="min-h-screen bg-zinc-100 text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50">
60+
<Header />
61+
<main className="mx-auto flex min-h-[calc(100vh-3.5rem)] max-w-5xl px-4 py-8">
62+
{children}
63+
</main>
64+
</div>
65+
);
66+
}
67+
68+
function RouteLoadingState() {
69+
return (
70+
<section className="grid w-full place-items-center">
71+
<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">
72+
<div className="flex flex-col gap-5 sm:flex-row sm:items-center">
73+
<div className="h-24 w-24 shrink-0 rounded-full bg-zinc-200 dark:bg-zinc-800" />
74+
<div className="min-w-0 flex-1">
75+
<div className="h-4 w-24 rounded bg-zinc-200 dark:bg-zinc-800" />
76+
<div className="mt-3 h-9 w-full max-w-md rounded bg-zinc-200 dark:bg-zinc-800" />
77+
</div>
78+
</div>
79+
<div className="mt-8 grid gap-3 text-sm">
80+
<div className="grid gap-1 sm:grid-cols-[8rem_1fr]">
81+
<div className="h-5 w-16 rounded bg-zinc-200 dark:bg-zinc-800" />
82+
<div className="h-5 w-full rounded bg-zinc-200 dark:bg-zinc-800" />
83+
</div>
84+
<div className="grid gap-1 sm:grid-cols-[8rem_1fr]">
85+
<div className="h-5 w-20 rounded bg-zinc-200 dark:bg-zinc-800" />
86+
<div className="h-5 w-12 rounded bg-zinc-200 dark:bg-zinc-800" />
87+
</div>
88+
</div>
89+
<div className="mt-8 h-9 w-20 rounded-md bg-zinc-200 dark:bg-zinc-800" />
90+
</div>
91+
</section>
92+
);
93+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useUser } from "@stackframe/tanstack-start";
2+
import { createFileRoute } from "@tanstack/react-router";
3+
import { AuthDemoCard } from "~/components/auth-demo-card";
4+
5+
export const Route = createFileRoute("/client")({
6+
ssr: false,
7+
component: ClientAuthDemoPage,
8+
});
9+
10+
const clientSnippet = `import { useUser } from "@stackframe/tanstack-start";
11+
import { createFileRoute } from "@tanstack/react-router";
12+
13+
export const Route = createFileRoute("/client")({
14+
ssr: false,
15+
component: ClientAuthDemoPage,
16+
});
17+
18+
function ClientAuthDemoPage() {
19+
// This route is skipped during SSR. The user is resolved
20+
// in the browser from the client token store.
21+
const user = useUser({ includeRestricted: true });
22+
23+
return <div>{user?.displayName ?? "Signed out"}</div>;
24+
}`;
25+
26+
function ClientAuthDemoPage() {
27+
const user = useUser({ includeRestricted: true });
28+
29+
return (
30+
<AuthDemoCard
31+
eyebrow="Client-only route"
32+
title="Stack Auth user fetched in the browser"
33+
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."
34+
user={user}
35+
code={clientSnippet}
36+
/>
37+
);
38+
}

examples/tanstack-start-demo/src/routes/index.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
22
import { UserAvatar, useStackApp, useUser } from "@stackframe/tanstack-start";
3-
import { createFileRoute } from "@tanstack/react-router";
3+
import { Link, createFileRoute } from "@tanstack/react-router";
44

55
export const Route = createFileRoute("/")({
66
component: HomePage,
@@ -19,6 +19,14 @@ function HomePage() {
1919
<p className="mt-4 text-zinc-600 dark:text-zinc-300">
2020
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.
2121
</p>
22+
<div className="mt-6 flex flex-wrap gap-3">
23+
<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">
24+
SSR demo
25+
</Link>
26+
<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">
27+
Client demo
28+
</Link>
29+
</div>
2230
<div className="mt-6 flex flex-wrap gap-3">
2331
<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())}>
2432
Sign in
@@ -66,6 +74,12 @@ function HomePage() {
6674
</dl>
6775

6876
<div className="mt-8 flex flex-wrap gap-3">
77+
<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">
78+
SSR demo
79+
</Link>
80+
<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">
81+
Client demo
82+
</Link>
6983
<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())}>
7084
Sign out
7185
</button>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { useUser } from "@stackframe/tanstack-start";
2+
import { createFileRoute } from "@tanstack/react-router";
3+
import { AuthDemoCard } from "~/components/auth-demo-card";
4+
5+
export const Route = createFileRoute("/ssr")({
6+
component: SsrAuthDemoPage,
7+
});
8+
9+
const ssrSnippet = `import { useUser } from "@stackframe/tanstack-start";
10+
import { createFileRoute } from "@tanstack/react-router";
11+
12+
export const Route = createFileRoute("/ssr")({
13+
component: SsrAuthDemoPage,
14+
});
15+
16+
function SsrAuthDemoPage() {
17+
// This hook can suspend during SSR while Stack Auth reads
18+
// the TanStack Start request cookies and fetches the user.
19+
const user = useUser({ includeRestricted: true });
20+
21+
return <div>{user?.displayName ?? "Signed out"}</div>;
22+
}`;
23+
24+
function SsrAuthDemoPage() {
25+
const user = useUser({ includeRestricted: true });
26+
27+
return (
28+
<AuthDemoCard
29+
eyebrow="SSR route"
30+
title="Stack Auth user fetched during server render"
31+
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."
32+
user={user}
33+
code={ssrSnippet}
34+
/>
35+
);
36+
}

0 commit comments

Comments
 (0)