Skip to content

Commit 68ae6d1

Browse files
authored
[codex] Add TanStack Start SDK integration (#1399)
## Summary - Adds the generated `@stackframe/tanstack-start` workspace package registration. - Adds TanStack Start platform macros/dependencies to the SDK template and generator. - Adds TanStack Start cookie/token-store support plus the handler SSR guard needed by Start. ## Scope This intentionally excludes Dashboard V2 routes, hooks, components, app shell logic, and dashboard API type additions. Those stay in the existing dashboard PR/branch. ## Validation - `pnpm install --lockfile-only --ignore-scripts` - `pnpm install --ignore-scripts` - `pnpm -C packages/template lint src/components-page/stack-handler-client.tsx src/lib/cookie.ts src/lib/stack-app/apps/implementations/client-app-impl.ts` Package typecheck was attempted with `pnpm -C packages/template typecheck`, but the clean worktree lacks generated package declaration outputs for workspace dependencies such as `@stackframe/stack-shared` and `@stackframe/stack-ui`. Per repo instructions, package builds/codegen are not run by agents. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * TanStack Start integration: published SDK package, example demo app, dashboard onboarding flow, framework-aware CTAs/docs, and a TanStack-specific provider for client-only auth routes. * Improved client/server auth: safer runtime guards and consistent cookie/token-store behavior across SSR and client. * **Documentation** * New Integrations guide and expanded getting-started/setup docs with TanStack Start examples and env/key guidance. * **Chores** * Template, build, tooling, and demo config updates to support the new platform. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent acc646c commit 68ae6d1

53 files changed

Lines changed: 1535 additions & 44 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,12 @@ packages/js/*
140140
packages/react/*
141141
packages/next/*
142142
packages/stack/*
143+
packages/tanstack-start/*
143144
!packages/js/package.json
144145
!packages/react/package.json
145146
!packages/next/package.json
146147
!packages/stack/package.json
148+
!packages/tanstack-start/package.json
147149

148150
# claude code
149151
.claude/scheduled_tasks.lock
14.2 KB
Loading

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx

Lines changed: 188 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { CodeBlock } from '@/components/code-block';
4-
import { APIEnvKeys, NextJsEnvKeys } from '@/components/env-keys';
4+
import { APIEnvKeys, NextJsEnvKeys, ViteEnvKeys } from '@/components/env-keys';
55
import { InlineCode } from '@/components/inline-code';
66
import { StyledLink } from '@/components/link';
77
import { CopyPromptButton, Tabs, TabsContent, TabsList, TabsTrigger, Typography, cn } from "@/components/ui";
@@ -27,22 +27,58 @@ const nameClasses = "text-green-600 dark:text-green-500";
2727

2828
const INSTALL_COMMAND_BY_FRAMEWORK = {
2929
nextjs: 'npx @stackframe/stack-cli@latest init',
30+
tanstackStart: 'npm install @stackframe/tanstack-start',
3031
react: 'npm install @stackframe/react',
3132
javascript: 'npm install @stackframe/js',
3233
python: 'pip install requests',
3334
} as const;
3435

35-
const buildInstallPrompt = (command: string) => deindent`
36+
type SetupFramework = keyof typeof INSTALL_COMMAND_BY_FRAMEWORK;
37+
38+
const TANSTACK_START_SETUP_PROMPT = deindent`
39+
Please set up Stack Auth in my TanStack Start app.
40+
41+
1. Install the alpha TanStack Start package:
42+
43+
npm install @stackframe/tanstack-start
44+
45+
2. Configure the app with these environment variables:
46+
47+
VITE_STACK_PROJECT_ID=<project-id>
48+
STACK_SECRET_SERVER_KEY=<secret-server-key>
49+
50+
3. Create a StackClientApp using @stackframe/tanstack-start with:
51+
- projectId: import.meta.env.VITE_STACK_PROJECT_ID
52+
- tokenStore: "cookie"
53+
- redirectMethod: "window"
54+
55+
4. Wrap the TanStack Start root route with StackProvider and StackTheme.
56+
57+
5. Add a /handler/$ route using StackHandler. The handler route must set ssr: false and pass location={pathname} from useLocation().
58+
59+
Use only the environment variables listed above.
60+
61+
After it finishes, verify that the Stack Auth MCP server is registered in your AI client config — name: \`stack-auth\`, transport: \`http\`, URL: \`https://mcp.stack-auth.com/\`. If it is not registered, please add it manually so you have live access to Stack Auth docs and APIs.
62+
`;
63+
64+
const buildInstallPrompt = (framework: SetupFramework) => {
65+
if (framework === "tanstackStart") {
66+
return TANSTACK_START_SETUP_PROMPT;
67+
}
68+
69+
const command = INSTALL_COMMAND_BY_FRAMEWORK[framework];
70+
return deindent`
3671
Please run the following command in my project's terminal:
3772
3873
${command}
3974
4075
After it finishes, verify that the Stack Auth MCP server is registered in your AI client config — name: \`stack-auth\`, transport: \`http\`, URL: \`https://mcp.stack-auth.com/mcp\`. The command above should handle this automatically; if for any reason it didn't, please add the MCP server manually so you have live access to Stack Auth docs and APIs.
4176
`;
77+
};
4278

4379
export default function SetupPage(props: { toMetrics: () => void }) {
4480
const adminApp = useAdminApp();
45-
const [selectedFramework, setSelectedFramework] = useState<'nextjs' | 'react' | 'javascript' | 'python'>('nextjs');
81+
const [selectedFramework, setSelectedFramework] = useState<'nextjs' | 'tanstackStart' | 'react' | 'javascript' | 'python'>('nextjs');
4682
const [keys, setKeys] = useState<{ projectId: string, publishableClientKey?: string, secretServerKey: string } | null>(null);
4783
const projectConfig = adminApp.useProject().useConfig();
4884
const requirePublishableClientKey = projectConfig.project.requirePublishableClientKey;
@@ -220,6 +256,142 @@ export default function SetupPage(props: { toMetrics: () => void }) {
220256
}
221257
];
222258

259+
const tanstackStartSteps = [
260+
{
261+
step: 2,
262+
title: "Install Stack Auth",
263+
content: <>
264+
<Typography>
265+
In a new or existing TanStack Start project, install the alpha Stack Auth package:
266+
</Typography>
267+
<CodeBlock
268+
language="bash"
269+
content={`npm install @stackframe/tanstack-start`}
270+
customRender={
271+
<div className="p-4 font-mono text-sm">
272+
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@stackframe/tanstack-start</span>
273+
</div>
274+
}
275+
title="Terminal"
276+
icon="terminal"
277+
/>
278+
</>
279+
},
280+
{
281+
step: 3,
282+
title: "Create Keys",
283+
content: <>
284+
<Typography>
285+
Put these keys in your TanStack Start environment file.
286+
</Typography>
287+
<StackAuthKeys keys={keys} onGenerateKeys={onGenerateKeys} type="vite" />
288+
</>
289+
},
290+
{
291+
step: 4,
292+
title: "Create stack/client.ts file",
293+
content: <>
294+
<Typography>
295+
Create a new file called <InlineCode>src/stack/client.ts</InlineCode> and initialize Stack Auth with cookie storage.
296+
</Typography>
297+
<CodeBlock
298+
language="tsx"
299+
content={deindent`
300+
import { StackClientApp } from "@stackframe/tanstack-start";
301+
302+
export const stackClientApp = new StackClientApp({
303+
projectId: import.meta.env.VITE_STACK_PROJECT_ID,
304+
tokenStore: "cookie",
305+
redirectMethod: "window",
306+
});
307+
`}
308+
title="src/stack/client.ts"
309+
icon="code"
310+
/>
311+
</>
312+
},
313+
{
314+
step: 5,
315+
title: "Update the root route",
316+
content: <>
317+
<Typography>
318+
Wrap your TanStack Start root route with <InlineCode>StackProvider</InlineCode> and <InlineCode>StackTheme</InlineCode>.
319+
</Typography>
320+
<CodeBlock
321+
language="tsx"
322+
maxHeight={300}
323+
content={deindent`
324+
import { StackProvider, StackTheme } from "@stackframe/tanstack-start";
325+
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
326+
import { stackClientApp } from "../stack/client";
327+
328+
export const Route = createRootRoute({
329+
component: RootComponent,
330+
shellComponent: RootDocument,
331+
});
332+
333+
function RootComponent() {
334+
return (
335+
<StackProvider app={stackClientApp}>
336+
<StackTheme>
337+
<Outlet />
338+
</StackTheme>
339+
</StackProvider>
340+
);
341+
}
342+
343+
function RootDocument({ children }: { children: React.ReactNode }) {
344+
return (
345+
<html>
346+
<head>
347+
<HeadContent />
348+
</head>
349+
<body>
350+
{children}
351+
<Scripts />
352+
</body>
353+
</html>
354+
);
355+
}
356+
`}
357+
title="src/routes/__root.tsx"
358+
icon="code"
359+
/>
360+
</>
361+
},
362+
{
363+
step: 6,
364+
title: "Add the handler route",
365+
content: <>
366+
<Typography>
367+
Create a splat route for Stack Auth&apos;s built-in auth pages.
368+
</Typography>
369+
<CodeBlock
370+
language="tsx"
371+
content={deindent`
372+
import { StackHandler } from "@stackframe/tanstack-start";
373+
import { createFileRoute, useLocation } from "@tanstack/react-router";
374+
375+
export const Route = createFileRoute("/handler/$")({
376+
ssr: false,
377+
component: HandlerPage,
378+
});
379+
380+
function HandlerPage() {
381+
const { pathname } = useLocation();
382+
return <StackHandler fullPage location={pathname} />;
383+
}
384+
`}
385+
title="src/routes/handler/$.tsx"
386+
icon="code"
387+
/>
388+
<Typography>
389+
If you start your TanStack Start app and navigate to <StyledLink href="http://localhost:3000/handler/sign-up">http://localhost:3000/handler/sign-up</StyledLink>, you will see the sign-up page.
390+
</Typography>
391+
</>
392+
},
393+
];
394+
223395
const javascriptSteps = [
224396
{
225397
step: 2,
@@ -480,7 +652,7 @@ export default function SetupPage(props: { toMetrics: () => void }) {
480652
<CopyPromptButton
481653
variant="outline"
482654
size="sm"
483-
content={buildInstallPrompt(INSTALL_COMMAND_BY_FRAMEWORK[selectedFramework])}
655+
content={buildInstallPrompt(selectedFramework)}
484656
>
485657
<SparkleIcon className="w-4 h-4 mr-2 text-purple-500 dark:text-purple-400" weight="fill" />
486658
Copy prompt
@@ -500,6 +672,11 @@ export default function SetupPage(props: { toMetrics: () => void }) {
500672
name: 'Next.js',
501673
reverseIfDark: true,
502674
imgSrc: '/next-logo.svg',
675+
}, {
676+
id: 'tanstackStart',
677+
name: 'TanStack Start',
678+
reverseIfDark: false,
679+
imgSrc: '/tanstack-start-logo.png',
503680
}, {
504681
id: 'react',
505682
name: 'React',
@@ -538,6 +715,7 @@ export default function SetupPage(props: { toMetrics: () => void }) {
538715
</div>,
539716
},
540717
...(selectedFramework === 'nextjs' ? nextJsSteps : []),
718+
...(selectedFramework === 'tanstackStart' ? tanstackStartSteps : []),
541719
...(selectedFramework === 'react' ? reactSteps : []),
542720
...(selectedFramework === 'javascript' ? javascriptSteps : []),
543721
...(selectedFramework === 'python' ? pythonSteps : []),
@@ -638,7 +816,7 @@ function GlobeIllustrationInner() {
638816
function StackAuthKeys(props: {
639817
keys: { projectId: string, publishableClientKey?: string, secretServerKey: string } | null,
640818
onGenerateKeys: () => Promise<void>,
641-
type: 'next' | 'raw',
819+
type: 'next' | 'vite' | 'raw',
642820
}) {
643821
return (
644822
<div className="w-full border rounded-xl p-8 gap-4 flex flex-col">
@@ -650,6 +828,11 @@ function StackAuthKeys(props: {
650828
publishableClientKey={props.keys.publishableClientKey}
651829
secretServerKey={props.keys.secretServerKey}
652830
/>
831+
) : props.type === 'vite' ? (
832+
<ViteEnvKeys
833+
projectId={props.keys.projectId}
834+
secretServerKey={props.keys.secretServerKey}
835+
/>
653836
) : (
654837
<APIEnvKeys
655838
projectId={props.keys.projectId}

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useAdminApp } from "@/app/(main)/(protected)/projects/[projectId]/use-a
44
import { AppStoreEntry } from "@/components/app-store-entry";
55
import { useRouter } from "@/components/router";
66
import { useUpdateConfig } from "@/lib/config-update";
7-
import { ALL_APPS_FRONTEND, getAppPath, isSubApp, type AppId } from "@/lib/apps-frontend";
7+
import { ALL_APPS_FRONTEND, getAppPath, getDocumentationHref, isSubApp, type AppId } from "@/lib/apps-frontend";
88
import { isAppEnabled } from "@/lib/apps-utils";
99
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
1010
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
@@ -28,6 +28,8 @@ export default function AppDetailsPageClient({ appId }: { appId: AppId }) {
2828
const parentAppFrontend = parentAppId == null ? null : ALL_APPS_FRONTEND[parentAppId];
2929
const parentAppEnabled = parentAppId == null ? false : isAppEnabled(config.apps.installed, parentAppId);
3030
const appPath = getAppPath(project.id, appFrontend);
31+
const documentationHref = getDocumentationHref(appFrontend);
32+
const appDestination = documentationHref ?? appPath;
3133
const subAppDestinationPath = parentAppFrontend == null
3234
? null
3335
: parentAppEnabled
@@ -40,11 +42,19 @@ export default function AppDetailsPageClient({ appId }: { appId: AppId }) {
4042
configUpdate: { [`apps.installed.${appId}.enabled`]: true },
4143
pushable: true,
4244
});
43-
router.push(appPath);
45+
if (documentationHref != null) {
46+
window.location.href = documentationHref;
47+
} else {
48+
router.push(appPath);
49+
}
4450
};
4551

4652
const handleOpen = () => {
47-
router.push(subAppDestinationPath ?? appPath);
53+
if (documentationHref != null) {
54+
window.location.href = documentationHref;
55+
} else {
56+
router.push(subAppDestinationPath ?? appDestination);
57+
}
4858
};
4959

5060
const handleDisable = async () => {

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,11 @@ type AppSection = {
5858
items: {
5959
name: string,
6060
href: string,
61+
external?: boolean,
6162
match: (fullUrl: URL) => boolean,
6263
}[],
6364
firstItemHref?: string,
65+
firstItemExternal?: boolean,
6466
};
6567

6668
type BottomItem = {
@@ -209,6 +211,7 @@ function NavItem({
209211
if (isCollapsed) {
210212
// For sections, navigate to the first item when collapsed
211213
const collapsedHref = isSection && item.firstItemHref ? item.firstItemHref : href;
214+
const collapsedTarget = isSection && item.firstItemExternal ? "_blank" : undefined;
212215

213216
return (
214217
<div className="flex justify-center">
@@ -226,7 +229,7 @@ function NavItem({
226229
: "hover:bg-white/40 dark:hover:bg-background/60 text-muted-foreground hover:text-foreground"
227230
)}
228231
>
229-
<Link href={collapsedHref ?? "#"} onClick={onClick}>
232+
<Link href={collapsedHref ?? "#"} target={collapsedTarget} onClick={onClick}>
230233
<IconComponent className={iconClasses} />
231234
</Link>
232235
</Button>
@@ -351,6 +354,7 @@ function NavSubItem({
351354
return (
352355
<Link
353356
href={href}
357+
target={item.external ? "_blank" : undefined}
354358
onClick={onClick}
355359
className={cn(
356360
"group flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-150 hover:transition-none",
@@ -404,6 +408,7 @@ function AppNavItem({
404408
const items = navigableFrontend.navigationItems.map((navItem) => ({
405409
name: navItem.displayName,
406410
href: getItemPath(projectId, navigableFrontend, navItem),
411+
external: navItem.external,
407412
match: (fullUrl: URL) => testItemPath(projectId, navigableFrontend, navItem, fullUrl),
408413
}));
409414
return {
@@ -413,6 +418,7 @@ function AppNavItem({
413418
href: getAppPath(projectId, appFrontend),
414419
icon: appFrontend.icon,
415420
firstItemHref: items[0]?.href,
421+
firstItemExternal: items[0]?.external,
416422
};
417423
}, [app.displayName, appId, appFrontend, projectId]);
418424

apps/dashboard/src/components/app-square.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useAdminApp, useProjectId } from "@/app/(main)/(protected)/projects/[projectId]/use-admin-app";
22
import { useRouter } from "@/components/router";
33
import { Button, cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui";
4-
import { ALL_APPS_FRONTEND, AppFrontend, getAppPath, isSubApp } from "@/lib/apps-frontend";
4+
import { ALL_APPS_FRONTEND, AppFrontend, getAppPath, getDocumentationHref, isSubApp } from "@/lib/apps-frontend";
55
import { isAppEnabled } from "@/lib/apps-utils";
66
import { useUpdateConfig } from "@/lib/config-update";
77
import { CheckIcon, DotsThreeVerticalIcon } from "@phosphor-icons/react";
@@ -220,6 +220,7 @@ export function AppListItem({
220220

221221
const isEnabled = isAppEnabled(config.apps.installed, appId);
222222
const appPath = getAppPath(project.id, appFrontend);
223+
const appDestinationPath = getDocumentationHref(appFrontend) ?? appPath;
223224
const appDetailsPath = `/projects/${project.id}/apps/${appId}`;
224225
const router = useRouter();
225226
const parentAppId = isSubApp(appFrontend) ? appFrontend.parentAppId : null;
@@ -249,7 +250,7 @@ export function AppListItem({
249250

250251
return (
251252
<Link
252-
href={parentDestinationPath ?? (isEnabled ? appPath : appDetailsPath)}
253+
href={parentDestinationPath ?? (isEnabled ? appDestinationPath : appDetailsPath)}
253254
className={cn(
254255
"flex items-center gap-3 p-3 rounded-lg transition-all",
255256
"hover:bg-gray-50 dark:hover:bg-gray-800/50",

0 commit comments

Comments
 (0)