From 914c6cb65948cf551701d7ad614bafb32df6d2c9 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 17 Sep 2025 16:32:07 +0530 Subject: [PATCH] feat(cli): add convex + better-auth --- apps/cli/src/constants.ts | 3 +- apps/cli/src/helpers/core/auth-setup.ts | 41 +++++ apps/cli/src/helpers/core/convex-codegen.ts | 1 + apps/cli/src/helpers/core/create-project.ts | 5 - apps/cli/src/helpers/core/env-setup.ts | 59 ++++++- apps/cli/src/helpers/core/template-manager.ts | 53 ++++++ apps/cli/src/prompts/auth.ts | 46 +++-- apps/cli/src/utils/add-package-deps.ts | 16 +- apps/cli/src/utils/compatibility-rules.ts | 8 +- apps/cli/src/utils/config-validation.ts | 11 +- .../cli/templates/addons/biome/biome.json.hbs | 3 +- .../templates/addons/ultracite/biome.json.hbs | 1 + .../convex/backend/convex/auth.config.ts.hbs | 8 + .../convex/backend/convex/auth.ts.hbs | 48 ++++++ .../backend/convex/convex.config.ts.hbs | 7 + .../convex/backend/convex/http.ts.hbs | 12 ++ .../convex/backend/convex/privateData.ts.hbs | 16 ++ .../src/app/api/auth/[...all]/route.ts.hbs | 3 + .../react/next/src/app/dashboard/page.tsx.hbs | 40 +++++ .../next/src/components/sign-in-form.tsx.hbs | 129 ++++++++++++++ .../next/src/components/sign-up-form.tsx.hbs | 154 +++++++++++++++++ .../next/src/components/user-menu.tsx.hbs | 48 ++++++ .../web/react/next/src/lib/auth-client.ts.hbs | 6 + .../web/react/next/src/lib/auth-server.ts.hbs | 6 + .../src/components/sign-in-form.tsx.hbs | 133 +++++++++++++++ .../src/components/sign-up-form.tsx.hbs | 158 ++++++++++++++++++ .../src/components/user-menu.tsx.hbs | 50 ++++++ .../src/lib/auth-client.ts.hbs | 10 ++ .../src/routes/dashboard.tsx.hbs | 43 +++++ .../src/components/sign-in-form.tsx.hbs | 133 +++++++++++++++ .../src/components/sign-up-form.tsx.hbs | 158 ++++++++++++++++++ .../src/components/user-menu.tsx.hbs | 50 ++++++ .../tanstack-start/src/lib/auth-client.ts.hbs | 6 + .../tanstack-start/src/lib/auth-server.ts.hbs | 5 + .../src/routes/api/auth/$.ts.hbs | 11 ++ .../src/routes/dashboard.tsx.hbs | 43 +++++ .../src/components/theme-provider.tsx.hbs | 11 -- .../next/src/components/providers.tsx.hbs | 8 + .../react/tanstack-router/src/main.tsx.hbs | 9 +- .../tanstack-start/src/routes/__root.tsx.hbs | 47 ++++++ .../tanstack-start/src/routes/index.tsx.hbs | 4 +- .../web-base/src/components/header.tsx.hbs | 4 +- .../components/{loader.tsx => loader.tsx.hbs} | 0 .../src/app/(home)/new/_components/utils.ts | 61 +++++-- 44 files changed, 1602 insertions(+), 66 deletions(-) create mode 100644 apps/cli/templates/auth/better-auth/convex/backend/convex/auth.config.ts.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/backend/convex/convex.config.ts.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/backend/convex/http.ts.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/backend/convex/privateData.ts.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/next/src/app/api/auth/[...all]/route.ts.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/next/src/app/dashboard/page.tsx.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/next/src/components/sign-in-form.tsx.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/next/src/components/sign-up-form.tsx.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/next/src/components/user-menu.tsx.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/next/src/lib/auth-client.ts.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/next/src/lib/auth-server.ts.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/sign-in-form.tsx.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/sign-up-form.tsx.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/user-menu.tsx.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/lib/auth-client.ts.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/routes/dashboard.tsx.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/sign-in-form.tsx.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/sign-up-form.tsx.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/user-menu.tsx.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-client.ts.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-server.ts.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/api/auth/$.ts.hbs create mode 100644 apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/dashboard.tsx.hbs delete mode 100644 apps/cli/templates/auth/better-auth/web/react/next/src/components/theme-provider.tsx.hbs rename apps/cli/templates/frontend/react/web-base/src/components/{loader.tsx => loader.tsx.hbs} (100%) diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index efc14cdf1..091a456a8 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -126,11 +126,12 @@ export const dependencyVersionMap = { "@trpc/server": "^11.5.0", "@trpc/client": "^11.5.0", - convex: "^1.25.4", + convex: "^1.27.0", "@convex-dev/react-query": "^0.0.0-alpha.8", "convex-svelte": "^0.0.11", "convex-nuxt": "0.1.5", "convex-vue": "^0.1.5", + "@convex-dev/better-auth": "^0.8.4", "@tanstack/svelte-query": "^5.85.3", "@tanstack/svelte-query-devtools": "^5.85.3", diff --git a/apps/cli/src/helpers/core/auth-setup.ts b/apps/cli/src/helpers/core/auth-setup.ts index 3b6603968..f6246ce20 100644 --- a/apps/cli/src/helpers/core/auth-setup.ts +++ b/apps/cli/src/helpers/core/auth-setup.ts @@ -46,6 +46,47 @@ export async function setupAuth(config: ProjectConfig) { } } + if (auth === "better-auth") { + const convexBackendDir = path.join(projectDir, "packages/backend"); + const convexBackendDirExists = await fs.pathExists(convexBackendDir); + + if (convexBackendDirExists) { + await addPackageDependency({ + dependencies: ["better-auth", "@convex-dev/better-auth"], + customDependencies: { "better-auth": "1.3.8" }, + projectDir: convexBackendDir, + }); + } + + if (clientDirExists) { + const hasNextJs = frontend.includes("next"); + const hasTanStackStart = frontend.includes("tanstack-start"); + const hasViteReactOther = frontend.some((f) => + ["tanstack-router", "react-router"].includes(f), + ); + + if (hasNextJs) { + await addPackageDependency({ + dependencies: ["better-auth", "@convex-dev/better-auth"], + customDependencies: { "better-auth": "1.3.8" }, + projectDir: clientDir, + }); + } else if (hasTanStackStart) { + await addPackageDependency({ + dependencies: ["better-auth", "@convex-dev/better-auth"], + customDependencies: { "better-auth": "1.3.8" }, + projectDir: clientDir, + }); + } else if (hasViteReactOther) { + await addPackageDependency({ + dependencies: ["better-auth", "@convex-dev/better-auth"], + customDependencies: { "better-auth": "1.3.8" }, + projectDir: clientDir, + }); + } + } + } + const hasNativeWind = frontend.includes("native-nativewind"); const hasUnistyles = frontend.includes("native-unistyles"); if ( diff --git a/apps/cli/src/helpers/core/convex-codegen.ts b/apps/cli/src/helpers/core/convex-codegen.ts index 2b9159ef9..c771e2a57 100644 --- a/apps/cli/src/helpers/core/convex-codegen.ts +++ b/apps/cli/src/helpers/core/convex-codegen.ts @@ -3,6 +3,7 @@ import { execa } from "execa"; import type { PackageManager } from "../../types"; import { getPackageExecutionCommand } from "../../utils/package-runner"; +// having problems running this in convex + better-auth export async function runConvexCodegen( projectDir: string, packageManager: PackageManager | null | undefined, diff --git a/apps/cli/src/helpers/core/create-project.ts b/apps/cli/src/helpers/core/create-project.ts index 8de6ee17b..c7721830b 100644 --- a/apps/cli/src/helpers/core/create-project.ts +++ b/apps/cli/src/helpers/core/create-project.ts @@ -12,7 +12,6 @@ import { setupRuntime } from "../core/runtime-setup"; import { setupServerDeploy } from "../deployment/server-deploy-setup"; import { setupWebDeploy } from "../deployment/web-deploy-setup"; import { setupAuth } from "./auth-setup"; -import { runConvexCodegen } from "./convex-codegen"; import { createReadme } from "./create-readme"; import { setupEnvironmentVariables } from "./env-setup"; import { initializeGit } from "./git"; @@ -97,10 +96,6 @@ export async function createProject( await writeBtsConfig(options); - if (isConvex) { - await runConvexCodegen(projectDir, options.packageManager); - } - log.success("Project template successfully scaffolded!"); if (options.install) { diff --git a/apps/cli/src/helpers/core/env-setup.ts b/apps/cli/src/helpers/core/env-setup.ts index 162741f58..7336931b2 100644 --- a/apps/cli/src/helpers/core/env-setup.ts +++ b/apps/cli/src/helpers/core/env-setup.ts @@ -7,6 +7,7 @@ export interface EnvVariable { key: string; value: string | null | undefined; condition: boolean; + comment?: string; } export async function addEnvVariablesToFile( @@ -24,7 +25,7 @@ export async function addEnvVariablesToFile( let contentToAdd = ""; const exampleVariables: string[] = []; - for (const { key, value, condition } of variables) { + for (const { key, value, condition, comment } of variables) { if (condition) { const regex = new RegExp(`^${key}=.*$`, "m"); const valueToWrite = value ?? ""; @@ -37,6 +38,9 @@ export async function addEnvVariablesToFile( modified = true; } } else { + if (comment) { + contentToAdd += `# ${comment}\n`; + } contentToAdd += `${key}=${valueToWrite}\n`; modified = true; } @@ -179,6 +183,22 @@ export async function setupEnvironmentVariables(config: ProjectConfig) { } } + if (backend === "convex" && auth === "better-auth") { + if (hasNextJs) { + clientVars.push({ + key: "NEXT_PUBLIC_CONVEX_SITE_URL", + value: "https://", + condition: true, + }); + } else if (hasReactRouter || hasTanStackRouter || hasTanStackStart) { + clientVars.push({ + key: "VITE_CONVEX_SITE_URL", + value: "https://", + condition: true, + }); + } + } + await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars); } } @@ -217,6 +237,43 @@ export async function setupEnvironmentVariables(config: ProjectConfig) { } if (backend === "convex") { + if (auth === "better-auth") { + const convexBackendDir = path.join(projectDir, "packages/backend"); + if (await fs.pathExists(convexBackendDir)) { + const envLocalPath = path.join(convexBackendDir, ".env.local"); + + if ( + !(await fs.pathExists(envLocalPath)) || + !(await fs.readFile(envLocalPath, "utf8")).includes( + "npx convex env set", + ) + ) { + const convexCommands = `# Set Convex environment variables +npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32) +npx convex env set SITE_URL http://localhost:3001 + +`; + await fs.appendFile(envLocalPath, convexCommands); + } + + const convexBackendVars: EnvVariable[] = [ + { + key: hasNextJs + ? "NEXT_PUBLIC_CONVEX_SITE_URL" + : "VITE_CONVEX_SITE_URL", + value: "", + condition: true, + comment: "Same as CONVEX_URL but ends in .site", + }, + { + key: "SITE_URL", + value: "http://localhost:3001", + condition: true, + }, + ]; + await addEnvVariablesToFile(envLocalPath, convexBackendVars); + } + } return; } diff --git a/apps/cli/src/helpers/core/template-manager.ts b/apps/cli/src/helpers/core/template-manager.ts index 5ffa9ec94..f8c3e2c33 100644 --- a/apps/cli/src/helpers/core/template-manager.ts +++ b/apps/cli/src/helpers/core/template-manager.ts @@ -455,6 +455,59 @@ export async function setupAuthTemplate( return; } + if (context.backend === "convex" && authProvider === "better-auth") { + const convexBackendDestDir = path.join(projectDir, "packages/backend"); + const convexBetterAuthBackendSrc = path.join( + PKG_ROOT, + "templates/auth/better-auth/convex/backend", + ); + if (await fs.pathExists(convexBetterAuthBackendSrc)) { + await fs.ensureDir(convexBackendDestDir); + await processAndCopyFiles( + "**/*", + convexBetterAuthBackendSrc, + convexBackendDestDir, + context, + ); + } + + if (webAppDirExists && hasReactWeb) { + const convexBetterAuthWebBaseSrc = path.join( + PKG_ROOT, + "templates/auth/better-auth/convex/web/react/base", + ); + if (await fs.pathExists(convexBetterAuthWebBaseSrc)) { + await processAndCopyFiles( + "**/*", + convexBetterAuthWebBaseSrc, + webAppDir, + context, + ); + } + + const reactFramework = context.frontend.find((f) => + ["tanstack-router", "react-router", "tanstack-start", "next"].includes( + f, + ), + ); + if (reactFramework) { + const convexBetterAuthWebSrc = path.join( + PKG_ROOT, + `templates/auth/better-auth/convex/web/react/${reactFramework}`, + ); + if (await fs.pathExists(convexBetterAuthWebSrc)) { + await processAndCopyFiles( + "**/*", + convexBetterAuthWebSrc, + webAppDir, + context, + ); + } + } + } + return; + } + if (serverAppDirExists && context.backend !== "convex") { const authServerBaseSrc = path.join( PKG_ROOT, diff --git a/apps/cli/src/prompts/auth.ts b/apps/cli/src/prompts/auth.ts index cc3b17bd8..517dd34ce 100644 --- a/apps/cli/src/prompts/auth.ts +++ b/apps/cli/src/prompts/auth.ts @@ -11,25 +11,45 @@ export async function getAuthChoice( ) { if (auth !== undefined) return auth; if (backend === "convex") { - const unsupportedFrontends = frontend?.filter((f) => - ["nuxt", "svelte", "solid"].includes(f), + const supportedBetterAuthFrontends = frontend?.some((f) => + ["tanstack-router", "tanstack-start", "next"].includes(f), ); - if (unsupportedFrontends && unsupportedFrontends.length > 0) { - return "none"; + const hasClerkCompatibleFrontends = frontend?.some((f) => + [ + "react-router", + "tanstack-router", + "tanstack-start", + "next", + "native-nativewind", + "native-unistyles", + ].includes(f), + ); + + const options = []; + + if (supportedBetterAuthFrontends) { + options.push({ + value: "better-auth", + label: "Better-Auth", + hint: "comprehensive auth framework for TypeScript", + }); + } + + if (hasClerkCompatibleFrontends) { + options.push({ + value: "clerk", + label: "Clerk", + hint: "More than auth, Complete User Management", + }); } + options.push({ value: "none", label: "None", hint: "No auth" }); + const response = await select({ message: "Select authentication provider", - options: [ - { - value: "clerk", - label: "Clerk", - hint: "More than auth, Complete User Management", - }, - { value: "none", label: "None" }, - ], - initialValue: "clerk", + options, + initialValue: "none", }); if (isCancel(response)) return exitCancelled("Operation cancelled"); return response as Auth; diff --git a/apps/cli/src/utils/add-package-deps.ts b/apps/cli/src/utils/add-package-deps.ts index 6e1479cd4..6be80ef34 100644 --- a/apps/cli/src/utils/add-package-deps.ts +++ b/apps/cli/src/utils/add-package-deps.ts @@ -6,9 +6,17 @@ import { type AvailableDependencies, dependencyVersionMap } from "../constants"; export const addPackageDependency = async (opts: { dependencies?: AvailableDependencies[]; devDependencies?: AvailableDependencies[]; + customDependencies?: Record; + customDevDependencies?: Record; projectDir: string; }) => { - const { dependencies = [], devDependencies = [], projectDir } = opts; + const { + dependencies = [], + devDependencies = [], + customDependencies = {}, + customDevDependencies = {}, + projectDir, + } = opts; const pkgJsonPath = path.join(projectDir, "package.json"); @@ -18,7 +26,8 @@ export const addPackageDependency = async (opts: { if (!pkgJson.devDependencies) pkgJson.devDependencies = {}; for (const pkgName of dependencies) { - const version = dependencyVersionMap[pkgName]; + const version = + customDependencies[pkgName] || dependencyVersionMap[pkgName]; if (version) { pkgJson.dependencies[pkgName] = version; } else { @@ -27,7 +36,8 @@ export const addPackageDependency = async (opts: { } for (const pkgName of devDependencies) { - const version = dependencyVersionMap[pkgName]; + const version = + customDevDependencies[pkgName] || dependencyVersionMap[pkgName]; if (version) { pkgJson.devDependencies[pkgName] = version; } else { diff --git a/apps/cli/src/utils/compatibility-rules.ts b/apps/cli/src/utils/compatibility-rules.ts index 79eae4841..473f02c2a 100644 --- a/apps/cli/src/utils/compatibility-rules.ts +++ b/apps/cli/src/utils/compatibility-rules.ts @@ -254,7 +254,7 @@ export function validateAddonsAgainstFrontends( export function validatePaymentsCompatibility( payments: Payments | undefined, auth: Auth | undefined, - backend: Backend | undefined, + _backend: Backend | undefined, frontends: Frontend[] = [], ) { if (!payments || payments === "none") return; @@ -266,12 +266,6 @@ export function validatePaymentsCompatibility( ); } - if (backend === "convex") { - exitWithError( - "Polar payments is not compatible with Convex backend. Please use a different backend or choose a different payments provider.", - ); - } - const { web } = splitFrontends(frontends); if (web.length === 0 && frontends.length > 0) { exitWithError( diff --git a/apps/cli/src/utils/config-validation.ts b/apps/cli/src/utils/config-validation.ts index 1e95e8233..85c94eade 100644 --- a/apps/cli/src/utils/config-validation.ts +++ b/apps/cli/src/utils/config-validation.ts @@ -235,9 +235,16 @@ export function validateConvexConstraints( } if (has("auth") && config.auth === "better-auth") { - exitWithError( - "Better-Auth is not compatible with Convex backend. Please use '--auth clerk' or '--auth none'.", + const supportedFrontends = ["tanstack-router", "tanstack-start", "next"]; + const hasSupportedFrontend = config.frontend?.some((f) => + supportedFrontends.includes(f), ); + + if (!hasSupportedFrontend) { + exitWithError( + "Better-Auth with Convex backend is only supported with TanStack Router, TanStack Start, or Next.js frontends. Please use '--auth clerk' or '--auth none'.", + ); + } } } diff --git a/apps/cli/templates/addons/biome/biome.json.hbs b/apps/cli/templates/addons/biome/biome.json.hbs index 1b82d0f16..f93a62a50 100644 --- a/apps/cli/templates/addons/biome/biome.json.hbs +++ b/apps/cli/templates/addons/biome/biome.json.hbs @@ -22,6 +22,7 @@ "!**/.expo", "!**/.wrangler", "!**/.alchemy", + "!**/.svelte-kit", "!**/wrangler.jsonc", "!**/.source" ] @@ -66,7 +67,7 @@ "quoteStyle": "double" } } - {{#if (or (eq frontend "svelte") (eq frontend "nuxt"))}} + {{#if (or (includes frontend "svelte") (includes frontend "nuxt"))}} , "overrides": [ { diff --git a/apps/cli/templates/addons/ultracite/biome.json.hbs b/apps/cli/templates/addons/ultracite/biome.json.hbs index 2af9dd29f..490d9aa42 100644 --- a/apps/cli/templates/addons/ultracite/biome.json.hbs +++ b/apps/cli/templates/addons/ultracite/biome.json.hbs @@ -17,6 +17,7 @@ "!**/.expo", "!**/.wrangler", "!**/.alchemy", + "!**/.svelte-kit", "!**/wrangler.jsonc", "!**/.source" ] diff --git a/apps/cli/templates/auth/better-auth/convex/backend/convex/auth.config.ts.hbs b/apps/cli/templates/auth/better-auth/convex/backend/convex/auth.config.ts.hbs new file mode 100644 index 000000000..b8c7f39d5 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/backend/convex/auth.config.ts.hbs @@ -0,0 +1,8 @@ +export default { + providers: [ + { + domain: process.env.CONVEX_SITE_URL, + applicationID: "convex", + }, + ], +}; \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs b/apps/cli/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs new file mode 100644 index 000000000..0e4ea7d3f --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs @@ -0,0 +1,48 @@ +import { createClient, type GenericCtx } from "@convex-dev/better-auth"; +{{#if (or (includes frontend "tanstack-start") (includes frontend "next"))}} +import { convex } from "@convex-dev/better-auth/plugins"; +{{else}} +import { convex, crossDomain } from "@convex-dev/better-auth/plugins"; +{{/if}} +import { components } from "./_generated/api"; +import { DataModel } from "./_generated/dataModel"; +import { query } from "./_generated/server"; +import { betterAuth } from "better-auth"; + +const siteUrl = process.env.SITE_URL!; + +export const authComponent = createClient(components.betterAuth); + +export const createAuth = ( + ctx: GenericCtx, + { optionsOnly } = { optionsOnly: false }, +) => { + return betterAuth({ + logger: { + disabled: optionsOnly, + }, + {{#if (or (includes frontend "tanstack-start") (includes frontend "next"))}} + baseUrl: siteUrl, + {{else}} + trustedOrigins: [siteUrl], + {{/if}} + database: authComponent.adapter(ctx), + emailAndPassword: { + enabled: true, + requireEmailVerification: false, + }, + plugins: [ + {{#unless (or (includes frontend "tanstack-start") (includes frontend "next"))}} + crossDomain({ siteUrl }), + {{/unless}} + convex(), + ], + }); +}; + +export const getCurrentUser = query({ + args: {}, + handler: async (ctx) => { + return authComponent.getAuthUser(ctx); + }, +}); \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/convex/backend/convex/convex.config.ts.hbs b/apps/cli/templates/auth/better-auth/convex/backend/convex/convex.config.ts.hbs new file mode 100644 index 000000000..a49fc735f --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/backend/convex/convex.config.ts.hbs @@ -0,0 +1,7 @@ +import { defineApp } from "convex/server"; +import betterAuth from "@convex-dev/better-auth/convex.config"; + +const app = defineApp(); +app.use(betterAuth); + +export default app; \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/convex/backend/convex/http.ts.hbs b/apps/cli/templates/auth/better-auth/convex/backend/convex/http.ts.hbs new file mode 100644 index 000000000..91d770bac --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/backend/convex/http.ts.hbs @@ -0,0 +1,12 @@ +import { httpRouter } from "convex/server"; +import { authComponent, createAuth } from "./auth"; + +const http = httpRouter(); + +{{#if (or (includes frontend "tanstack-start") (includes frontend "next"))}} +authComponent.registerRoutes(http, createAuth); +{{else}} +authComponent.registerRoutes(http, createAuth, { cors: true }); +{{/if}} + +export default http; \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/convex/backend/convex/privateData.ts.hbs b/apps/cli/templates/auth/better-auth/convex/backend/convex/privateData.ts.hbs new file mode 100644 index 000000000..224d924fa --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/backend/convex/privateData.ts.hbs @@ -0,0 +1,16 @@ +import { query } from "./_generated/server"; + +export const get = query({ + args: {}, + handler: async (ctx) => { + const identity = await ctx.auth.getUserIdentity(); + if (identity === null) { + return { + message: "Not authenticated", + }; + } + return { + message: "This is private", + }; + }, +}); diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/next/src/app/api/auth/[...all]/route.ts.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/app/api/auth/[...all]/route.ts.hbs new file mode 100644 index 000000000..6cfe400ef --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/app/api/auth/[...all]/route.ts.hbs @@ -0,0 +1,3 @@ +import { nextJsHandler } from "@convex-dev/better-auth/nextjs"; + +export const { GET, POST } = nextJsHandler(); \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/next/src/app/dashboard/page.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/app/dashboard/page.tsx.hbs new file mode 100644 index 000000000..7e65c074d --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/app/dashboard/page.tsx.hbs @@ -0,0 +1,40 @@ +"use client" + +import SignInForm from "@/components/sign-in-form"; +import SignUpForm from "@/components/sign-up-form"; +import UserMenu from "@/components/user-menu"; +import { api } from "@{{projectName}}/backend/convex/_generated/api"; +import { + Authenticated, + AuthLoading, + Unauthenticated, + useQuery, +} from "convex/react"; +import { useState } from "react"; + +export default function DashboardPage() { + const [showSignIn, setShowSignIn] = useState(false); + const privateData = useQuery(api.privateData.get); + + return ( + <> + +
+

Dashboard

+

privateData: {privateData?.message}

+ +
+
+ + {showSignIn ? ( + setShowSignIn(false)} /> + ) : ( + setShowSignIn(true)} /> + )} + + +
Loading...
+
+ + ); +} diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/next/src/components/sign-in-form.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/components/sign-in-form.tsx.hbs new file mode 100644 index 000000000..af4d51f3c --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/components/sign-in-form.tsx.hbs @@ -0,0 +1,129 @@ +import { authClient } from "@/lib/auth-client"; +import { useForm } from "@tanstack/react-form"; +import { toast } from "sonner"; +import z from "zod"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; +import { useRouter } from "next/navigation"; + +export default function SignInForm({ + onSwitchToSignUp, +}: { + onSwitchToSignUp: () => void; +}) { + const router = useRouter(); + + const form = useForm({ + defaultValues: { + email: "", + password: "", + }, + onSubmit: async ({ value }) => { + await authClient.signIn.email( + { + email: value.email, + password: value.password, + }, + { + onSuccess: () => { + router.push("/dashboard"); + toast.success("Sign in successful"); + }, + onError: (error) => { + toast.error(error.error.message || error.error.statusText); + }, + }, + ); + }, + validators: { + onSubmit: z.object({ + email: z.email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + }), + }, + }); + + return ( +
+

Welcome Back

+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-4" + > +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ + + {(state) => ( + + )} + +
+ +
+ +
+
+ ); +} diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/next/src/components/sign-up-form.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/components/sign-up-form.tsx.hbs new file mode 100644 index 000000000..c5999c3a1 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/components/sign-up-form.tsx.hbs @@ -0,0 +1,154 @@ +import { authClient } from "@/lib/auth-client"; +import { useForm } from "@tanstack/react-form"; +import { toast } from "sonner"; +import z from "zod"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; +import { useRouter } from "next/navigation"; + +export default function SignUpForm({ + onSwitchToSignIn, +}: { + onSwitchToSignIn: () => void; +}) { + const router = useRouter(); + + const form = useForm({ + defaultValues: { + email: "", + password: "", + name: "", + }, + onSubmit: async ({ value }) => { + await authClient.signUp.email( + { + email: value.email, + password: value.password, + name: value.name, + }, + { + onSuccess: () => { + router.push("/dashboard"); + toast.success("Sign up successful"); + }, + onError: (error) => { + toast.error(error.error.message || error.error.statusText); + }, + }, + ); + }, + validators: { + onSubmit: z.object({ + name: z.string().min(2, "Name must be at least 2 characters"), + email: z.email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + }), + }, + }); + + return ( +
+

Create Account

+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-4" + > +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ + + {(state) => ( + + )} + +
+ +
+ +
+
+ ); +} diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/next/src/components/user-menu.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/components/user-menu.tsx.hbs new file mode 100644 index 000000000..736d7fbf5 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/components/user-menu.tsx.hbs @@ -0,0 +1,48 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { authClient } from "@/lib/auth-client"; +import { Button } from "./ui/button"; +import { useRouter } from "next/navigation"; +import { useQuery } from "convex/react"; +import { api } from "@{{projectName}}/backend/convex/_generated/api"; + +export default function UserMenu() { + const router = useRouter(); + const user = useQuery(api.auth.getCurrentUser) + + return ( + + + + + + My Account + + {user?.email} + + + + + + ); +} diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/next/src/lib/auth-client.ts.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/lib/auth-client.ts.hbs new file mode 100644 index 000000000..a639a21dc --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/lib/auth-client.ts.hbs @@ -0,0 +1,6 @@ +import { createAuthClient } from "better-auth/react"; +import { convexClient } from "@convex-dev/better-auth/client/plugins"; + +export const authClient = createAuthClient({ + plugins: [convexClient()], +}); \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/next/src/lib/auth-server.ts.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/lib/auth-server.ts.hbs new file mode 100644 index 000000000..130d7b990 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/next/src/lib/auth-server.ts.hbs @@ -0,0 +1,6 @@ +import { createAuth } from "@{{projectName}}/backend/convex/auth"; +import { getToken as getTokenNextjs } from "@convex-dev/better-auth/nextjs"; + +export const getToken = () => { + return getTokenNextjs(createAuth); +}; \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/sign-in-form.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/sign-in-form.tsx.hbs new file mode 100644 index 000000000..1906d70fe --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/sign-in-form.tsx.hbs @@ -0,0 +1,133 @@ +import { authClient } from "@/lib/auth-client"; +import { useForm } from "@tanstack/react-form"; +import { useNavigate } from "@tanstack/react-router"; +import { toast } from "sonner"; +import z from "zod"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; + +export default function SignInForm({ + onSwitchToSignUp, +}: { + onSwitchToSignUp: () => void; +}) { + const navigate = useNavigate({ + from: "/", + }); + + const form = useForm({ + defaultValues: { + email: "", + password: "", + }, + onSubmit: async ({ value }) => { + await authClient.signIn.email( + { + email: value.email, + password: value.password, + }, + { + onSuccess: () => { + navigate({ + to: "/dashboard", + }); + toast.success("Sign in successful"); + }, + onError: (error) => { + toast.error(error.error.message || error.error.statusText); + }, + }, + ); + }, + validators: { + onSubmit: z.object({ + email: z.email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + }), + }, + }); + + return ( +
+

Welcome Back

+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-4" + > +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ + + {(state) => ( + + )} + +
+ +
+ +
+
+ ); +} diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/sign-up-form.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/sign-up-form.tsx.hbs new file mode 100644 index 000000000..a35c86fb6 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/sign-up-form.tsx.hbs @@ -0,0 +1,158 @@ +import { authClient } from "@/lib/auth-client"; +import { useForm } from "@tanstack/react-form"; +import { useNavigate } from "@tanstack/react-router"; +import { toast } from "sonner"; +import z from "zod"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; + +export default function SignUpForm({ + onSwitchToSignIn, +}: { + onSwitchToSignIn: () => void; +}) { + const navigate = useNavigate({ + from: "/", + }); + + const form = useForm({ + defaultValues: { + email: "", + password: "", + name: "", + }, + onSubmit: async ({ value }) => { + await authClient.signUp.email( + { + email: value.email, + password: value.password, + name: value.name, + }, + { + onSuccess: () => { + navigate({ + to: "/dashboard", + }); + toast.success("Sign up successful"); + }, + onError: (error) => { + toast.error(error.error.message || error.error.statusText); + }, + }, + ); + }, + validators: { + onSubmit: z.object({ + name: z.string().min(2, "Name must be at least 2 characters"), + email: z.email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + }), + }, + }); + + return ( +
+

Create Account

+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-4" + > +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ + + {(state) => ( + + )} + +
+ +
+ +
+
+ ); +} diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/user-menu.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/user-menu.tsx.hbs new file mode 100644 index 000000000..83324a224 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/components/user-menu.tsx.hbs @@ -0,0 +1,50 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { authClient } from "@/lib/auth-client"; +import { useNavigate } from "@tanstack/react-router"; +import { Button } from "./ui/button"; +import { useQuery } from "convex/react"; +import { api } from "@{{projectName}}/backend/convex/_generated/api"; + +export default function UserMenu() { + const navigate = useNavigate(); + const user = useQuery(api.auth.getCurrentUser) + + return ( + + + + + + My Account + + {user?.email} + + + + + + ); +} diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/lib/auth-client.ts.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/lib/auth-client.ts.hbs new file mode 100644 index 000000000..7cfe3198b --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/lib/auth-client.ts.hbs @@ -0,0 +1,10 @@ +import { createAuthClient } from "better-auth/react"; +import { + convexClient, + crossDomainClient, +} from "@convex-dev/better-auth/client/plugins"; + +export const authClient = createAuthClient({ + baseURL: import.meta.env.VITE_CONVEX_SITE_URL, + plugins: [convexClient(), crossDomainClient()], +}); \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/routes/dashboard.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/routes/dashboard.tsx.hbs new file mode 100644 index 000000000..c5faeaf08 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-router/src/routes/dashboard.tsx.hbs @@ -0,0 +1,43 @@ +import SignInForm from "@/components/sign-in-form"; +import SignUpForm from "@/components/sign-up-form"; +import UserMenu from "@/components/user-menu"; +import { api } from "@{{projectName}}/backend/convex/_generated/api"; +import { createFileRoute } from "@tanstack/react-router"; +import { + Authenticated, + AuthLoading, + Unauthenticated, + useQuery, +} from "convex/react"; +import { useState } from "react"; + +export const Route = createFileRoute("/dashboard")({ + component: RouteComponent, +}); + +function RouteComponent() { + const [showSignIn, setShowSignIn] = useState(false); + const privateData = useQuery(api.privateData.get); + + return ( + <> + +
+

Dashboard

+

privateData: {privateData?.message}

+ +
+
+ + {showSignIn ? ( + setShowSignIn(false)} /> + ) : ( + setShowSignIn(true)} /> + )} + + +
Loading...
+
+ + ); +} diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/sign-in-form.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/sign-in-form.tsx.hbs new file mode 100644 index 000000000..1906d70fe --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/sign-in-form.tsx.hbs @@ -0,0 +1,133 @@ +import { authClient } from "@/lib/auth-client"; +import { useForm } from "@tanstack/react-form"; +import { useNavigate } from "@tanstack/react-router"; +import { toast } from "sonner"; +import z from "zod"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; + +export default function SignInForm({ + onSwitchToSignUp, +}: { + onSwitchToSignUp: () => void; +}) { + const navigate = useNavigate({ + from: "/", + }); + + const form = useForm({ + defaultValues: { + email: "", + password: "", + }, + onSubmit: async ({ value }) => { + await authClient.signIn.email( + { + email: value.email, + password: value.password, + }, + { + onSuccess: () => { + navigate({ + to: "/dashboard", + }); + toast.success("Sign in successful"); + }, + onError: (error) => { + toast.error(error.error.message || error.error.statusText); + }, + }, + ); + }, + validators: { + onSubmit: z.object({ + email: z.email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + }), + }, + }); + + return ( +
+

Welcome Back

+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-4" + > +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ + + {(state) => ( + + )} + +
+ +
+ +
+
+ ); +} diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/sign-up-form.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/sign-up-form.tsx.hbs new file mode 100644 index 000000000..a35c86fb6 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/sign-up-form.tsx.hbs @@ -0,0 +1,158 @@ +import { authClient } from "@/lib/auth-client"; +import { useForm } from "@tanstack/react-form"; +import { useNavigate } from "@tanstack/react-router"; +import { toast } from "sonner"; +import z from "zod"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; + +export default function SignUpForm({ + onSwitchToSignIn, +}: { + onSwitchToSignIn: () => void; +}) { + const navigate = useNavigate({ + from: "/", + }); + + const form = useForm({ + defaultValues: { + email: "", + password: "", + name: "", + }, + onSubmit: async ({ value }) => { + await authClient.signUp.email( + { + email: value.email, + password: value.password, + name: value.name, + }, + { + onSuccess: () => { + navigate({ + to: "/dashboard", + }); + toast.success("Sign up successful"); + }, + onError: (error) => { + toast.error(error.error.message || error.error.statusText); + }, + }, + ); + }, + validators: { + onSubmit: z.object({ + name: z.string().min(2, "Name must be at least 2 characters"), + email: z.email("Invalid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + }), + }, + }); + + return ( +
+

Create Account

+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + className="space-y-4" + > +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ +
+ + {(field) => ( +
+ + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

+ {error?.message} +

+ ))} +
+ )} +
+
+ + + {(state) => ( + + )} + +
+ +
+ +
+
+ ); +} diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/user-menu.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/user-menu.tsx.hbs new file mode 100644 index 000000000..83324a224 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/components/user-menu.tsx.hbs @@ -0,0 +1,50 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { authClient } from "@/lib/auth-client"; +import { useNavigate } from "@tanstack/react-router"; +import { Button } from "./ui/button"; +import { useQuery } from "convex/react"; +import { api } from "@{{projectName}}/backend/convex/_generated/api"; + +export default function UserMenu() { + const navigate = useNavigate(); + const user = useQuery(api.auth.getCurrentUser) + + return ( + + + + + + My Account + + {user?.email} + + + + + + ); +} diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-client.ts.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-client.ts.hbs new file mode 100644 index 000000000..fcf05a3e4 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-client.ts.hbs @@ -0,0 +1,6 @@ +import { createAuthClient } from "better-auth/react"; +import { convexClient } from "@convex-dev/better-auth/client/plugins"; + +export const authClient = createAuthClient({ + plugins: [convexClient()], +}); \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-server.ts.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-server.ts.hbs new file mode 100644 index 000000000..74fe4294e --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/lib/auth-server.ts.hbs @@ -0,0 +1,5 @@ +import { createAuth } from "convex/auth"; +import { setupFetchClient } from "@convex-dev/better-auth/react-start"; + +export const { fetchQuery, fetchMutation, fetchAction } = + setupFetchClient(createAuth); \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/api/auth/$.ts.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/api/auth/$.ts.hbs new file mode 100644 index 000000000..a97bb2532 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/api/auth/$.ts.hbs @@ -0,0 +1,11 @@ +import { reactStartHandler } from '@convex-dev/better-auth/react-start' +import { createServerFileRoute } from '@tanstack/react-start/server' + +export const ServerRoute = createServerFileRoute('/api/auth/$').methods({ + GET: ({ request }) => { + return reactStartHandler(request) + }, + POST: ({ request }) => { + return reactStartHandler(request) + }, +}) \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/dashboard.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/dashboard.tsx.hbs new file mode 100644 index 000000000..c5faeaf08 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/web/react/tanstack-start/src/routes/dashboard.tsx.hbs @@ -0,0 +1,43 @@ +import SignInForm from "@/components/sign-in-form"; +import SignUpForm from "@/components/sign-up-form"; +import UserMenu from "@/components/user-menu"; +import { api } from "@{{projectName}}/backend/convex/_generated/api"; +import { createFileRoute } from "@tanstack/react-router"; +import { + Authenticated, + AuthLoading, + Unauthenticated, + useQuery, +} from "convex/react"; +import { useState } from "react"; + +export const Route = createFileRoute("/dashboard")({ + component: RouteComponent, +}); + +function RouteComponent() { + const [showSignIn, setShowSignIn] = useState(false); + const privateData = useQuery(api.privateData.get); + + return ( + <> + +
+

Dashboard

+

privateData: {privateData?.message}

+ +
+
+ + {showSignIn ? ( + setShowSignIn(false)} /> + ) : ( + setShowSignIn(true)} /> + )} + + +
Loading...
+
+ + ); +} diff --git a/apps/cli/templates/auth/better-auth/web/react/next/src/components/theme-provider.tsx.hbs b/apps/cli/templates/auth/better-auth/web/react/next/src/components/theme-provider.tsx.hbs deleted file mode 100644 index 6a1ffe4d0..000000000 --- a/apps/cli/templates/auth/better-auth/web/react/next/src/components/theme-provider.tsx.hbs +++ /dev/null @@ -1,11 +0,0 @@ -"use client" - -import * as React from "react" -import { ThemeProvider as NextThemesProvider } from "next-themes" - -export function ThemeProvider({ - children, - ...props -}: React.ComponentProps) { - return {children} -} diff --git a/apps/cli/templates/frontend/react/next/src/components/providers.tsx.hbs b/apps/cli/templates/frontend/react/next/src/components/providers.tsx.hbs index 36ebfaead..868c42260 100644 --- a/apps/cli/templates/frontend/react/next/src/components/providers.tsx.hbs +++ b/apps/cli/templates/frontend/react/next/src/components/providers.tsx.hbs @@ -5,6 +5,10 @@ import { useAuth } from "@clerk/nextjs"; import { ConvexReactClient } from "convex/react"; import { ConvexProviderWithClerk } from "convex/react-clerk"; +{{else if (eq auth "better-auth")}} +import { ConvexProvider, ConvexReactClient } from "convex/react"; +import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; +import { authClient } from "@/lib/auth-client"; {{else}} import { ConvexProvider, ConvexReactClient } from "convex/react"; {{/if}} @@ -44,6 +48,10 @@ export default function Providers({ {children} + {{else if (eq auth "better-auth")}} + + {children} + {{else}} {children} {{/if}} diff --git a/apps/cli/templates/frontend/react/tanstack-router/src/main.tsx.hbs b/apps/cli/templates/frontend/react/tanstack-router/src/main.tsx.hbs index 0898548c2..e337cc816 100644 --- a/apps/cli/templates/frontend/react/tanstack-router/src/main.tsx.hbs +++ b/apps/cli/templates/frontend/react/tanstack-router/src/main.tsx.hbs @@ -16,10 +16,15 @@ import { routeTree } from "./routeTree.gen"; {{#if (eq auth "clerk")}} import { ClerkProvider, useAuth } from "@clerk/clerk-react"; import { ConvexProviderWithClerk } from "convex/react-clerk"; + {{else if (eq auth "better-auth")}} + import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; + import { authClient } from "@/lib/auth-client"; {{else}} import { ConvexProvider } from "convex/react"; {{/if}} - const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string); + const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string{{#if (eq auth "better-auth")}}, { + expectAuth: true, + }{{/if}}); {{/if}} const router = createRouter({ @@ -57,6 +62,8 @@ const router = createRouter({ ); + {{else if (eq auth "better-auth")}} + return {children}; {{else}} return {children}; {{/if}} diff --git a/apps/cli/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs b/apps/cli/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs index 2b4c8cc87..5525c7641 100644 --- a/apps/cli/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs +++ b/apps/cli/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs @@ -8,7 +8,9 @@ import { Scripts, createRootRouteWithContext, useRouterState, +{{#if (and (eq backend "convex") (or (eq auth "clerk") (eq auth "better-auth")))}} useRouteContext, +{{/if}} } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import Header from "../components/header"; @@ -36,6 +38,23 @@ const fetchClerkAuth = createServerFn({ method: "GET" }).handler(async () => { const token = await auth.getToken({ template: "convex" }); return { userId: auth.userId, token }; }); +{{else if (and (eq backend "convex") (eq auth "better-auth"))}} +import { createServerFn } from "@tanstack/react-start"; +import { getWebRequest, getCookie } from "@tanstack/react-start/server"; +import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; +import { fetchSession, getCookieName } from "@convex-dev/better-auth/react-start"; +import { authClient } from "@/lib/auth-client"; +import { createAuth } from "@{{projectName}}/backend/convex/auth"; + +const fetchAuth = createServerFn({ method: "GET" }).handler(async () => { + const { session } = await fetchSession(getWebRequest()); + const sessionCookieName = getCookieName(createAuth); + const token = getCookie(sessionCookieName); + return { + userId: session?.user.id, + token, + }; +}); {{/if}} {{#if (eq backend "convex")}} @@ -95,6 +114,14 @@ export const Route = createRootRouteWithContext()({ } return { userId, token }; }, + {{else if (and (eq backend "convex") (eq auth "better-auth"))}} + beforeLoad: async (ctx) => { + const { userId, token } = await fetchAuth(); + if (token) { + ctx.context.convexQueryClient.serverHttpClient?.setAuth(token); + } + return { userId, token }; + }, {{/if}} }); @@ -122,6 +149,26 @@ function RootDocument() { ); + {{else if (and (eq backend "convex") (eq auth "better-auth"))}} + const context = useRouteContext({ from: Route.id }); + return ( + + + + + + +
+
+ {isFetching ? : } +
+ + + + + +
+ ); {{else}} return ( diff --git a/apps/cli/templates/frontend/react/tanstack-start/src/routes/index.tsx.hbs b/apps/cli/templates/frontend/react/tanstack-start/src/routes/index.tsx.hbs index 390677a47..37c532f3a 100644 --- a/apps/cli/templates/frontend/react/tanstack-start/src/routes/index.tsx.hbs +++ b/apps/cli/templates/frontend/react/tanstack-start/src/routes/index.tsx.hbs @@ -1,7 +1,7 @@ import { createFileRoute } from "@tanstack/react-router"; {{#if (eq backend "convex")}} import { convexQuery } from "@convex-dev/react-query"; -import { useSuspenseQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { api } from "@{{projectName}}/backend/convex/_generated/api"; {{else if (or (eq api "trpc") (eq api "orpc"))}} import { useQuery } from "@tanstack/react-query"; @@ -35,7 +35,7 @@ const TITLE_TEXT = ` function HomeComponent() { {{#if (eq backend "convex")}} - const healthCheck = useSuspenseQuery(convexQuery(api.healthCheck.get, {})); + const healthCheck = useQuery(convexQuery(api.healthCheck.get, {})); {{else if (eq api "trpc")}} const trpc = useTRPC(); const healthCheck = useQuery(trpc.healthCheck.queryOptions()); diff --git a/apps/cli/templates/frontend/react/web-base/src/components/header.tsx.hbs b/apps/cli/templates/frontend/react/web-base/src/components/header.tsx.hbs index 7f54d591a..2770a3226 100644 --- a/apps/cli/templates/frontend/react/web-base/src/components/header.tsx.hbs +++ b/apps/cli/templates/frontend/react/web-base/src/components/header.tsx.hbs @@ -9,7 +9,7 @@ import { Link } from "@tanstack/react-router"; {{#unless (includes frontend "tanstack-start")}} import { ModeToggle } from "./mode-toggle"; {{/unless}} -{{#if (eq auth "better-auth")}} +{{#if (and (eq auth "better-auth") (ne backend "convex"))}} import UserMenu from "./user-menu"; {{/if}} @@ -67,7 +67,7 @@ export default function Header() { {{#unless (includes frontend "tanstack-start")}} {{/unless}} - {{#if (eq auth "better-auth")}} + {{#if (and (eq auth "better-auth") (ne backend "convex"))}} {{/if}} diff --git a/apps/cli/templates/frontend/react/web-base/src/components/loader.tsx b/apps/cli/templates/frontend/react/web-base/src/components/loader.tsx.hbs similarity index 100% rename from apps/cli/templates/frontend/react/web-base/src/components/loader.tsx rename to apps/cli/templates/frontend/react/web-base/src/components/loader.tsx.hbs diff --git a/apps/web/src/app/(home)/new/_components/utils.ts b/apps/web/src/app/(home)/new/_components/utils.ts index 13bde1d30..da086494c 100644 --- a/apps/web/src/app/(home)/new/_components/utils.ts +++ b/apps/web/src/app/(home)/new/_components/utils.ts @@ -102,7 +102,16 @@ export const analyzeStackCompatibility = ( ["native-nativewind", "native-unistyles"].includes(f), ); - if (nextStack.auth !== "clerk" || !hasClerkCompatibleFrontend) { + const hasBetterAuthCompatibleFrontend = nextStack.webFrontend.some((f) => + ["tanstack-router", "tanstack-start", "next"].includes(f), + ); + + if (nextStack.auth === "clerk" && !hasClerkCompatibleFrontend) { + convexOverrides.auth = "none"; + } else if ( + nextStack.auth === "better-auth" && + !hasBetterAuthCompatibleFrontend + ) { convexOverrides.auth = "none"; } @@ -801,21 +810,27 @@ export const analyzeStackCompatibility = ( } if (nextStack.backend === "convex" && nextStack.auth === "better-auth") { - notes.auth.notes.push( - "Better-Auth is not compatible with Convex backend. Auth will be set to 'None'.", + const hasBetterAuthCompatibleFrontend = nextStack.webFrontend.some( + (f) => ["tanstack-router", "tanstack-start", "next"].includes(f), ); - notes.backend.notes.push( - "Convex backend only supports Clerk auth or no auth. Auth will be disabled.", - ); - notes.auth.hasIssue = true; - notes.backend.hasIssue = true; - nextStack.auth = "none"; - changed = true; - changes.push({ - category: "auth", - message: - "Auth set to 'None' (Better-Auth not compatible with Convex backend - use Clerk instead)", - }); + + if (!hasBetterAuthCompatibleFrontend) { + notes.auth.notes.push( + "Better-Auth with Convex requires TanStack Router, TanStack Start, or Next.js frontend. Auth will be set to 'None'.", + ); + notes.backend.notes.push( + "Convex backend with Better-Auth requires compatible frontend. Auth will be disabled.", + ); + notes.auth.hasIssue = true; + notes.backend.hasIssue = true; + nextStack.auth = "none"; + changed = true; + changes.push({ + category: "auth", + message: + "Auth set to 'None' (Better-Auth with Convex requires TanStack Router, TanStack Start, or Next.js frontend)", + }); + } } if (nextStack.payments === "polar") { @@ -1165,7 +1180,13 @@ export const getDisabledReason = ( return "Convex backend requires DB Setup to be 'None'. Convex handles database setup automatically."; } if (category === "auth" && optionId === "better-auth") { - return "Convex backend is not compatible with Better-Auth. Use Clerk authentication instead."; + const hasBetterAuthCompatibleFrontend = currentStack.webFrontend.some( + (f) => ["tanstack-router", "tanstack-start", "next"].includes(f), + ); + + if (!hasBetterAuthCompatibleFrontend) { + return "Better-Auth with Convex requires TanStack Router, TanStack Start, or Next.js frontend."; + } } } @@ -1262,7 +1283,13 @@ export const getDisabledReason = ( if (category === "auth" && optionId === "better-auth") { if (finalStack.backend === "convex") { - return "Better-Auth is not compatible with Convex backend. Use Clerk authentication instead."; + const hasBetterAuthCompatibleFrontend = finalStack.webFrontend.some((f) => + ["tanstack-router", "tanstack-start", "next"].includes(f), + ); + + if (!hasBetterAuthCompatibleFrontend) { + return "Better-Auth with Convex requires TanStack Router, TanStack Start, or Next.js frontend."; + } } }