Skip to content

Commit 1db0f30

Browse files
committed
Add TanStack Start SDK integration
1 parent e831972 commit 1db0f30

12 files changed

Lines changed: 1120 additions & 213 deletions

File tree

.claude/CLAUDE-KNOWLEDGE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,3 +361,6 @@ A: Invalid `tools` entries are rejected by `requestBodySchema` in `apps/backend/
361361

362362
## Q: Why did the internal metrics E2E snapshots need to change in April 2026?
363363
A: The `/api/v1/internal/metrics` response now intentionally includes `analytics_overview.daily_anonymous_visitors_fallback`, `analytics_overview.anonymous_visitors_fallback`, and `active_users_by_country`. Those additions are reflected in `packages/stack-shared/src/interface/admin-metrics.ts` and the backend route, so the E2E snapshots must include them instead of treating them as regressions.
364+
365+
## Q: How should a TanStack Start SDK package be added without dragging Dashboard V2 logic into the same PR?
366+
A: Keep the integration PR scoped to generated package registration (`packages/tanstack-start/package.json`, `.gitignore`, `scripts/generate-sdks.ts`, `scripts/utils.ts`), template/package dependency metadata, and SDK runtime changes needed by TanStack Start (`cookie.ts`, token-store handling, handler SSR guard). Leave dashboard routes, hooks, app wiring, and admin API types in the dashboard PR.

.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
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
{
2+
"//": "THIS FILE IS AUTO-GENERATED FROM TEMPLATE. DO NOT EDIT IT DIRECTLY, INSTEAD EDIT THE CORRESPONDING FILE IN packages/template (FOR package.json FILES, PLEASE EDIT package-template.json)",
3+
"name": "@stackframe/tanstack-start",
4+
"version": "2.8.86",
5+
"repository": "https://github.com/stack-auth/stack-auth",
6+
"sideEffects": false,
7+
"main": "./dist/index.js",
8+
"types": "./dist/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./dist/index.d.ts",
12+
"import": "./dist/esm/index.js",
13+
"require": "./dist/index.js"
14+
},
15+
"./convex.config": {
16+
"types": "./dist/integrations/convex/component/convex.config.d.ts",
17+
"import": "./dist/esm/integrations/convex/component/convex.config.js",
18+
"require": "./dist/integrations/convex/component/convex.config.js"
19+
},
20+
"./convex-auth.config": {
21+
"types": "./dist/integrations/convex.d.ts",
22+
"import": "./dist/esm/integrations/convex.js",
23+
"require": "./dist/integrations/convex.js"
24+
}
25+
},
26+
"homepage": "https://stack-auth.com",
27+
"scripts": {
28+
"typecheck": "tsc --noEmit",
29+
"clean": "rimraf dist && rimraf node_modules",
30+
"lint": "eslint --ext .tsx,.ts .",
31+
"build": "rimraf dist && pnpm run css && tsdown",
32+
"dev": "rimraf dist && concurrently -n \"build,codegen\" -k \"tsdown --watch\" \"pnpm run codegen:watch\"",
33+
"codegen": "pnpm run css",
34+
"codegen:watch": "pnpm run css:watch",
35+
"css": "pnpm run css-tw && pnpm run css-sc",
36+
"css:watch": "concurrently -n \"tw,sc\" -k \"pnpm run css-tw:watch\" \"pnpm run css-sc:watch\"",
37+
"css-tw:watch": "tailwindcss -i ./src/global.css -o ./src/generated/tailwind.css --watch",
38+
"css-tw": "tailwindcss -i ./src/global.css -o ./src/generated/tailwind.css",
39+
"css-sc": "tsx ./scripts/process-css.ts ./src/generated/tailwind.css ./src/generated/global-css.ts",
40+
"css-sc:watch": "chokidar --silent './src/generated/tailwind.css' -c 'pnpm run css-sc' --throttle 2000"
41+
},
42+
"files": [
43+
"README.md",
44+
"dist",
45+
"CHANGELOG.md",
46+
"LICENSE"
47+
],
48+
"dependencies": {
49+
"@ai-sdk/react": "^3.0.72",
50+
"ai": "^6.0.0",
51+
"@hookform/resolvers": "^5.2.2",
52+
"@stripe/react-stripe-js": "^3.8.1",
53+
"@stripe/stripe-js": "^7.7.0",
54+
"@simplewebauthn/browser": "^13.2.2",
55+
"@stackframe/stack-shared": "workspace:*",
56+
"@stackframe/stack-ui": "workspace:*",
57+
"@tanstack/react-table": "^8.21.3",
58+
"browser-image-compression": "^2.0.2",
59+
"color": "^5.0.3",
60+
"cookie": "^1.1.1",
61+
"jose": "^6.1.3",
62+
"js-cookie": "^3.0.5",
63+
"lucide-react": "^0.378.0",
64+
"oauth4webapi": "^3.8.3",
65+
"@oslojs/otp": "^1.1.0",
66+
"qrcode": "^1.5.4",
67+
"react-easy-crop": "^5.5.6",
68+
"react-hook-form": "^7.70.0",
69+
"tailwindcss-animate": "^1.0.7",
70+
"rrweb": "^1.1.3",
71+
"tsx": "^4.21.0",
72+
"yup": "^1.7.1"
73+
},
74+
"peerDependencies": {
75+
"@types/react": ">=18.3.0",
76+
"@tanstack/react-router": ">=1.100.0",
77+
"@tanstack/react-start": ">=1.100.0",
78+
"react": ">=18.3.0"
79+
},
80+
"peerDependenciesMeta": {
81+
"@tanstack/react-router": {
82+
"optional": true
83+
},
84+
"@tanstack/react-start": {
85+
"optional": true
86+
},
87+
"@types/react": {
88+
"optional": true
89+
}
90+
},
91+
"devDependencies": {
92+
"@quetzallabs/i18n": "^0.1.19",
93+
"@types/color": "^3.0.6",
94+
"@types/cookie": "^0.6.0",
95+
"@types/js-cookie": "^3.0.6",
96+
"@types/qrcode": "^1.5.5",
97+
"@types/react-avatar-editor": "^13.0.3",
98+
"autoprefixer": "^10.4.17",
99+
"chokidar-cli": "^3.0.0",
100+
"esbuild": "^0.20.2",
101+
"i18next": "^23.14.0",
102+
"i18next-parser": "^9.0.2",
103+
"@tanstack/react-router": "^1.167.4",
104+
"@tanstack/react-start": "^1.166.15",
105+
"postcss": "^8.4.38",
106+
"postcss-nested": "^6.0.1",
107+
"react": "^19.0.0",
108+
"@types/react-dom": "^19.0.0",
109+
"react-dom": "^19.0.0",
110+
"rimraf": "^6.1.2",
111+
"tailwindcss": "^3.4.4",
112+
"tsdown": "^0.20.3",
113+
"convex": "^1.27.0"
114+
}
115+
}

packages/template/package-template.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"name": "@stackframe/js",
66
"//": "ELSE_IF_PLATFORM next",
77
"name": "@stackframe/stack",
8+
"//": "ELSE_IF_PLATFORM tanstack-start",
9+
"name": "@stackframe/tanstack-start",
810
"//": "ELSE_IF_PLATFORM react",
911
"name": "@stackframe/react",
1012
"//": "END_PLATFORM",
@@ -131,6 +133,10 @@
131133
"react-dom": ">=18.3.0",
132134
"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0",
133135
"//": "END_PLATFORM",
136+
"//": "IF_PLATFORM tanstack-start",
137+
"@tanstack/react-router": ">=1.100.0",
138+
"@tanstack/react-start": ">=1.100.0",
139+
"//": "END_PLATFORM",
134140
"react": ">=18.3.0"
135141
},
136142
"//": "END_PLATFORM",
@@ -141,6 +147,14 @@
141147
"optional": true
142148
},
143149
"//": "END_PLATFORM",
150+
"//": "IF_PLATFORM tanstack-start",
151+
"@tanstack/react-router": {
152+
"optional": true
153+
},
154+
"@tanstack/react-start": {
155+
"optional": true
156+
},
157+
"//": "END_PLATFORM",
144158
"@types/react": {
145159
"optional": true
146160
}
@@ -160,6 +174,10 @@
160174
"i18next-parser": "^9.0.2",
161175
"//": "NEXT_LINE_PLATFORM next",
162176
"next": "^14.2.35",
177+
"//": "NEXT_LINE_PLATFORM template tanstack-start",
178+
"@tanstack/react-router": "^1.167.4",
179+
"//": "NEXT_LINE_PLATFORM template tanstack-start",
180+
"@tanstack/react-start": "^1.166.15",
163181
"postcss": "^8.4.38",
164182
"postcss-nested": "^6.0.1",
165183
"react": "^19.0.0",

packages/template/package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,20 @@
9494
"@types/react-dom": ">=18.3.0",
9595
"react-dom": ">=18.3.0",
9696
"next": ">=14.1 || >=15.0.0-canary.0 || >=15.0.0-rc.0",
97+
"@tanstack/react-router": ">=1.100.0",
98+
"@tanstack/react-start": ">=1.100.0",
9799
"react": ">=18.3.0"
98100
},
99101
"peerDependenciesMeta": {
100102
"@types/react-dom": {
101103
"optional": true
102104
},
105+
"@tanstack/react-router": {
106+
"optional": true
107+
},
108+
"@tanstack/react-start": {
109+
"optional": true
110+
},
103111
"@types/react": {
104112
"optional": true
105113
}
@@ -117,6 +125,8 @@
117125
"i18next": "^23.14.0",
118126
"i18next-parser": "^9.0.2",
119127
"next": "^14.2.35",
128+
"@tanstack/react-router": "^1.167.4",
129+
"@tanstack/react-start": "^1.166.15",
120130
"postcss": "^8.4.38",
121131
"postcss-nested": "^6.0.1",
122132
"react": "^19.0.0",
@@ -127,4 +137,4 @@
127137
"tsdown": "^0.20.3",
128138
"convex": "^1.27.0"
129139
}
130-
}
140+
}

packages/template/src/components-page/stack-handler-client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ export function StackHandlerClient(props: BaseHandlerProps & Partial<RouteProps>
237237
const navigateRef = useRef(navigate);
238238
navigateRef.current = navigate;
239239
const currentLocation = props.location ?? window.location.pathname;
240-
const searchParamsSource = new URLSearchParams(window.location.search);
240+
const searchParamsSource = new URLSearchParams(typeof window === "undefined" ? "" : window.location.search);
241241
const redirectTargets: (string | undefined)[] = [];
242242
END_PLATFORM */
243243

packages/template/src/lib/cookie.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { cookies as rscCookies, headers as rscHeaders } from '@stackframe/stack-sc/force-react-server'; // THIS_LINE_PLATFORM next
2+
import { getCookie as tssGetCookie, getCookies as tssGetCookies, setCookie as tssSetCookie, deleteCookie as tssDeleteCookie, getRequestHeader as tssGetRequestHeader } from '@tanstack/react-start/server'; // THIS_LINE_PLATFORM tanstack-start
23
import { isBrowserLike } from '@stackframe/stack-shared/dist/utils/env';
34
import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors';
45
import Cookies from "js-cookie";
@@ -104,12 +105,73 @@ export async function createCookieHelper(): Promise<CookieHelper> {
104105
await rscCookies(),
105106
await rscHeaders(),
106107
);
108+
// ELSE_IF_PLATFORM tanstack-start
109+
return createTanStackStartCookieHelper();
107110
// ELSE_PLATFORM
108111
return await createPlaceholderCookieHelper();
109112
// END_PLATFORM
110113
}
111114
}
112115

116+
export function createCookieHelperSync(): CookieHelper {
117+
if (isBrowserLike()) {
118+
return createBrowserCookieHelper();
119+
}
120+
// IF_PLATFORM tanstack-start
121+
return createTanStackStartCookieHelper();
122+
// ELSE_PLATFORM
123+
function throwError(): never {
124+
throw new StackAssertionError("Synchronous server cookie helpers are not available on this platform");
125+
}
126+
return {
127+
get: throwError,
128+
getAll: throwError,
129+
set: throwError,
130+
setOrDelete: throwError,
131+
delete: throwError,
132+
};
133+
// END_PLATFORM
134+
}
135+
136+
// IF_PLATFORM tanstack-start
137+
function determineSecureFromTanStackStartContext(): boolean {
138+
return tssGetRequestHeader("x-forwarded-proto") === "https"
139+
|| (tssGetCookie("stack-is-https") !== undefined);
140+
}
141+
142+
function requiresSecureAttribute(name: string): boolean {
143+
return name.startsWith("__Host-");
144+
}
145+
146+
function createTanStackStartCookieHelper(): CookieHelper {
147+
const helper: CookieHelper = {
148+
get: (name: string) => tssGetCookie(name) ?? null,
149+
getAll: () => tssGetCookies(),
150+
set: (name: string, value: string, options: SetCookieOptions) => {
151+
tssSetCookie(name, value, {
152+
secure: options.secure ?? (requiresSecureAttribute(name) || determineSecureFromTanStackStartContext()),
153+
maxAge: options.maxAge === "session" ? undefined : options.maxAge,
154+
domain: options.domain,
155+
sameSite: "lax",
156+
path: "/",
157+
});
158+
},
159+
setOrDelete: (name, value, options) => {
160+
if (value === null) helper.delete(name, options);
161+
else helper.set(name, value, options);
162+
},
163+
delete: (name: string, options: DeleteCookieOptions) => {
164+
tssDeleteCookie(name, {
165+
secure: requiresSecureAttribute(name),
166+
domain: options.domain,
167+
path: "/",
168+
});
169+
},
170+
};
171+
return helper;
172+
}
173+
// END_PLATFORM
174+
113175
export function createBrowserCookieHelper(): CookieHelper {
114176
return {
115177
get: getCookieClient,
@@ -232,6 +294,8 @@ export async function isSecure(): Promise<boolean> {
232294
}
233295
// IF_PLATFORM next
234296
return determineSecureFromServerContext(await rscCookies(), await rscHeaders());
297+
// ELSE_IF_PLATFORM tanstack-start
298+
return determineSecureFromTanStackStartContext();
235299
// END_PLATFORM
236300
return false;
237301
}

packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import React, { useCallback, useMemo } from "react"; // THIS_LINE_PLATFORM react
4242
import type * as yup from "yup";
4343
import { constructRedirectUrl } from "../../../../utils/url";
4444
import { getNewOAuthProviderOrScopeUrl, callOAuthCallback } from "../../../auth";
45-
import { CookieHelper, createBrowserCookieHelper, createCookieHelper, createPlaceholderCookieHelper, deleteCookie, deleteCookieClient, isSecure as isSecureCookieContext, saveVerifierAndState, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie";
45+
import { CookieHelper, createBrowserCookieHelper, createCookieHelper, createCookieHelperSync, createPlaceholderCookieHelper, deleteCookie, deleteCookieClient, isSecure as isSecureCookieContext, saveVerifierAndState, setOrDeleteCookie, setOrDeleteCookieClient } from "../../../cookie";
4646
import { envVars } from "../../../env";
4747
import { ApiKey, ApiKeyCreationOptions, ApiKeyUpdateOptions, apiKeyCreationOptionsToCrud } from "../../api-keys";
4848
import { ConvexCtx, GetCurrentPartialUserOptions, GetCurrentUserOptions, HandlerUrlOptions, HandlerUrls, OAuthScopesOnSignIn, RedirectMethod, RedirectToOptions, RequestLike, ResolvedHandlerUrls, TokenStoreInit, stackAppInternalsSymbol } from "../../common";
@@ -930,6 +930,11 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
930930

931931
switch (tokenStoreInit) {
932932
case "cookie": {
933+
// IF_PLATFORM tanstack-start
934+
if (!isBrowserLike()) {
935+
return this._getOrCreateTokenStore(cookieHelper, "nextjs-cookie");
936+
}
937+
// END_PLATFORM
933938
return this._getBrowserCookieTokenStore();
934939
}
935940
case "nextjs-cookie": {
@@ -1024,6 +1029,11 @@ export class _StackClientAppImplIncomplete<HasTokenStore extends boolean, Projec
10241029

10251030
// IF_PLATFORM react-like
10261031
protected _useTokenStore(overrideTokenStoreInit?: TokenStoreInit): Store<TokenObject> {
1032+
// IF_PLATFORM tanstack-start
1033+
if (!isBrowserLike()) {
1034+
return this._getOrCreateTokenStore(createCookieHelperSync(), overrideTokenStoreInit);
1035+
}
1036+
// END_PLATFORM
10271037
suspendIfSsr();
10281038
const cookieHelper = createBrowserCookieHelper();
10291039
const tokenStore = this._getOrCreateTokenStore(cookieHelper, overrideTokenStoreInit);

0 commit comments

Comments
 (0)