From b3ae93570402c7c91eef3eb5d589c08afe7812d2 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 10 Oct 2025 06:28:30 +0530 Subject: [PATCH] feat(cli): add fullstack tanstack start --- apps/cli/src/helpers/core/api-setup.ts | 7 +- apps/cli/src/helpers/core/create-project.ts | 3 + apps/cli/src/helpers/core/env-setup.ts | 12 +- .../cli/src/helpers/core/post-installation.ts | 12 +- apps/cli/src/helpers/core/template-manager.ts | 18 ++- apps/cli/src/helpers/core/workspace-setup.ts | 34 ++-- apps/cli/src/prompts/backend.ts | 4 +- .../cli/src/utils/better-auth-plugin-setup.ts | 81 ++++++++++ apps/cli/src/utils/compatibility-rules.ts | 8 +- .../src/app/api/rpc/[[...rest]]/route.ts.hbs | 2 +- .../src/routes/api/rpc/$.ts.hbs | 58 +++++++ .../api/orpc/server/src/context.ts.hbs | 18 +++ .../orpc/web/react/base/src/utils/orpc.ts.hbs | 36 +++++ .../src/routes/api/trpc/$.ts.hbs | 22 +++ .../api/trpc/server/src/context.ts.hbs | 21 +++ .../src/routes/api/auth/$.ts.hbs | 15 ++ .../better-auth/server/base/src/index.ts.hbs | 57 ++----- .../backend/server/elysia/src/index.ts.hbs | 2 +- .../backend/server/express/src/index.ts.hbs | 2 +- .../backend/server/fastify/src/index.ts.hbs | 2 +- .../backend/server/hono/src/index.ts.hbs | 2 +- .../tanstack-start/src/routes/api/ai/$.ts.hbs | 31 ++++ .../tanstack-start/src/routes/ai.tsx.hbs | 2 +- .../react/tanstack-start/src/router.tsx.hbs | 2 +- .../src/app/(home)/new/_components/utils.ts | 153 +++++++++++++++--- apps/web/src/lib/constant.ts | 40 ++++- apps/web/src/lib/stack-utils.ts | 10 +- 27 files changed, 539 insertions(+), 115 deletions(-) create mode 100644 apps/cli/src/utils/better-auth-plugin-setup.ts create mode 100644 apps/cli/templates/api/orpc/fullstack/tanstack-start/src/routes/api/rpc/$.ts.hbs create mode 100644 apps/cli/templates/api/trpc/fullstack/tanstack-start/src/routes/api/trpc/$.ts.hbs create mode 100644 apps/cli/templates/auth/better-auth/fullstack/tanstack-start/src/routes/api/auth/$.ts.hbs create mode 100644 apps/cli/templates/examples/ai/fullstack/tanstack-start/src/routes/api/ai/$.ts.hbs diff --git a/apps/cli/src/helpers/core/api-setup.ts b/apps/cli/src/helpers/core/api-setup.ts index 1a7f11d9c..bbb57330d 100644 --- a/apps/cli/src/helpers/core/api-setup.ts +++ b/apps/cli/src/helpers/core/api-setup.ts @@ -68,7 +68,9 @@ function getApiDependencies( if (frontendType.hasReactWeb) { if (api === "orpc") { - deps.web = { dependencies: ["@orpc/tanstack-query", "@orpc/client"] }; + deps.web = { + dependencies: ["@orpc/tanstack-query", "@orpc/client", "@orpc/server"], + }; } else if (api === "trpc") { deps.web = { dependencies: [ @@ -84,6 +86,7 @@ function getApiDependencies( "@tanstack/vue-query", "@orpc/tanstack-query", "@orpc/client", + "@orpc/server", ], devDependencies: ["@tanstack/vue-query-devtools"], }; @@ -92,6 +95,7 @@ function getApiDependencies( dependencies: [ "@orpc/tanstack-query", "@orpc/client", + "@orpc/server", "@tanstack/svelte-query", ], devDependencies: ["@tanstack/svelte-query-devtools"], @@ -101,6 +105,7 @@ function getApiDependencies( dependencies: [ "@orpc/tanstack-query", "@orpc/client", + "@orpc/server", "@tanstack/solid-query", ], devDependencies: [ diff --git a/apps/cli/src/helpers/core/create-project.ts b/apps/cli/src/helpers/core/create-project.ts index d67f94e25..39649757b 100644 --- a/apps/cli/src/helpers/core/create-project.ts +++ b/apps/cli/src/helpers/core/create-project.ts @@ -1,6 +1,7 @@ import { log } from "@clack/prompts"; import fs from "fs-extra"; import type { ProjectConfig } from "../../types"; +import { setupBetterAuthPlugins } from "../../utils/better-auth-plugin-setup"; import { writeBtsConfig } from "../../utils/bts-config"; import { exitWithError } from "../../utils/errors"; import { setupCatalogs } from "../../utils/setup-catalogs"; @@ -86,6 +87,8 @@ export async function createProject( await setupAuth(options); } + await setupBetterAuthPlugins(projectDir, options); + if (options.payments && options.payments !== "none") { await setupPayments(options); } diff --git a/apps/cli/src/helpers/core/env-setup.ts b/apps/cli/src/helpers/core/env-setup.ts index 59a2872f7..93927a3f9 100644 --- a/apps/cli/src/helpers/core/env-setup.ts +++ b/apps/cli/src/helpers/core/env-setup.ts @@ -10,6 +10,7 @@ function getClientServerVar( const hasNextJs = frontend.includes("next"); const hasNuxt = frontend.includes("nuxt"); const hasSvelte = frontend.includes("svelte"); + const hasTanstackStart = frontend.includes("tanstack-start"); // For fullstack self, no base URL is needed (same-origin) if (backend === "self") { @@ -20,6 +21,7 @@ function getClientServerVar( if (hasNextJs) key = "NEXT_PUBLIC_SERVER_URL"; else if (hasNuxt) key = "NUXT_PUBLIC_SERVER_URL"; else if (hasSvelte) key = "PUBLIC_SERVER_URL"; + else if (hasTanstackStart) key = "VITE_SERVER_URL"; return { key, value: "http://localhost:3000", write: true } as const; } @@ -28,9 +30,11 @@ function getConvexVar(frontend: string[]) { const hasNextJs = frontend.includes("next"); const hasNuxt = frontend.includes("nuxt"); const hasSvelte = frontend.includes("svelte"); + const hasTanstackStart = frontend.includes("tanstack-start"); if (hasNextJs) return "NEXT_PUBLIC_CONVEX_URL"; if (hasNuxt) return "NUXT_PUBLIC_CONVEX_URL"; if (hasSvelte) return "PUBLIC_CONVEX_URL"; + if (hasTanstackStart) return "VITE_CONVEX_URL"; return "VITE_CONVEX_URL"; } @@ -227,8 +231,12 @@ export async function setupEnvironmentVariables(config: ProjectConfig) { const nativeDir = path.join(projectDir, "apps/native"); if (await fs.pathExists(nativeDir)) { let envVarName = "EXPO_PUBLIC_SERVER_URL"; - let serverUrl = - backend === "self" ? "http://localhost:3001" : "http://localhost:3000"; + let serverUrl = "http://localhost:3000"; + + if (backend === "self") { + // Both TanStack Start and Next.js use port 3001 for fullstack + serverUrl = "http://localhost:3001"; + } if (backend === "convex") { envVarName = "EXPO_PUBLIC_CONVEX_URL"; diff --git a/apps/cli/src/helpers/core/post-installation.ts b/apps/cli/src/helpers/core/post-installation.ts index 6b1e055d7..670d3f666 100644 --- a/apps/cli/src/helpers/core/post-installation.ts +++ b/apps/cli/src/helpers/core/post-installation.ts @@ -63,7 +63,7 @@ export async function displayPostInstallInstructions( const nativeInstructions = frontend?.includes("native-nativewind") || frontend?.includes("native-unistyles") - ? getNativeInstructions(isConvex, isBackendSelf) + ? getNativeInstructions(isConvex, isBackendSelf, frontend || []) : ""; const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") @@ -171,12 +171,12 @@ export async function displayPostInstallInstructions( output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`; if (api === "orpc") { - output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:3000/api\n`; + output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:3000/api-reference\n`; } } if (isBackendSelf && api === "orpc") { - output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:${webPort}/rpc/api\n`; + output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:${webPort}/api/rpc/api-reference\n`; } if (addons?.includes("starlight")) { @@ -214,7 +214,11 @@ export async function displayPostInstallInstructions( consola.box(output); } -function getNativeInstructions(isConvex: boolean, isBackendSelf: boolean) { +function getNativeInstructions( + isConvex: boolean, + isBackendSelf: boolean, + _frontend: string[], +) { const envVar = isConvex ? "EXPO_PUBLIC_CONVEX_URL" : "EXPO_PUBLIC_SERVER_URL"; const exampleUrl = isConvex ? "https://" diff --git a/apps/cli/src/helpers/core/template-manager.ts b/apps/cli/src/helpers/core/template-manager.ts index 7271105c9..16fb38ab4 100644 --- a/apps/cli/src/helpers/core/template-manager.ts +++ b/apps/cli/src/helpers/core/template-manager.ts @@ -125,12 +125,12 @@ export async function setupFrontendTemplates( if ( context.backend === "self" && - reactFramework === "next" && + (reactFramework === "next" || reactFramework === "tanstack-start") && context.api !== "none" ) { const apiFullstackDir = path.join( PKG_ROOT, - `templates/api/${context.api}/fullstack/next`, + `templates/api/${context.api}/fullstack/${reactFramework}`, ); if (await fs.pathExists(apiFullstackDir)) { await processAndCopyFiles( @@ -597,10 +597,13 @@ export async function setupAuthTemplate( ); } - if (context.backend === "self" && reactFramework === "next") { + if ( + context.backend === "self" && + (reactFramework === "next" || reactFramework === "tanstack-start") + ) { const authFullstackSrc = path.join( PKG_ROOT, - `templates/auth/${authProvider}/fullstack/next`, + `templates/auth/${authProvider}/fullstack/${reactFramework}`, ); if (await fs.pathExists(authFullstackSrc)) { await processAndCopyFiles( @@ -938,10 +941,13 @@ export async function setupExamplesTemplate( } else { } - if (context.backend === "self" && reactFramework === "next") { + if ( + context.backend === "self" && + (reactFramework === "next" || reactFramework === "tanstack-start") + ) { const exampleFullstackSrc = path.join( exampleBaseDir, - "fullstack/next", + `fullstack/${reactFramework}`, ); if (await fs.pathExists(exampleFullstackSrc)) { await processAndCopyFiles( diff --git a/apps/cli/src/helpers/core/workspace-setup.ts b/apps/cli/src/helpers/core/workspace-setup.ts index f21f4d5fe..34786bc54 100644 --- a/apps/cli/src/helpers/core/workspace-setup.ts +++ b/apps/cli/src/helpers/core/workspace-setup.ts @@ -26,25 +26,33 @@ export async function setupWorkspaceDependencies( const authPackageDir = path.join(projectDir, "packages/auth"); if (await fs.pathExists(authPackageDir)) { + const authDeps: Record = {}; + if (options.database !== "none" && (await fs.pathExists(dbPackageDir))) { + authDeps[`@${projectName}/db`] = workspaceVersion; + } + await addPackageDependency({ dependencies: commonDeps, devDependencies: commonDevDeps, - customDependencies: { - [`@${projectName}/db`]: workspaceVersion, - }, + customDependencies: authDeps, projectDir: authPackageDir, }); } const apiPackageDir = path.join(projectDir, "packages/api"); if (await fs.pathExists(apiPackageDir)) { + const apiDeps: Record = {}; + if (options.auth !== "none" && (await fs.pathExists(authPackageDir))) { + apiDeps[`@${projectName}/auth`] = workspaceVersion; + } + if (options.database !== "none" && (await fs.pathExists(dbPackageDir))) { + apiDeps[`@${projectName}/db`] = workspaceVersion; + } + await addPackageDependency({ dependencies: commonDeps, devDependencies: commonDevDeps, - customDependencies: { - [`@${projectName}/auth`]: workspaceVersion, - [`@${projectName}/db`]: workspaceVersion, - }, + customDependencies: apiDeps, projectDir: apiPackageDir, }); } @@ -52,13 +60,13 @@ export async function setupWorkspaceDependencies( const serverPackageDir = path.join(projectDir, "apps/server"); if (await fs.pathExists(serverPackageDir)) { const serverDeps: Record = {}; - if (await fs.pathExists(apiPackageDir)) { + if (options.api !== "none" && (await fs.pathExists(apiPackageDir))) { serverDeps[`@${projectName}/api`] = workspaceVersion; } - if (await fs.pathExists(authPackageDir)) { + if (options.auth !== "none" && (await fs.pathExists(authPackageDir))) { serverDeps[`@${projectName}/auth`] = workspaceVersion; } - if (await fs.pathExists(dbPackageDir)) { + if (options.database !== "none" && (await fs.pathExists(dbPackageDir))) { serverDeps[`@${projectName}/db`] = workspaceVersion; } @@ -74,10 +82,10 @@ export async function setupWorkspaceDependencies( if (await fs.pathExists(webPackageDir)) { const webDeps: Record = {}; - if (await fs.pathExists(apiPackageDir)) { + if (options.api !== "none" && (await fs.pathExists(apiPackageDir))) { webDeps[`@${projectName}/api`] = workspaceVersion; } - if (await fs.pathExists(authPackageDir)) { + if (options.auth !== "none" && (await fs.pathExists(authPackageDir))) { webDeps[`@${projectName}/auth`] = workspaceVersion; } @@ -93,7 +101,7 @@ export async function setupWorkspaceDependencies( if (await fs.pathExists(nativePackageDir)) { const nativeDeps: Record = {}; - if (await fs.pathExists(apiPackageDir)) { + if (options.api !== "none" && (await fs.pathExists(apiPackageDir))) { nativeDeps[`@${projectName}/api`] = workspaceVersion; } diff --git a/apps/cli/src/prompts/backend.ts b/apps/cli/src/prompts/backend.ts index 639ecc386..8a1e7a0b8 100644 --- a/apps/cli/src/prompts/backend.ts +++ b/apps/cli/src/prompts/backend.ts @@ -3,12 +3,12 @@ import { DEFAULT_CONFIG } from "../constants"; import type { Backend, Frontend } from "../types"; import { exitCancelled } from "../utils/errors"; -// Temporarily restrict to Next.js only for backend="self" +// Temporarily restrict to Next.js and TanStack Start only for backend="self" const FULLSTACK_FRONTENDS: readonly Frontend[] = [ "next", + "tanstack-start", // "nuxt", // TODO: Add support in future update // "svelte", // TODO: Add support in future update - // "tanstack-start", // TODO: Add support in future update ] as const; export async function getBackendFrameworkChoice( diff --git a/apps/cli/src/utils/better-auth-plugin-setup.ts b/apps/cli/src/utils/better-auth-plugin-setup.ts new file mode 100644 index 000000000..f360c95e8 --- /dev/null +++ b/apps/cli/src/utils/better-auth-plugin-setup.ts @@ -0,0 +1,81 @@ +import { SyntaxKind } from "ts-morph"; +import type { ProjectConfig } from "../types"; +import { ensureArrayProperty, tsProject } from "./ts-morph"; + +export async function setupBetterAuthPlugins( + projectDir: string, + config: ProjectConfig, +) { + const authIndexPath = `${projectDir}/packages/auth/src/index.ts`; + const authIndexFile = tsProject.addSourceFileAtPath(authIndexPath); + + if (!authIndexFile) { + console.warn("Better Auth index file not found, skipping plugin setup"); + return; + } + + const pluginsToAdd: string[] = []; + const importsToAdd: string[] = []; + + if ( + config.backend === "self" && + config.frontend?.includes("tanstack-start") + ) { + pluginsToAdd.push("reactStartCookies()"); + importsToAdd.push( + 'import { reactStartCookies } from "better-auth/react-start";', + ); + } + + if ( + config.frontend?.includes("native-nativewind") || + config.frontend?.includes("native-unistyles") + ) { + pluginsToAdd.push("expo()"); + importsToAdd.push('import { expo } from "@better-auth/expo";'); + } + + if (pluginsToAdd.length === 0) { + return; + } + + importsToAdd.forEach((importStatement) => { + const existingImport = authIndexFile.getImportDeclaration((declaration) => + declaration + .getModuleSpecifierValue() + .includes(importStatement.split('"')[1]), + ); + + if (!existingImport) { + authIndexFile.insertImportDeclaration(0, { + moduleSpecifier: importStatement.split('"')[1], + namedImports: [importStatement.split("{")[1].split("}")[0].trim()], + }); + } + }); + + const betterAuthCall = authIndexFile + .getDescendantsOfKind(SyntaxKind.CallExpression) + .find((call) => call.getExpression().getText() === "betterAuth"); + + if (betterAuthCall) { + const configObject = betterAuthCall.getArguments()[0]; + + if ( + configObject && + configObject.getKind() === SyntaxKind.ObjectLiteralExpression + ) { + const objLiteral = configObject.asKindOrThrow( + SyntaxKind.ObjectLiteralExpression, + ); + + const pluginsArray = ensureArrayProperty(objLiteral, "plugins"); + + pluginsToAdd.forEach((plugin) => { + pluginsArray.addElement(plugin); + }); + } + } + + authIndexFile.save(); +} diff --git a/apps/cli/src/utils/compatibility-rules.ts b/apps/cli/src/utils/compatibility-rules.ts index 1417aed79..347d6d132 100644 --- a/apps/cli/src/utils/compatibility-rules.ts +++ b/apps/cli/src/utils/compatibility-rules.ts @@ -43,12 +43,12 @@ export function ensureSingleWebAndNative(frontends: Frontend[]) { } } -// Temporarily restrict to Next.js only for backend="self" +// Temporarily restrict to Next.js and TanStack Start only for backend="self" const FULLSTACK_FRONTENDS: readonly Frontend[] = [ "next", + "tanstack-start", // "nuxt", // TODO: Add support in future update // "svelte", // TODO: Add support in future update - // "tanstack-start", // TODO: Add support in future update ] as const; export function validateSelfBackendCompatibility( @@ -66,7 +66,7 @@ export function validateSelfBackendCompatibility( if (!hasSupportedWeb) { exitWithError( - "Backend 'self' (fullstack) currently only supports Next.js frontend. Please use --frontend next. Support for Nuxt, SvelteKit, and TanStack Start will be added in a future update.", + "Backend 'self' (fullstack) currently only supports Next.js and TanStack Start frontends. Please use --frontend next or --frontend tanstack-start. Support for Nuxt and SvelteKit will be added in a future update.", ); } @@ -86,7 +86,7 @@ export function validateSelfBackendCompatibility( backend === "self" ) { exitWithError( - "Backend 'self' (fullstack) currently only supports Next.js frontend. Please use --frontend next or choose a different backend. Support for Nuxt, SvelteKit, and TanStack Start will be added in a future update.", + "Backend 'self' (fullstack) currently only supports Next.js and TanStack Start frontends. Please use --frontend next or --frontend tanstack-start or choose a different backend. Support for Nuxt and SvelteKit will be added in a future update.", ); } } diff --git a/apps/cli/templates/api/orpc/fullstack/next/src/app/api/rpc/[[...rest]]/route.ts.hbs b/apps/cli/templates/api/orpc/fullstack/next/src/app/api/rpc/[[...rest]]/route.ts.hbs index 9e41d2182..c6006b9b3 100644 --- a/apps/cli/templates/api/orpc/fullstack/next/src/app/api/rpc/[[...rest]]/route.ts.hbs +++ b/apps/cli/templates/api/orpc/fullstack/next/src/app/api/rpc/[[...rest]]/route.ts.hbs @@ -35,7 +35,7 @@ async function handleRequest(req: NextRequest) { if (rpcResult.response) return rpcResult.response; const apiResult = await apiHandler.handle(req, { - prefix: "/api/rpc/api", + prefix: "/api/rpc/api-reference", context: await createContext(req), }); if (apiResult.response) return apiResult.response; diff --git a/apps/cli/templates/api/orpc/fullstack/tanstack-start/src/routes/api/rpc/$.ts.hbs b/apps/cli/templates/api/orpc/fullstack/tanstack-start/src/routes/api/rpc/$.ts.hbs new file mode 100644 index 000000000..6ca146729 --- /dev/null +++ b/apps/cli/templates/api/orpc/fullstack/tanstack-start/src/routes/api/rpc/$.ts.hbs @@ -0,0 +1,58 @@ +import { createContext } from "@{{projectName}}/api/context"; +import { appRouter } from "@{{projectName}}/api/routers/index"; +import { OpenAPIHandler } from "@orpc/openapi/fetch"; +import { OpenAPIReferencePlugin } from "@orpc/openapi/plugins"; +import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4"; +import { RPCHandler } from "@orpc/server/fetch"; +import { onError } from "@orpc/server"; +import { createFileRoute } from "@tanstack/react-router"; + +const rpcHandler = new RPCHandler(appRouter, { + interceptors: [ + onError((error) => { + console.error(error); + }), + ], +}); + +const apiHandler = new OpenAPIHandler(appRouter, { + plugins: [ + new OpenAPIReferencePlugin({ + schemaConverters: [new ZodToJsonSchemaConverter()], + }), + ], + interceptors: [ + onError((error) => { + console.error(error); + }), + ], +}); + +async function handle({ request }: { request: Request }) { + const rpcResult = await rpcHandler.handle(request, { + prefix: "/api/rpc", + context: await createContext({ req: request }), + }); + if (rpcResult.response) return rpcResult.response; + + const apiResult = await apiHandler.handle(request, { + prefix: "/api/rpc/api-reference", + context: await createContext({ req: request }), + }); + if (apiResult.response) return apiResult.response; + + return new Response("Not found", { status: 404 }); +} + +export const Route = createFileRoute('/api/rpc/$')({ + server: { + handlers: { + HEAD: handle, + GET: handle, + POST: handle, + PUT: handle, + PATCH: handle, + DELETE: handle, + }, + }, +}) \ No newline at end of file diff --git a/apps/cli/templates/api/orpc/server/src/context.ts.hbs b/apps/cli/templates/api/orpc/server/src/context.ts.hbs index 8b068201e..3b361daac 100644 --- a/apps/cli/templates/api/orpc/server/src/context.ts.hbs +++ b/apps/cli/templates/api/orpc/server/src/context.ts.hbs @@ -17,6 +17,24 @@ export async function createContext(req: NextRequest) { {{/if}} } +{{else if (and (eq backend 'self') (includes frontend "tanstack-start"))}} +{{#if (eq auth "better-auth")}} +import { auth } from "@{{projectName}}/auth"; +{{/if}} + +export async function createContext({ req }: { req: Request }) { +{{#if (eq auth "better-auth")}} + const session = await auth.api.getSession({ + headers: req.headers, + }); + return { + session, + }; +{{else}} + return {}; +{{/if}} +} + {{else if (eq backend 'hono')}} import type { Context as HonoContext } from "hono"; {{#if (eq auth "better-auth")}} diff --git a/apps/cli/templates/api/orpc/web/react/base/src/utils/orpc.ts.hbs b/apps/cli/templates/api/orpc/web/react/base/src/utils/orpc.ts.hbs index ef9babf7b..9eb6be5c0 100644 --- a/apps/cli/templates/api/orpc/web/react/base/src/utils/orpc.ts.hbs +++ b/apps/cli/templates/api/orpc/web/react/base/src/utils/orpc.ts.hbs @@ -3,7 +3,15 @@ import { RPCLink } from "@orpc/client/fetch"; import { createTanstackQueryUtils } from "@orpc/tanstack-query"; import { QueryCache, QueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; +{{#if (includes frontend "tanstack-start")}} +import { createRouterClient } from "@orpc/server"; +import type { RouterClient } from "@orpc/server"; +import { createIsomorphicFn } from "@tanstack/react-start"; +import { appRouter } from "@{{projectName}}/api/routers/index"; +import { createContext } from "@{{projectName}}/api/context"; +{{else}} import type { AppRouterClient } from "@{{projectName}}/api/routers/index"; +{{/if}} export const queryClient = new QueryClient({ queryCache: new QueryCache({ @@ -20,6 +28,33 @@ export const queryClient = new QueryClient({ }), }); +{{#if (includes frontend "tanstack-start")}} +const getORPCClient = createIsomorphicFn() + .server(() => + createRouterClient(appRouter, { + context: async ({ req }) => { + return createContext({ req }); + }, + }), + ) + .client((): RouterClient => { + const link = new RPCLink({ + url: {{#if (eq backend "self")}}`${window.location.origin}/api/rpc`{{else}}`${import.meta.env.VITE_SERVER_URL}/rpc`{{/if}}, + {{#if (eq auth "better-auth")}} + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + {{/if}} + }); + + return createORPCClient(link); + }); + +export const client: RouterClient = getORPCClient(); +{{else}} export const link = new RPCLink({ {{#if (and (eq backend "self") (includes frontend "next"))}} url: `${typeof window !== "undefined" ? window.location.origin : "http://localhost:3001"}/api/rpc`, @@ -49,5 +84,6 @@ export const link = new RPCLink({ }); export const client: AppRouterClient = createORPCClient(link) +{{/if}} export const orpc = createTanstackQueryUtils(client) diff --git a/apps/cli/templates/api/trpc/fullstack/tanstack-start/src/routes/api/trpc/$.ts.hbs b/apps/cli/templates/api/trpc/fullstack/tanstack-start/src/routes/api/trpc/$.ts.hbs new file mode 100644 index 000000000..e4b0a8f85 --- /dev/null +++ b/apps/cli/templates/api/trpc/fullstack/tanstack-start/src/routes/api/trpc/$.ts.hbs @@ -0,0 +1,22 @@ +import { fetchRequestHandler } from '@trpc/server/adapters/fetch' +import { appRouter } from '@{{projectName}}/api/routers/index' +import { createContext } from '@{{projectName}}/api/context' +import { createFileRoute } from '@tanstack/react-router' + +function handler({ request }: { request: Request }) { + return fetchRequestHandler({ + req: request, + router: appRouter, + createContext, + endpoint: '/api/trpc', + }) +} + +export const Route = createFileRoute('/api/trpc/$')({ + server: { + handlers: { + GET: handler, + POST: handler, + }, + }, +}) diff --git a/apps/cli/templates/api/trpc/server/src/context.ts.hbs b/apps/cli/templates/api/trpc/server/src/context.ts.hbs index 50baaf177..ea91d56a9 100644 --- a/apps/cli/templates/api/trpc/server/src/context.ts.hbs +++ b/apps/cli/templates/api/trpc/server/src/context.ts.hbs @@ -20,6 +20,27 @@ export async function createContext(req: NextRequest) { {{/if}} } +{{else if (and (eq backend 'self') (includes frontend "tanstack-start"))}} +{{#if (eq auth "better-auth")}} +import { auth } from "@{{projectName}}/auth"; +{{/if}} + +export async function createContext({ req }: { req: Request }) { +{{#if (eq auth "better-auth")}} + const session = await auth.api.getSession({ + headers: req.headers, + }); + return { + session, + }; +{{else}} + // No auth configured + return { + session: null, + }; +{{/if}} +} + {{else if (eq backend 'hono')}} import type { Context as HonoContext } from "hono"; {{#if (eq auth "better-auth")}} diff --git a/apps/cli/templates/auth/better-auth/fullstack/tanstack-start/src/routes/api/auth/$.ts.hbs b/apps/cli/templates/auth/better-auth/fullstack/tanstack-start/src/routes/api/auth/$.ts.hbs new file mode 100644 index 000000000..11a712ae2 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/fullstack/tanstack-start/src/routes/api/auth/$.ts.hbs @@ -0,0 +1,15 @@ +import { auth } from '@{{projectName}}/auth' +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/api/auth/$')({ + server: { + handlers: { + GET: ({ request }) => { + return auth.handler(request) + }, + POST: ({ request }) => { + return auth.handler(request) + }, + }, + }, +}) \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/server/base/src/index.ts.hbs b/apps/cli/templates/auth/better-auth/server/base/src/index.ts.hbs index 43eb98cbb..236c935c5 100644 --- a/apps/cli/templates/auth/better-auth/server/base/src/index.ts.hbs +++ b/apps/cli/templates/auth/better-auth/server/base/src/index.ts.hbs @@ -1,9 +1,6 @@ {{#if (eq orm "prisma")}} import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; -{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} -import { expo } from "@better-auth/expo"; -{{/if}} {{#if (eq payments "polar")}} import { polar, checkout, portal } from "@polar-sh/better-auth"; import { polarClient } from "./lib/payments"; @@ -26,6 +23,7 @@ export const auth = betterAuth({ emailAndPassword: { enabled: true, }, + {{#if (ne backend "self")}} advanced: { defaultCookieAttributes: { sameSite: "none", @@ -33,6 +31,7 @@ export const auth = betterAuth({ httpOnly: true, }, }, + {{/if}} {{#if (eq payments "polar")}} plugins: [ polar({ @@ -53,14 +52,7 @@ export const auth = betterAuth({ portal(), ], }), - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - expo(), - {{/if}} ], - {{else}} - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - plugins: [expo()], - {{/if}} {{/if}} }); {{/if}} @@ -69,9 +61,6 @@ export const auth = betterAuth({ {{#if (or (eq runtime "bun") (eq runtime "node") (eq runtime "none"))}} import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; -{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} -import { expo } from "@better-auth/expo"; -{{/if}} {{#if (eq payments "polar")}} import { polar, checkout, portal } from "@polar-sh/better-auth"; import { polarClient } from "./lib/payments"; @@ -95,6 +84,7 @@ export const auth = betterAuth({ emailAndPassword: { enabled: true, }, + {{#if (ne backend "self")}} advanced: { defaultCookieAttributes: { sameSite: "none", @@ -102,6 +92,7 @@ export const auth = betterAuth({ httpOnly: true, }, }, + {{/if}} {{#if (eq payments "polar")}} plugins: [ polar({ @@ -122,14 +113,7 @@ export const auth = betterAuth({ portal(), ], }), - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - expo(), - {{/if}} ], - {{else}} - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - plugins: [expo()], - {{/if}} {{/if}} }); {{/if}} @@ -137,9 +121,6 @@ export const auth = betterAuth({ {{#if (eq runtime "workers")}} import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; -{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} -import { expo } from "@better-auth/expo"; -{{/if}} {{#if (eq payments "polar")}} import { polar, checkout, portal } from "@polar-sh/better-auth"; import { polarClient } from "./lib/payments"; @@ -207,10 +188,6 @@ export const auth = betterAuth({ ], }), ], - {{else}} - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - plugins: [expo()], - {{/if}} {{/if}} }); {{/if}} @@ -219,9 +196,6 @@ export const auth = betterAuth({ {{#if (eq orm "mongoose")}} import { betterAuth } from "better-auth"; import { mongodbAdapter } from "better-auth/adapters/mongodb"; -{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} -import { expo } from "@better-auth/expo"; -{{/if}} {{#if (eq payments "polar")}} import { polar, checkout, portal } from "@polar-sh/better-auth"; import { polarClient } from "./lib/payments"; @@ -239,6 +213,7 @@ export const auth = betterAuth({ emailAndPassword: { enabled: true, }, + {{#if (ne backend "self")}} advanced: { defaultCookieAttributes: { sameSite: "none", @@ -246,6 +221,7 @@ export const auth = betterAuth({ httpOnly: true, }, }, + {{/if}} {{#if (eq payments "polar")}} plugins: [ polar({ @@ -266,23 +242,13 @@ export const auth = betterAuth({ portal(), ], }), - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - expo(), - {{/if}} ], - {{else}} - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - plugins: [expo()], - {{/if}} {{/if}} }); {{/if}} {{#if (eq orm "none")}} import { betterAuth } from "better-auth"; -{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} -import { expo } from "@better-auth/expo"; -{{/if}} {{#if (eq payments "polar")}} import { polar, checkout, portal } from "@polar-sh/better-auth"; import { polarClient } from "./lib/payments"; @@ -299,6 +265,7 @@ export const auth = betterAuth({ emailAndPassword: { enabled: true, }, + {{#if (ne backend "self")}} advanced: { defaultCookieAttributes: { sameSite: "none", @@ -306,6 +273,7 @@ export const auth = betterAuth({ httpOnly: true, }, }, + {{/if}} {{#if (eq payments "polar")}} plugins: [ polar({ @@ -326,14 +294,7 @@ export const auth = betterAuth({ portal(), ], }), - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - expo(), - {{/if}} ], - {{else}} - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} - plugins: [expo()], - {{/if}} {{/if}} }); -{{/if}} +{{/if}} \ No newline at end of file diff --git a/apps/cli/templates/backend/server/elysia/src/index.ts.hbs b/apps/cli/templates/backend/server/elysia/src/index.ts.hbs index 6445b0dd3..2196f673b 100644 --- a/apps/cli/templates/backend/server/elysia/src/index.ts.hbs +++ b/apps/cli/templates/backend/server/elysia/src/index.ts.hbs @@ -82,7 +82,7 @@ const app = new Elysia() }) .all('/api*', async (context) => { const { response } = await apiHandler.handle(context.request, { - prefix: '/api', + prefix: '/api-reference', context: await createContext({ context }) }) return response ?? new Response('Not Found', { status: 404 }) diff --git a/apps/cli/templates/backend/server/express/src/index.ts.hbs b/apps/cli/templates/backend/server/express/src/index.ts.hbs index 366beb574..83e8ea540 100644 --- a/apps/cli/templates/backend/server/express/src/index.ts.hbs +++ b/apps/cli/templates/backend/server/express/src/index.ts.hbs @@ -86,7 +86,7 @@ app.use(async (req, res, next) => { if (rpcResult.matched) return; const apiResult = await apiHandler.handle(req, res, { - prefix: "/api", + prefix: "/api-reference", {{#if (eq auth "better-auth")}} context: await createContext({ req }), {{else}} diff --git a/apps/cli/templates/backend/server/fastify/src/index.ts.hbs b/apps/cli/templates/backend/server/fastify/src/index.ts.hbs index 58627794b..460a4f242 100644 --- a/apps/cli/templates/backend/server/fastify/src/index.ts.hbs +++ b/apps/cli/templates/backend/server/fastify/src/index.ts.hbs @@ -87,7 +87,7 @@ const fastify = Fastify({ const apiResult = await apiHandler.handle(req, res, { context: await createContext(req.headers), - prefix: "/api", + prefix: "/api-reference", }); if (apiResult.matched) { diff --git a/apps/cli/templates/backend/server/hono/src/index.ts.hbs b/apps/cli/templates/backend/server/hono/src/index.ts.hbs index 32e482e01..b22af31db 100644 --- a/apps/cli/templates/backend/server/hono/src/index.ts.hbs +++ b/apps/cli/templates/backend/server/hono/src/index.ts.hbs @@ -92,7 +92,7 @@ app.use("/*", async (c, next) => { } const apiResult = await apiHandler.handle(c.req.raw, { - prefix: "/api", + prefix: "/api-reference", context: context, }); diff --git a/apps/cli/templates/examples/ai/fullstack/tanstack-start/src/routes/api/ai/$.ts.hbs b/apps/cli/templates/examples/ai/fullstack/tanstack-start/src/routes/api/ai/$.ts.hbs new file mode 100644 index 000000000..57f32cef0 --- /dev/null +++ b/apps/cli/templates/examples/ai/fullstack/tanstack-start/src/routes/api/ai/$.ts.hbs @@ -0,0 +1,31 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { google } from "@ai-sdk/google"; +import { streamText, type UIMessage, convertToModelMessages } from "ai"; + +export const Route = createFileRoute("/api/ai/$")({ + server: { + handlers: { + POST: async ({ request }) => { + try { + const { messages }: { messages: UIMessage[] } = await request.json(); + + const result = streamText({ + model: google("gemini-2.5-flash"), + messages: convertToModelMessages(messages), + }); + + return result.toUIMessageStreamResponse(); + } catch (error) { + console.error("AI API error:", error); + return new Response( + JSON.stringify({ error: "Failed to process AI request" }), + { + status: 500, + headers: { "Content-Type": "application/json" }, + }, + ); + } + }, + }, + }, +}); diff --git a/apps/cli/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs b/apps/cli/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs index e76dfc646..ad41eaaf5 100644 --- a/apps/cli/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs +++ b/apps/cli/templates/examples/ai/web/react/tanstack-start/src/routes/ai.tsx.hbs @@ -15,7 +15,7 @@ function RouteComponent() { const [input, setInput] = useState(""); const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ - api: `${import.meta.env.VITE_SERVER_URL}/ai`, + api: {{#if (eq backend "self")}}"/api/ai"{{else}}`${import.meta.env.VITE_SERVER_URL}/ai`{{/if}}, }), }); diff --git a/apps/cli/templates/frontend/react/tanstack-start/src/router.tsx.hbs b/apps/cli/templates/frontend/react/tanstack-start/src/router.tsx.hbs index a0c6955ac..9d70bc02f 100644 --- a/apps/cli/templates/frontend/react/tanstack-start/src/router.tsx.hbs +++ b/apps/cli/templates/frontend/react/tanstack-start/src/router.tsx.hbs @@ -85,7 +85,7 @@ export const queryClient = new QueryClient({ const trpcClient = createTRPCClient({ links: [ httpBatchLink({ - url: `${import.meta.env.VITE_SERVER_URL}/trpc`, + url: {{#if (eq backend "self")}}"/api/trpc"{{else}}`${import.meta.env.VITE_SERVER_URL}/trpc`{{/if}}, {{#if (eq auth "better-auth")}} fetch(url, options) { return fetch(url, { diff --git a/apps/web/src/app/(home)/new/_components/utils.ts b/apps/web/src/app/(home)/new/_components/utils.ts index 49c188d9f..8d43ac567 100644 --- a/apps/web/src/app/(home)/new/_components/utils.ts +++ b/apps/web/src/app/(home)/new/_components/utils.ts @@ -81,7 +81,12 @@ export const analyzeStackCompatibility = ( const isConvex = nextStack.backend === "convex"; const isBackendNone = nextStack.backend === "none"; - const isBackendSelf = nextStack.backend === "self"; + const isBackendSelf = + nextStack.backend === "self-next" || + nextStack.backend === "self-tanstack-start"; + const isBackendSelfNext = nextStack.backend === "self-next"; + const isBackendSelfTanstackStart = + nextStack.backend === "self-tanstack-start"; if (isConvex) { const convexOverrides: Partial = { @@ -195,9 +200,11 @@ export const analyzeStackCompatibility = ( } } } else if (isBackendSelf) { - const hasNextFrontend = nextStack.webFrontend.includes("next"); + const hasSupportedFrontend = + nextStack.webFrontend.includes("next") || + nextStack.webFrontend.includes("tanstack-start"); - if (!hasNextFrontend) { + if (!hasSupportedFrontend) { const originalWebFrontendLength = nextStack.webFrontend.length; nextStack.webFrontend = ["next"]; @@ -207,17 +214,17 @@ export const analyzeStackCompatibility = ( ) { changed = true; notes.webFrontend.notes.push( - "Self backend (fullstack) currently only supports Next.js frontend. Other frontends have been removed.", + "Self backend (fullstack) currently only supports Next.js and TanStack Start frontends. Other frontends have been removed.", ); notes.backend.notes.push( - "Self backend (fullstack) requires Next.js frontend.", + "Self backend (fullstack) requires Next.js or TanStack Start frontend.", ); notes.webFrontend.hasIssue = true; notes.backend.hasIssue = true; changes.push({ category: "backend", message: - "Frontend set to 'Next.js' (Self backend currently only supports Next.js)", + "Frontend set to 'Next.js' (Self backend currently only supports Next.js and TanStack Start)", }); } } @@ -239,6 +246,62 @@ export const analyzeStackCompatibility = ( "Runtime set to 'None' (Self backend uses frontend's built-in API routes)", }); } + if (isBackendSelfNext) { + const hasNextFrontend = nextStack.webFrontend.includes("next"); + if (!hasNextFrontend) { + const originalWebFrontendLength = nextStack.webFrontend.length; + nextStack.webFrontend = ["next"]; + + if ( + originalWebFrontendLength !== 1 || + !nextStack.webFrontend.includes("next") + ) { + changed = true; + notes.webFrontend.notes.push( + "Next.js fullstack backend requires Next.js frontend. Frontend has been set to Next.js.", + ); + notes.backend.notes.push( + "Next.js fullstack backend requires Next.js frontend.", + ); + notes.webFrontend.hasIssue = true; + notes.backend.hasIssue = true; + changes.push({ + category: "backend", + message: + "Frontend set to 'Next.js' (Next.js fullstack backend requires Next.js frontend)", + }); + } + } + } + + if (isBackendSelfTanstackStart) { + const hasTanstackStartFrontend = + nextStack.webFrontend.includes("tanstack-start"); + if (!hasTanstackStartFrontend) { + const originalWebFrontendLength = nextStack.webFrontend.length; + nextStack.webFrontend = ["tanstack-start"]; + + if ( + originalWebFrontendLength !== 1 || + !nextStack.webFrontend.includes("tanstack-start") + ) { + changed = true; + notes.webFrontend.notes.push( + "TanStack Start fullstack backend requires TanStack Start frontend. Frontend has been set to TanStack Start.", + ); + notes.backend.notes.push( + "TanStack Start fullstack backend requires TanStack Start frontend.", + ); + notes.webFrontend.hasIssue = true; + notes.backend.hasIssue = true; + changes.push({ + category: "backend", + message: + "Frontend set to 'TanStack Start' (TanStack Start fullstack backend requires TanStack Start frontend)", + }); + } + } + } } else { if (nextStack.runtime === "none") { notes.runtime.notes.push( @@ -1116,10 +1179,12 @@ export const analyzeStackCompatibility = ( nextStack.serverDeploy !== "none" && (nextStack.backend === "none" || nextStack.backend === "convex" || - nextStack.backend === "self") + nextStack.backend === "self-next" || + nextStack.backend === "self-tanstack-start") ) { const backendType = - nextStack.backend === "self" + nextStack.backend === "self-next" || + nextStack.backend === "self-tanstack-start" ? "Self backend (fullstack)" : nextStack.backend === "convex" ? "Convex backend" @@ -1355,19 +1420,35 @@ export const getDisabledReason = ( return "Cloudflare Workers runtime only supports Hono backend. Switch to Hono to use Workers runtime."; } - if (category === "backend" && optionId === "self") { + if (category === "backend" && optionId === "self-next") { const hasNextFrontend = finalStack.webFrontend.includes("next"); if (!hasNextFrontend) { - return "Self backend (fullstack) currently only supports Next.js frontend. Select Next.js frontend first."; + return "Next.js fullstack backend requires Next.js frontend. Select Next.js frontend first."; + } + } + + if (category === "backend" && optionId === "self-tanstack-start") { + const hasTanstackStartFrontend = + finalStack.webFrontend.includes("tanstack-start"); + if (!hasTanstackStartFrontend) { + return "TanStack Start fullstack backend requires TanStack Start frontend. Select TanStack Start frontend first."; } } if ( category === "webFrontend" && optionId !== "next" && - finalStack.backend === "self" + finalStack.backend === "self-next" + ) { + return "Next.js fullstack backend only supports Next.js frontend. Select Next.js frontend first."; + } + + if ( + category === "webFrontend" && + optionId !== "tanstack-start" && + finalStack.backend === "self-tanstack-start" ) { - return "Self backend (fullstack) currently only supports Next.js frontend. Support for other frameworks will be added in a future update."; + return "TanStack Start fullstack backend only supports TanStack Start frontend. Select TanStack Start frontend first."; } if ( @@ -1382,24 +1463,27 @@ export const getDisabledReason = ( category === "runtime" && optionId === "none" && finalStack.backend !== "convex" && - finalStack.backend !== "self" + finalStack.backend !== "self-next" && + finalStack.backend !== "self-tanstack-start" ) { - return "Runtime 'None' is only available with Convex backend or Self backend (fullstack). Switch to Convex or Self to use this option."; + return "Runtime 'None' is only available with Convex backend, Next.js fullstack, or TanStack Start fullstack. Switch to one of these backends to use this option."; } if ( category === "runtime" && optionId !== "none" && - finalStack.backend === "self" + (finalStack.backend === "self-next" || + finalStack.backend === "self-tanstack-start") ) { - return "Self backend (fullstack) uses frontend's built-in API routes and requires runtime to be 'None'. Self backend doesn't need a separate runtime."; + return "Fullstack backends use frontend's built-in API routes and require runtime to be 'None'. Fullstack backends don't need a separate runtime."; } if ( category === "orm" && finalStack.database === "none" && optionId !== "none" && - finalStack.backend !== "self" + finalStack.backend !== "self-next" && + finalStack.backend !== "self-tanstack-start" ) { return "ORM requires a database. Select a database first (SQLite, PostgreSQL, or MongoDB)."; } @@ -1408,13 +1492,18 @@ export const getDisabledReason = ( category === "database" && optionId !== "none" && finalStack.orm === "none" && - finalStack.backend !== "self" + finalStack.backend !== "self-next" && + finalStack.backend !== "self-tanstack-start" ) { return "Database requires an ORM. Select an ORM first (Drizzle, Prisma, or Mongoose)."; } if (category === "database" && optionId === "mongodb") { - if (finalStack.orm === "none" && finalStack.backend !== "self") { + if ( + finalStack.orm === "none" && + finalStack.backend !== "self-next" && + finalStack.backend !== "self-tanstack-start" + ) { return "MongoDB requires an ORM. Select Prisma or Mongoose ORM first."; } if (finalStack.orm !== "prisma" && finalStack.orm !== "mongoose") { @@ -1429,7 +1518,11 @@ export const getDisabledReason = ( } if (category === "database" && optionId === "sqlite") { - if (finalStack.orm === "none" && finalStack.backend !== "self") { + if ( + finalStack.orm === "none" && + finalStack.backend !== "self-next" && + finalStack.backend !== "self-tanstack-start" + ) { return "SQLite requires an ORM. Select Drizzle or Prisma ORM first."; } if (finalStack.dbSetup === "mongodb-atlas") { @@ -1483,13 +1576,21 @@ export const getDisabledReason = ( if (finalStack.database === "mongodb") { return "Drizzle ORM does not support MongoDB. Use Prisma or Mongoose ORM instead."; } - if (finalStack.database === "none" && finalStack.backend !== "self") { + if ( + finalStack.database === "none" && + finalStack.backend !== "self-next" && + finalStack.backend !== "self-tanstack-start" + ) { return "Drizzle ORM requires a database. Select a database first (SQLite, PostgreSQL, or MySQL)."; } } if (category === "orm" && optionId === "prisma") { - if (finalStack.database === "none" && finalStack.backend !== "self") { + if ( + finalStack.database === "none" && + finalStack.backend !== "self-next" && + finalStack.backend !== "self-tanstack-start" + ) { return "Prisma ORM requires a database. Select a database first (SQLite, PostgreSQL, MySQL, or MongoDB)."; } if (finalStack.dbSetup === "turso" && finalStack.database !== "sqlite") { @@ -1649,9 +1750,13 @@ export const getDisabledReason = ( optionId !== "none" && (finalStack.backend === "none" || finalStack.backend === "convex" || - finalStack.backend === "self") + finalStack.backend === "self-next" || + finalStack.backend === "self-tanstack-start") ) { - if (finalStack.backend === "self") { + if ( + finalStack.backend === "self-next" || + finalStack.backend === "self-tanstack-start" + ) { return "Self backend (fullstack) uses frontend's built-in API routes and doesn't need server deployment."; } return "Server deployment requires a supported backend (Hono, Express, Fastify, or Elysia). Convex has its own deployment."; diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 267bd6986..678e34a8f 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -202,12 +202,19 @@ export const TECH_OPTIONS: Record< color: "from-pink-500 to-pink-700", }, { - id: "self", + id: "self-next", name: "Fullstack Next.js", - description: "Use frontend's built-in API routes", + description: "Use Next.js built-in API routes", icon: `${ICON_BASE_URL}/nextjs.svg`, color: "from-gray-700 to-black", }, + { + id: "self-tanstack-start", + name: "Fullstack TanStack Start", + description: "Use TanStack Start's built-in API routes", + icon: `${ICON_BASE_URL}/tanstack.svg`, + color: "from-purple-400 to-purple-600", + }, { id: "none", name: "No Backend", @@ -669,7 +676,34 @@ export const PRESET_TEMPLATES = [ projectName: "my-better-t-app", webFrontend: ["next"], nativeFrontend: ["none"], - backend: "self", + backend: "self-next", + runtime: "none", + database: "sqlite", + orm: "drizzle", + dbSetup: "none", + auth: "better-auth", + payments: "none", + packageManager: "bun", + addons: ["turborepo"], + examples: ["todo"], + git: "true", + install: "true", + api: "trpc", + webDeploy: "none", + serverDeploy: "none", + yolo: "false", + }, + }, + { + id: "tanstack-start-fullstack", + name: "TanStack Start Fullstack", + description: + "Full-stack TanStack Start app with built-in API routes and internal packages", + stack: { + projectName: "my-better-t-app", + webFrontend: ["tanstack-start"], + nativeFrontend: ["none"], + backend: "self-tanstack-start", runtime: "none", database: "sqlite", orm: "drizzle", diff --git a/apps/web/src/lib/stack-utils.ts b/apps/web/src/lib/stack-utils.ts index a09a985bb..25ee7cccd 100644 --- a/apps/web/src/lib/stack-utils.ts +++ b/apps/web/src/lib/stack-utils.ts @@ -79,13 +79,21 @@ export function generateStackCommand(stack: StackState) { return `${base} ${projectName} --yes`; } + // Map web interface backend IDs to CLI backend flags + const mapBackendToCli = (backend: string) => { + if (backend === "self-next" || backend === "self-tanstack-start") { + return "self"; + } + return backend; + }; + const flags = [ `--frontend ${ [...stack.webFrontend, ...stack.nativeFrontend] .filter((v, _, arr) => v !== "none" || arr.length === 1) .join(" ") || "none" }`, - `--backend ${stack.backend}`, + `--backend ${mapBackendToCli(stack.backend)}`, `--runtime ${stack.runtime}`, `--api ${stack.api}`, `--auth ${stack.auth}`,