Skip to content

Commit 24e33c6

Browse files
feat(cli): add convex + better-auth (#582)
1 parent c581f1c commit 24e33c6

File tree

44 files changed

+1602
-66
lines changed

Some content is hidden

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

44 files changed

+1602
-66
lines changed

apps/cli/src/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,12 @@ export const dependencyVersionMap = {
126126
"@trpc/server": "^11.5.0",
127127
"@trpc/client": "^11.5.0",
128128

129-
convex: "^1.25.4",
129+
convex: "^1.27.0",
130130
"@convex-dev/react-query": "^0.0.0-alpha.8",
131131
"convex-svelte": "^0.0.11",
132132
"convex-nuxt": "0.1.5",
133133
"convex-vue": "^0.1.5",
134+
"@convex-dev/better-auth": "^0.8.4",
134135

135136
"@tanstack/svelte-query": "^5.85.3",
136137
"@tanstack/svelte-query-devtools": "^5.85.3",

apps/cli/src/helpers/core/auth-setup.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,47 @@ export async function setupAuth(config: ProjectConfig) {
4646
}
4747
}
4848

49+
if (auth === "better-auth") {
50+
const convexBackendDir = path.join(projectDir, "packages/backend");
51+
const convexBackendDirExists = await fs.pathExists(convexBackendDir);
52+
53+
if (convexBackendDirExists) {
54+
await addPackageDependency({
55+
dependencies: ["better-auth", "@convex-dev/better-auth"],
56+
customDependencies: { "better-auth": "1.3.8" },
57+
projectDir: convexBackendDir,
58+
});
59+
}
60+
61+
if (clientDirExists) {
62+
const hasNextJs = frontend.includes("next");
63+
const hasTanStackStart = frontend.includes("tanstack-start");
64+
const hasViteReactOther = frontend.some((f) =>
65+
["tanstack-router", "react-router"].includes(f),
66+
);
67+
68+
if (hasNextJs) {
69+
await addPackageDependency({
70+
dependencies: ["better-auth", "@convex-dev/better-auth"],
71+
customDependencies: { "better-auth": "1.3.8" },
72+
projectDir: clientDir,
73+
});
74+
} else if (hasTanStackStart) {
75+
await addPackageDependency({
76+
dependencies: ["better-auth", "@convex-dev/better-auth"],
77+
customDependencies: { "better-auth": "1.3.8" },
78+
projectDir: clientDir,
79+
});
80+
} else if (hasViteReactOther) {
81+
await addPackageDependency({
82+
dependencies: ["better-auth", "@convex-dev/better-auth"],
83+
customDependencies: { "better-auth": "1.3.8" },
84+
projectDir: clientDir,
85+
});
86+
}
87+
}
88+
}
89+
4990
const hasNativeWind = frontend.includes("native-nativewind");
5091
const hasUnistyles = frontend.includes("native-unistyles");
5192
if (

apps/cli/src/helpers/core/convex-codegen.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { execa } from "execa";
33
import type { PackageManager } from "../../types";
44
import { getPackageExecutionCommand } from "../../utils/package-runner";
55

6+
// having problems running this in convex + better-auth
67
export async function runConvexCodegen(
78
projectDir: string,
89
packageManager: PackageManager | null | undefined,

apps/cli/src/helpers/core/create-project.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { setupRuntime } from "../core/runtime-setup";
1212
import { setupServerDeploy } from "../deployment/server-deploy-setup";
1313
import { setupWebDeploy } from "../deployment/web-deploy-setup";
1414
import { setupAuth } from "./auth-setup";
15-
import { runConvexCodegen } from "./convex-codegen";
1615
import { createReadme } from "./create-readme";
1716
import { setupEnvironmentVariables } from "./env-setup";
1817
import { initializeGit } from "./git";
@@ -97,10 +96,6 @@ export async function createProject(
9796

9897
await writeBtsConfig(options);
9998

100-
if (isConvex) {
101-
await runConvexCodegen(projectDir, options.packageManager);
102-
}
103-
10499
log.success("Project template successfully scaffolded!");
105100

106101
if (options.install) {

apps/cli/src/helpers/core/env-setup.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface EnvVariable {
77
key: string;
88
value: string | null | undefined;
99
condition: boolean;
10+
comment?: string;
1011
}
1112

1213
export async function addEnvVariablesToFile(
@@ -24,7 +25,7 @@ export async function addEnvVariablesToFile(
2425
let contentToAdd = "";
2526
const exampleVariables: string[] = [];
2627

27-
for (const { key, value, condition } of variables) {
28+
for (const { key, value, condition, comment } of variables) {
2829
if (condition) {
2930
const regex = new RegExp(`^${key}=.*$`, "m");
3031
const valueToWrite = value ?? "";
@@ -37,6 +38,9 @@ export async function addEnvVariablesToFile(
3738
modified = true;
3839
}
3940
} else {
41+
if (comment) {
42+
contentToAdd += `# ${comment}\n`;
43+
}
4044
contentToAdd += `${key}=${valueToWrite}\n`;
4145
modified = true;
4246
}
@@ -179,6 +183,22 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
179183
}
180184
}
181185

186+
if (backend === "convex" && auth === "better-auth") {
187+
if (hasNextJs) {
188+
clientVars.push({
189+
key: "NEXT_PUBLIC_CONVEX_SITE_URL",
190+
value: "https://<YOUR_CONVEX_URL>",
191+
condition: true,
192+
});
193+
} else if (hasReactRouter || hasTanStackRouter || hasTanStackStart) {
194+
clientVars.push({
195+
key: "VITE_CONVEX_SITE_URL",
196+
value: "https://<YOUR_CONVEX_URL>",
197+
condition: true,
198+
});
199+
}
200+
}
201+
182202
await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
183203
}
184204
}
@@ -217,6 +237,43 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
217237
}
218238

219239
if (backend === "convex") {
240+
if (auth === "better-auth") {
241+
const convexBackendDir = path.join(projectDir, "packages/backend");
242+
if (await fs.pathExists(convexBackendDir)) {
243+
const envLocalPath = path.join(convexBackendDir, ".env.local");
244+
245+
if (
246+
!(await fs.pathExists(envLocalPath)) ||
247+
!(await fs.readFile(envLocalPath, "utf8")).includes(
248+
"npx convex env set",
249+
)
250+
) {
251+
const convexCommands = `# Set Convex environment variables
252+
npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
253+
npx convex env set SITE_URL http://localhost:3001
254+
255+
`;
256+
await fs.appendFile(envLocalPath, convexCommands);
257+
}
258+
259+
const convexBackendVars: EnvVariable[] = [
260+
{
261+
key: hasNextJs
262+
? "NEXT_PUBLIC_CONVEX_SITE_URL"
263+
: "VITE_CONVEX_SITE_URL",
264+
value: "",
265+
condition: true,
266+
comment: "Same as CONVEX_URL but ends in .site",
267+
},
268+
{
269+
key: "SITE_URL",
270+
value: "http://localhost:3001",
271+
condition: true,
272+
},
273+
];
274+
await addEnvVariablesToFile(envLocalPath, convexBackendVars);
275+
}
276+
}
220277
return;
221278
}
222279

apps/cli/src/helpers/core/template-manager.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,59 @@ export async function setupAuthTemplate(
455455
return;
456456
}
457457

458+
if (context.backend === "convex" && authProvider === "better-auth") {
459+
const convexBackendDestDir = path.join(projectDir, "packages/backend");
460+
const convexBetterAuthBackendSrc = path.join(
461+
PKG_ROOT,
462+
"templates/auth/better-auth/convex/backend",
463+
);
464+
if (await fs.pathExists(convexBetterAuthBackendSrc)) {
465+
await fs.ensureDir(convexBackendDestDir);
466+
await processAndCopyFiles(
467+
"**/*",
468+
convexBetterAuthBackendSrc,
469+
convexBackendDestDir,
470+
context,
471+
);
472+
}
473+
474+
if (webAppDirExists && hasReactWeb) {
475+
const convexBetterAuthWebBaseSrc = path.join(
476+
PKG_ROOT,
477+
"templates/auth/better-auth/convex/web/react/base",
478+
);
479+
if (await fs.pathExists(convexBetterAuthWebBaseSrc)) {
480+
await processAndCopyFiles(
481+
"**/*",
482+
convexBetterAuthWebBaseSrc,
483+
webAppDir,
484+
context,
485+
);
486+
}
487+
488+
const reactFramework = context.frontend.find((f) =>
489+
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
490+
f,
491+
),
492+
);
493+
if (reactFramework) {
494+
const convexBetterAuthWebSrc = path.join(
495+
PKG_ROOT,
496+
`templates/auth/better-auth/convex/web/react/${reactFramework}`,
497+
);
498+
if (await fs.pathExists(convexBetterAuthWebSrc)) {
499+
await processAndCopyFiles(
500+
"**/*",
501+
convexBetterAuthWebSrc,
502+
webAppDir,
503+
context,
504+
);
505+
}
506+
}
507+
}
508+
return;
509+
}
510+
458511
if (serverAppDirExists && context.backend !== "convex") {
459512
const authServerBaseSrc = path.join(
460513
PKG_ROOT,

apps/cli/src/prompts/auth.ts

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,45 @@ export async function getAuthChoice(
1111
) {
1212
if (auth !== undefined) return auth;
1313
if (backend === "convex") {
14-
const unsupportedFrontends = frontend?.filter((f) =>
15-
["nuxt", "svelte", "solid"].includes(f),
14+
const supportedBetterAuthFrontends = frontend?.some((f) =>
15+
["tanstack-router", "tanstack-start", "next"].includes(f),
1616
);
1717

18-
if (unsupportedFrontends && unsupportedFrontends.length > 0) {
19-
return "none";
18+
const hasClerkCompatibleFrontends = frontend?.some((f) =>
19+
[
20+
"react-router",
21+
"tanstack-router",
22+
"tanstack-start",
23+
"next",
24+
"native-nativewind",
25+
"native-unistyles",
26+
].includes(f),
27+
);
28+
29+
const options = [];
30+
31+
if (supportedBetterAuthFrontends) {
32+
options.push({
33+
value: "better-auth",
34+
label: "Better-Auth",
35+
hint: "comprehensive auth framework for TypeScript",
36+
});
37+
}
38+
39+
if (hasClerkCompatibleFrontends) {
40+
options.push({
41+
value: "clerk",
42+
label: "Clerk",
43+
hint: "More than auth, Complete User Management",
44+
});
2045
}
2146

47+
options.push({ value: "none", label: "None", hint: "No auth" });
48+
2249
const response = await select({
2350
message: "Select authentication provider",
24-
options: [
25-
{
26-
value: "clerk",
27-
label: "Clerk",
28-
hint: "More than auth, Complete User Management",
29-
},
30-
{ value: "none", label: "None" },
31-
],
32-
initialValue: "clerk",
51+
options,
52+
initialValue: "none",
3353
});
3454
if (isCancel(response)) return exitCancelled("Operation cancelled");
3555
return response as Auth;

apps/cli/src/utils/add-package-deps.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@ import { type AvailableDependencies, dependencyVersionMap } from "../constants";
66
export const addPackageDependency = async (opts: {
77
dependencies?: AvailableDependencies[];
88
devDependencies?: AvailableDependencies[];
9+
customDependencies?: Record<string, string>;
10+
customDevDependencies?: Record<string, string>;
911
projectDir: string;
1012
}) => {
11-
const { dependencies = [], devDependencies = [], projectDir } = opts;
13+
const {
14+
dependencies = [],
15+
devDependencies = [],
16+
customDependencies = {},
17+
customDevDependencies = {},
18+
projectDir,
19+
} = opts;
1220

1321
const pkgJsonPath = path.join(projectDir, "package.json");
1422

@@ -18,7 +26,8 @@ export const addPackageDependency = async (opts: {
1826
if (!pkgJson.devDependencies) pkgJson.devDependencies = {};
1927

2028
for (const pkgName of dependencies) {
21-
const version = dependencyVersionMap[pkgName];
29+
const version =
30+
customDependencies[pkgName] || dependencyVersionMap[pkgName];
2231
if (version) {
2332
pkgJson.dependencies[pkgName] = version;
2433
} else {
@@ -27,7 +36,8 @@ export const addPackageDependency = async (opts: {
2736
}
2837

2938
for (const pkgName of devDependencies) {
30-
const version = dependencyVersionMap[pkgName];
39+
const version =
40+
customDevDependencies[pkgName] || dependencyVersionMap[pkgName];
3141
if (version) {
3242
pkgJson.devDependencies[pkgName] = version;
3343
} else {

apps/cli/src/utils/compatibility-rules.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ export function validateAddonsAgainstFrontends(
254254
export function validatePaymentsCompatibility(
255255
payments: Payments | undefined,
256256
auth: Auth | undefined,
257-
backend: Backend | undefined,
257+
_backend: Backend | undefined,
258258
frontends: Frontend[] = [],
259259
) {
260260
if (!payments || payments === "none") return;
@@ -266,12 +266,6 @@ export function validatePaymentsCompatibility(
266266
);
267267
}
268268

269-
if (backend === "convex") {
270-
exitWithError(
271-
"Polar payments is not compatible with Convex backend. Please use a different backend or choose a different payments provider.",
272-
);
273-
}
274-
275269
const { web } = splitFrontends(frontends);
276270
if (web.length === 0 && frontends.length > 0) {
277271
exitWithError(

apps/cli/src/utils/config-validation.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,16 @@ export function validateConvexConstraints(
235235
}
236236

237237
if (has("auth") && config.auth === "better-auth") {
238-
exitWithError(
239-
"Better-Auth is not compatible with Convex backend. Please use '--auth clerk' or '--auth none'.",
238+
const supportedFrontends = ["tanstack-router", "tanstack-start", "next"];
239+
const hasSupportedFrontend = config.frontend?.some((f) =>
240+
supportedFrontends.includes(f),
240241
);
242+
243+
if (!hasSupportedFrontend) {
244+
exitWithError(
245+
"Better-Auth with Convex backend is only supported with TanStack Router, TanStack Start, or Next.js frontends. Please use '--auth clerk' or '--auth none'.",
246+
);
247+
}
241248
}
242249
}
243250

0 commit comments

Comments
 (0)