feat: add clerk auth support with convex (#548)

This commit is contained in:
Aman Varshney
2025-08-29 00:21:08 +05:30
committed by GitHub
parent 8d48ae0359
commit 54bcdf1cbc
153 changed files with 1954 additions and 771 deletions

View File

@@ -13,7 +13,7 @@ export const DEFAULT_CONFIG_BASE = {
frontend: ["tanstack-router"],
database: "sqlite",
orm: "drizzle",
auth: true,
auth: "better-auth",
addons: ["turborepo"],
examples: [],
git: true,
@@ -43,6 +43,11 @@ export const dependencyVersionMap = {
"better-auth": "^1.3.7",
"@better-auth/expo": "^1.3.7",
"@clerk/nextjs": "^6.31.5",
"@clerk/clerk-react": "^5.45.0",
"@clerk/tanstack-react-start": "^0.23.1",
"@clerk/clerk-expo": "^2.14.25",
"drizzle-orm": "^0.44.2",
"drizzle-kit": "^0.31.2",

View File

@@ -44,7 +44,7 @@ export async function addAddonsToProject(
frontend: detectedConfig.frontend || [],
addons: input.addons,
examples: detectedConfig.examples || [],
auth: detectedConfig.auth || false,
auth: detectedConfig.auth || "none",
git: false,
packageManager:
input.packageManager || detectedConfig.packageManager || "npm",

View File

@@ -68,7 +68,7 @@ export async function addDeploymentToProject(
frontend: detectedConfig.frontend || [],
addons: detectedConfig.addons || [],
examples: detectedConfig.examples || [],
auth: detectedConfig.auth || false,
auth: detectedConfig.auth || "none",
git: false,
packageManager:
input.packageManager || detectedConfig.packageManager || "npm",

View File

@@ -7,7 +7,7 @@ import { addPackageDependency } from "../../utils/add-package-deps";
export async function setupAuth(config: ProjectConfig) {
const { auth, frontend, backend, projectDir } = config;
if (backend === "convex" || !auth) {
if (!auth || auth === "none") {
return;
}
@@ -20,7 +20,48 @@ export async function setupAuth(config: ProjectConfig) {
const serverDirExists = await fs.pathExists(serverDir);
try {
if (serverDirExists) {
if (backend === "convex") {
if (auth === "clerk" && 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: ["@clerk/nextjs"],
projectDir: clientDir,
});
} else if (hasTanStackStart) {
await addPackageDependency({
dependencies: ["@clerk/tanstack-react-start"],
projectDir: clientDir,
});
} else if (hasViteReactOther) {
await addPackageDependency({
dependencies: ["@clerk/clerk-react"],
projectDir: clientDir,
});
}
}
const hasNativeWind = frontend.includes("native-nativewind");
const hasUnistyles = frontend.includes("native-unistyles");
if (
auth === "clerk" &&
nativeDirExists &&
(hasNativeWind || hasUnistyles)
) {
await addPackageDependency({
dependencies: ["@clerk/clerk-expo"],
projectDir: nativeDir,
});
}
return;
}
if (serverDirExists && auth === "better-auth") {
await addPackageDependency({
dependencies: ["better-auth"],
projectDir: serverDir,
@@ -40,10 +81,12 @@ export async function setupAuth(config: ProjectConfig) {
);
if (hasWebFrontend && clientDirExists) {
await addPackageDependency({
dependencies: ["better-auth"],
projectDir: clientDir,
});
if (auth === "better-auth") {
await addPackageDependency({
dependencies: ["better-auth"],
projectDir: clientDir,
});
}
}
if (
@@ -51,15 +94,17 @@ export async function setupAuth(config: ProjectConfig) {
frontend.includes("native-unistyles")) &&
nativeDirExists
) {
await addPackageDependency({
dependencies: ["better-auth", "@better-auth/expo"],
projectDir: nativeDir,
});
if (serverDirExists) {
if (auth === "better-auth") {
await addPackageDependency({
dependencies: ["@better-auth/expo"],
projectDir: serverDir,
dependencies: ["better-auth", "@better-auth/expo"],
projectDir: nativeDir,
});
if (serverDirExists) {
await addPackageDependency({
dependencies: ["@better-auth/expo"],
projectDir: serverDir,
});
}
}
}
} catch (error) {

View File

@@ -103,7 +103,7 @@ export async function createProjectHandler(
frontend: [],
addons: [],
examples: [],
auth: false,
auth: "none",
git: false,
packageManager: "npm",
install: false,
@@ -154,11 +154,11 @@ export async function createProjectHandler(
if (config.backend === "convex") {
log.info(
"Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo",
`Due to '--backend convex' flag, the following options have been automatically set: database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo`,
);
} else if (config.backend === "none") {
log.info(
"Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
"Due to '--backend none', the following options have been automatically set: --auth none, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
);
}

View File

@@ -5,7 +5,6 @@ import { writeBtsConfig } from "../../utils/bts-config";
import { exitWithError } from "../../utils/errors";
import { formatProjectWithBiome } from "../../utils/format-with-biome";
import { setupAddons } from "../addons/addons-setup";
import { setupAuth } from "../addons/auth-setup";
import { setupExamples } from "../addons/examples-setup";
import { setupApi } from "../core/api-setup";
import { setupBackendDependencies } from "../core/backend-setup";
@@ -13,6 +12,7 @@ import { setupDatabase } from "../core/db-setup";
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";
@@ -46,8 +46,8 @@ export async function createProject(options: ProjectConfig) {
if (!isConvex) {
await setupDbOrmTemplates(projectDir, options);
await setupDockerComposeTemplates(projectDir, options);
await setupAuthTemplate(projectDir, options);
}
await setupAuthTemplate(projectDir, options);
if (options.examples.length > 0 && options.examples[0] !== "none") {
await setupExamplesTemplate(projectDir, options);
}
@@ -70,7 +70,7 @@ export async function createProject(options: ProjectConfig) {
await setupAddons(options);
}
if (!isConvex && options.auth) {
if (options.auth && options.auth !== "none") {
await setupAuth(options);
}

View File

@@ -4,6 +4,7 @@ import fs from "fs-extra";
import type {
Addons,
API,
Auth,
Database,
DatabaseSetup,
Frontend,
@@ -98,7 +99,11 @@ This project uses Convex as a backend. You'll need to set up Convex before runni
${packageManagerRunCmd} dev:setup
\`\`\`
Follow the prompts to create a new Convex project and connect it to your application.`
Follow the prompts to create a new Convex project and connect it to your application.${
auth === "clerk"
? " See [Convex + Clerk guide](https://docs.convex.dev/auth/clerk) for auth setup."
: ""
}`
: generateDatabaseSetup(
database,
auth,
@@ -135,6 +140,7 @@ ${generateProjectStructure(
addons,
isConvex,
api,
auth,
)}
\`\`\`
@@ -258,6 +264,7 @@ function generateProjectStructure(
addons: Addons[],
isConvex: boolean,
api: API,
auth: Auth,
): string {
const structure: string[] = [`${projectName}/`, "├── apps/"];
@@ -317,6 +324,12 @@ function generateProjectStructure(
structure.push(
"│ └── backend/ # Convex backend functions and schema",
);
if (auth === "clerk") {
structure.push(
"│ ├── convex/ # Convex functions and schema",
"│ └── .env.local # Convex environment variables",
);
}
} else if (!isBackendNone) {
const backendName = backend[0].toUpperCase() + backend.slice(1);
const apiName = api !== "none" ? api.toUpperCase() : "";
@@ -329,7 +342,7 @@ function generateProjectStructure(
function generateFeaturesList(
database: Database,
auth: boolean,
auth: Auth,
addons: Addons[],
orm: ORM,
runtime: Runtime,
@@ -449,10 +462,9 @@ function generateFeaturesList(
);
}
if (auth && !isConvex) {
addonsList.push(
"- **Authentication** - Email & password authentication with Better Auth",
);
if (auth !== "none") {
const authLabel = auth === "clerk" ? "Clerk" : "Better-Auth";
addonsList.push(`- **Authentication** - ${authLabel}`);
}
for (const addon of addons) {
@@ -476,7 +488,7 @@ function generateFeaturesList(
function generateDatabaseSetup(
database: Database,
_auth: boolean,
_auth: Auth,
packageManagerRunCmd: string,
orm: ORM,
dbSetup: DatabaseSetup,
@@ -575,7 +587,7 @@ function generateScriptsList(
packageManagerRunCmd: string,
database: Database,
orm: ORM,
_auth: boolean,
_auth: Auth,
hasNative: boolean,
addons: Addons[],
backend: string,

View File

@@ -1,7 +1,7 @@
import path from "node:path";
import fs from "fs-extra";
import type { ProjectConfig } from "../../types";
import { generateAuthSecret } from "../addons/auth-setup";
import { generateAuthSecret } from "./auth-setup";
export interface EnvVariable {
key: string;
@@ -143,6 +143,42 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
condition: true,
},
];
if (backend === "convex" && auth === "clerk") {
if (hasNextJs) {
clientVars.push(
{
key: "NEXT_PUBLIC_CLERK_FRONTEND_API_URL",
value: "",
condition: true,
},
{
key: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
value: "",
condition: true,
},
{
key: "CLERK_SECRET_KEY",
value: "",
condition: true,
},
);
} else if (hasReactRouter || hasTanStackRouter || hasTanStackStart) {
clientVars.push({
key: "VITE_CLERK_PUBLISHABLE_KEY",
value: "",
condition: true,
});
if (hasTanStackStart) {
clientVars.push({
key: "CLERK_SECRET_KEY",
value: "",
condition: true,
});
}
}
}
await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
}
}
@@ -168,6 +204,14 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
condition: true,
},
];
if (backend === "convex" && auth === "clerk") {
nativeVars.push({
key: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY",
value: "",
condition: true,
});
}
await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
}
}

View File

@@ -69,6 +69,8 @@ export async function displayPostInstallInstructions(
const starlightInstructions = addons?.includes("starlight")
? getStarlightInstructions(runCmd)
: "";
const clerkInstructions =
isConvex && config.auth === "clerk" ? getClerkInstructions() : "";
const wranglerDeployInstructions = getWranglerDeployInstructions(
runCmd,
webDeploy,
@@ -119,6 +121,16 @@ export async function displayPostInstallInstructions(
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup\n${pc.dim(
" (this will guide you through Convex project setup)",
)}\n`;
if (config.auth === "clerk") {
output += `${pc.cyan(`${stepCounter++}.`)} ${pc.bold("Clerk Setup:")}\n${pc.dim(
" Follow the Convex + Clerk guide to configure authentication:",
)}\n${pc.cyan(" ")}${pc.underline("https://docs.convex.dev/auth/clerk")}\n\n`;
output += `${pc.cyan(`${stepCounter++}.`)} ${pc.bold("Required Environment Variables:")}\n`;
output += `${pc.dim(" •")} Set CLERK_JWT_ISSUER_DOMAIN in Convex Dashboard\n`;
output += `${pc.dim(" •")} Set CLERK_PUBLISHABLE_KEY in apps/*/.env\n`;
}
output += `${pc.cyan(
`${stepCounter++}.`,
)} Copy environment variables from\n${pc.white(
@@ -175,6 +187,7 @@ export async function displayPostInstallInstructions(
if (alchemyDeployInstructions)
output += `\n${alchemyDeployInstructions.trim()}\n`;
if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
@@ -403,6 +416,10 @@ function getWranglerDeployInstructions(
return instructions.length ? `\n${instructions.join("\n")}` : "";
}
function getClerkInstructions(): string {
return `${pc.bold("Clerk Authentication Setup:")}\n${pc.cyan("1.")} Sign up for Clerk at ${pc.underline("https://clerk.com/sign-up")}\n${pc.cyan("2.")} Create a new application in Clerk Dashboard\n${pc.cyan("3.")} Create a JWT template named ${pc.bold("'convex'")} (exact name required)\n${pc.cyan("4.")} Copy your Clerk Frontend API URL (Issuer URL)\n${pc.cyan("5.")} Set environment variables:\n${pc.dim(" •")} CLERK_JWT_ISSUER_DOMAIN in Convex Dashboard\n${pc.dim(" •")} CLERK_PUBLISHABLE_KEY in apps/*/.env\n${pc.cyan("6.")} Follow the complete guide: ${pc.underline("https://docs.convex.dev/auth/clerk")}\n${pc.yellow("NOTE:")} Use Convex's <Authenticated> components instead of Clerk's <SignedIn>`;
}
function getAlchemyDeployInstructions(
runCmd?: string,
webDeploy?: string,

View File

@@ -362,7 +362,7 @@ export async function setupAuthTemplate(
projectDir: string,
context: ProjectConfig,
) {
if (context.backend === "convex" || !context.auth) return;
if (!context.auth || context.auth === "none") return;
const serverAppDir = path.join(projectDir, "apps/server");
const webAppDir = path.join(projectDir, "apps/web");
@@ -382,8 +382,88 @@ export async function setupAuthTemplate(
const hasUnistyles = context.frontend.includes("native-unistyles");
const hasNative = hasNativeWind || hasUnistyles;
if (serverAppDirExists) {
const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base");
const authProvider = context.auth;
if (context.backend === "convex" && authProvider === "clerk") {
const convexBackendDestDir = path.join(projectDir, "packages/backend");
const convexClerkBackendSrc = path.join(
PKG_ROOT,
"templates/auth/clerk/convex/backend",
);
if (await fs.pathExists(convexClerkBackendSrc)) {
await fs.ensureDir(convexBackendDestDir);
await processAndCopyFiles(
"**/*",
convexClerkBackendSrc,
convexBackendDestDir,
context,
);
}
if (webAppDirExists) {
const reactFramework = context.frontend.find((f) =>
["tanstack-router", "react-router", "tanstack-start", "next"].includes(
f,
),
);
if (reactFramework) {
const convexClerkWebSrc = path.join(
PKG_ROOT,
`templates/auth/clerk/convex/web/react/${reactFramework}`,
);
if (await fs.pathExists(convexClerkWebSrc)) {
await processAndCopyFiles(
"**/*",
convexClerkWebSrc,
webAppDir,
context,
);
}
}
}
if (nativeAppDirExists) {
const convexClerkNativeBaseSrc = path.join(
PKG_ROOT,
"templates/auth/clerk/convex/native/base",
);
if (await fs.pathExists(convexClerkNativeBaseSrc)) {
await processAndCopyFiles(
"**/*",
convexClerkNativeBaseSrc,
nativeAppDir,
context,
);
}
const hasNativeWind = context.frontend.includes("native-nativewind");
const hasUnistyles = context.frontend.includes("native-unistyles");
let nativeFrameworkPath = "";
if (hasNativeWind) nativeFrameworkPath = "nativewind";
else if (hasUnistyles) nativeFrameworkPath = "unistyles";
if (nativeFrameworkPath) {
const convexClerkNativeFrameworkSrc = path.join(
PKG_ROOT,
`templates/auth/clerk/convex/native/${nativeFrameworkPath}`,
);
if (await fs.pathExists(convexClerkNativeFrameworkSrc)) {
await processAndCopyFiles(
"**/*",
convexClerkNativeFrameworkSrc,
nativeAppDir,
context,
);
}
}
}
return;
}
if (serverAppDirExists && context.backend !== "convex") {
const authServerBaseSrc = path.join(
PKG_ROOT,
`templates/auth/${authProvider}/server/base`,
);
if (await fs.pathExists(authServerBaseSrc)) {
await processAndCopyFiles(
"**/*",
@@ -391,13 +471,12 @@ export async function setupAuthTemplate(
serverAppDir,
context,
);
} else {
}
if (context.backend === "next") {
const authServerNextSrc = path.join(
PKG_ROOT,
"templates/auth/server/next",
`templates/auth/${authProvider}/server/next`,
);
if (await fs.pathExists(authServerNextSrc)) {
await processAndCopyFiles(
@@ -406,7 +485,6 @@ export async function setupAuthTemplate(
serverAppDir,
context,
);
} else {
}
}
@@ -417,22 +495,21 @@ export async function setupAuthTemplate(
if (orm === "drizzle") {
authDbSrc = path.join(
PKG_ROOT,
`templates/auth/server/db/drizzle/${db}`,
`templates/auth/${authProvider}/server/db/drizzle/${db}`,
);
} else if (orm === "prisma") {
authDbSrc = path.join(
PKG_ROOT,
`templates/auth/server/db/prisma/${db}`,
`templates/auth/${authProvider}/server/db/prisma/${db}`,
);
} else if (orm === "mongoose") {
authDbSrc = path.join(
PKG_ROOT,
`templates/auth/server/db/mongoose/${db}`,
`templates/auth/${authProvider}/server/db/mongoose/${db}`,
);
}
if (authDbSrc && (await fs.pathExists(authDbSrc))) {
await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
} else if (authDbSrc) {
}
}
}
@@ -444,11 +521,10 @@ export async function setupAuthTemplate(
if (hasReactWeb) {
const authWebBaseSrc = path.join(
PKG_ROOT,
"templates/auth/web/react/base",
`templates/auth/${authProvider}/web/react/base`,
);
if (await fs.pathExists(authWebBaseSrc)) {
await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
} else {
}
const reactFramework = context.frontend.find((f) =>
@@ -459,7 +535,7 @@ export async function setupAuthTemplate(
if (reactFramework) {
const authWebFrameworkSrc = path.join(
PKG_ROOT,
`templates/auth/web/react/${reactFramework}`,
`templates/auth/${authProvider}/web/react/${reactFramework}`,
);
if (await fs.pathExists(authWebFrameworkSrc)) {
await processAndCopyFiles(
@@ -468,26 +544,31 @@ export async function setupAuthTemplate(
webAppDir,
context,
);
} else {
}
}
} else if (hasNuxtWeb) {
const authWebNuxtSrc = path.join(PKG_ROOT, "templates/auth/web/nuxt");
const authWebNuxtSrc = path.join(
PKG_ROOT,
`templates/auth/${authProvider}/web/nuxt`,
);
if (await fs.pathExists(authWebNuxtSrc)) {
await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
} else {
}
} else if (hasSvelteWeb) {
const authWebSvelteSrc = path.join(PKG_ROOT, "templates/auth/web/svelte");
const authWebSvelteSrc = path.join(
PKG_ROOT,
`templates/auth/${authProvider}/web/svelte`,
);
if (await fs.pathExists(authWebSvelteSrc)) {
await processAndCopyFiles("**/*", authWebSvelteSrc, webAppDir, context);
} else {
}
} else if (hasSolidWeb) {
const authWebSolidSrc = path.join(PKG_ROOT, "templates/auth/web/solid");
const authWebSolidSrc = path.join(
PKG_ROOT,
`templates/auth/${authProvider}/web/solid`,
);
if (await fs.pathExists(authWebSolidSrc)) {
await processAndCopyFiles("**/*", authWebSolidSrc, webAppDir, context);
} else {
}
}
}
@@ -495,7 +576,7 @@ export async function setupAuthTemplate(
if (hasNative && nativeAppDirExists) {
const authNativeBaseSrc = path.join(
PKG_ROOT,
"templates/auth/native/native-base",
`templates/auth/${authProvider}/native/native-base`,
);
if (await fs.pathExists(authNativeBaseSrc)) {
await processAndCopyFiles(
@@ -516,7 +597,7 @@ export async function setupAuthTemplate(
if (nativeFrameworkAuthPath) {
const authNativeFrameworkSrc = path.join(
PKG_ROOT,
`templates/auth/native/${nativeFrameworkAuthPath}`,
`templates/auth/${authProvider}/native/${nativeFrameworkAuthPath}`,
);
if (await fs.pathExists(authNativeFrameworkSrc)) {
await processAndCopyFiles(

View File

@@ -12,6 +12,7 @@ import {
AddonsSchema,
type API,
APISchema,
AuthSchema,
type Backend,
BackendSchema,
type BetterTStackConfig,
@@ -78,7 +79,7 @@ export const router = t.router({
.describe("Show detailed result information"),
database: DatabaseSchema.optional(),
orm: ORMSchema.optional(),
auth: z.boolean().optional(),
auth: AuthSchema.optional(),
frontend: z.array(FrontendSchema).optional(),
addons: z.array(AddonsSchema).optional(),
examples: z.array(ExamplesSchema).optional(),
@@ -202,7 +203,7 @@ export function createBtsCli() {
* backend: "hono",
* database: "sqlite",
* orm: "drizzle",
* auth: true,
* auth: "better-auth",
* addons: ["biome", "turborepo"],
* packageManager: "bun",
* install: false,

View File

@@ -1,27 +1,56 @@
import { confirm, isCancel } from "@clack/prompts";
import { isCancel, select } from "@clack/prompts";
import { DEFAULT_CONFIG } from "../constants";
import type { Backend } from "../types";
import type { Auth, Backend } from "../types";
import { exitCancelled } from "../utils/errors";
export async function getAuthChoice(
auth: boolean | undefined,
auth: Auth | undefined,
hasDatabase: boolean,
backend?: Backend,
frontend?: string[],
) {
if (auth !== undefined) return auth;
if (backend === "convex") {
return false;
const unsupportedFrontends = frontend?.filter((f) =>
["nuxt", "svelte", "solid"].includes(f),
);
if (unsupportedFrontends && unsupportedFrontends.length > 0) {
return "none";
}
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",
});
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response as Auth;
}
if (!hasDatabase) return false;
if (!hasDatabase) return "none";
if (auth !== undefined) return auth;
const response = await confirm({
message: "Add authentication with Better-Auth?",
const response = await select({
message: "Select authentication provider",
options: [
{
value: "better-auth",
label: "Better-Auth",
hint: "comprehensive auth framework for TypeScript",
},
{ value: "none", label: "None" },
],
initialValue: DEFAULT_CONFIG.auth,
});
if (isCancel(response)) return exitCancelled("Operation cancelled");
return response;
return response as Auth;
}

View File

@@ -2,6 +2,7 @@ import { group } from "@clack/prompts";
import type {
Addons,
API,
Auth,
Backend,
Database,
DatabaseSetup,
@@ -38,7 +39,7 @@ type PromptGroupResults = {
database: Database;
orm: ORM;
api: API;
auth: boolean;
auth: Auth;
addons: Addons[];
examples: Examples[];
dbSetup: DatabaseSetup;
@@ -57,7 +58,8 @@ export async function gatherConfig(
): Promise<ProjectConfig> {
const result = await group<PromptGroupResults>(
{
frontend: () => getFrontendChoice(flags.frontend, flags.backend),
frontend: () =>
getFrontendChoice(flags.frontend, flags.backend, flags.auth),
backend: ({ results }) =>
getBackendFrameworkChoice(flags.backend, results.frontend),
runtime: ({ results }) =>
@@ -75,7 +77,12 @@ export async function gatherConfig(
api: ({ results }) =>
getApiChoice(flags.api, results.frontend, results.backend),
auth: ({ results }) =>
getAuthChoice(flags.auth, results.database !== "none", results.backend),
getAuthChoice(
flags.auth as import("../types").Auth | undefined,
results.database !== "none",
results.backend,
results.frontend,
),
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
examples: ({ results }) =>
getExamplesChoice(
@@ -121,7 +128,6 @@ export async function gatherConfig(
result.database = "none";
result.orm = "none";
result.api = "none";
result.auth = false;
result.dbSetup = "none";
result.examples = ["todo"];
}
@@ -131,7 +137,7 @@ export async function gatherConfig(
result.database = "none";
result.orm = "none";
result.api = "none";
result.auth = false;
result.auth = "none";
result.dbSetup = "none";
result.examples = [];
}

View File

@@ -7,6 +7,7 @@ import { exitCancelled } from "../utils/errors";
export async function getFrontendChoice(
frontendOptions?: Frontend[],
backend?: Backend,
auth?: string,
): Promise<Frontend[]> {
if (frontendOptions !== undefined) return frontendOptions;
@@ -72,7 +73,7 @@ export async function getFrontendChoice(
];
const webOptions = allWebOptions.filter((option) =>
isFrontendAllowedWithBackend(option.value, backend),
isFrontendAllowedWithBackend(option.value, backend, auth),
);
const webFramework = await select<Frontend>({

View File

@@ -80,6 +80,11 @@ export type DatabaseSetup = z.infer<typeof DatabaseSetupSchema>;
export const APISchema = z.enum(["trpc", "orpc", "none"]).describe("API type");
export type API = z.infer<typeof APISchema>;
export const AuthSchema = z
.enum(["better-auth", "clerk", "none"])
.describe("Authentication provider");
export type Auth = z.infer<typeof AuthSchema>;
export const ProjectNameSchema = z
.string()
.min(1, "Project name cannot be empty")
@@ -125,7 +130,7 @@ export type CreateInput = {
verbose?: boolean;
database?: Database;
orm?: ORM;
auth?: boolean;
auth?: Auth;
frontend?: Frontend[];
addons?: Addons[];
examples?: Examples[];
@@ -167,7 +172,7 @@ export interface ProjectConfig {
frontend: Frontend[];
addons: Addons[];
examples: Examples[];
auth: boolean;
auth: Auth;
git: boolean;
packageManager: PackageManager;
install: boolean;
@@ -187,7 +192,7 @@ export interface BetterTStackConfig {
frontend: Frontend[];
addons: Addons[];
examples: Examples[];
auth: boolean;
auth: Auth;
packageManager: PackageManager;
dbSetup: DatabaseSetup;
api: API;

View File

@@ -135,7 +135,6 @@ export function validateWorkersCompatibility(
export function coerceBackendPresets(config: Partial<ProjectConfig>) {
if (config.backend === "convex") {
config.auth = false;
config.database = "none";
config.orm = "none";
config.api = "none";
@@ -144,7 +143,7 @@ export function coerceBackendPresets(config: Partial<ProjectConfig>) {
config.examples = ["todo"] as ProjectConfig["examples"];
}
if (config.backend === "none") {
config.auth = false;
config.auth = "none" as ProjectConfig["auth"];
config.database = "none";
config.orm = "none";
config.api = "none";
@@ -161,7 +160,13 @@ export function incompatibleFlagsForBackend(
): string[] {
const list: string[] = [];
if (backend === "convex") {
if (providedFlags.has("auth") && options.auth === true) list.push("--auth");
if (
providedFlags.has("auth") &&
options.auth &&
options.auth !== "none" &&
options.auth !== "clerk"
)
list.push(`--auth ${options.auth}`);
if (providedFlags.has("database") && options.database !== "none")
list.push(`--database ${options.database}`);
if (providedFlags.has("orm") && options.orm !== "none")
@@ -174,7 +179,8 @@ export function incompatibleFlagsForBackend(
list.push(`--db-setup ${options.dbSetup}`);
}
if (backend === "none") {
if (providedFlags.has("auth") && options.auth === true) list.push("--auth");
if (providedFlags.has("auth") && options.auth && options.auth !== "none")
list.push(`--auth ${options.auth}`);
if (providedFlags.has("database") && options.database !== "none")
list.push(`--database ${options.database}`);
if (providedFlags.has("orm") && options.orm !== "none")
@@ -210,8 +216,15 @@ export function validateApiFrontendCompatibility(
export function isFrontendAllowedWithBackend(
frontend: Frontend,
backend?: ProjectConfig["backend"],
auth?: string,
) {
if (backend === "convex" && frontend === "solid") return false;
if (auth === "clerk" && backend === "convex") {
const incompatibleFrontends = ["nuxt", "svelte", "solid"];
if (incompatibleFrontends.includes(frontend)) return false;
}
return true;
}

View File

@@ -1,6 +1,7 @@
import path from "node:path";
import type {
API,
Auth,
Backend,
CLIInput,
Database,
@@ -57,7 +58,7 @@ export function processFlags(
}
if (options.auth !== undefined) {
config.auth = options.auth;
config.auth = options.auth as Auth;
}
if (options.git !== undefined) {

View File

@@ -66,15 +66,21 @@ export function validateDatabaseOrmAuth(
);
}
if (has("auth") && has("database") && cfg.auth && db === "none") {
if (
has("auth") &&
has("database") &&
cfg.auth !== "none" &&
db === "none" &&
cfg.backend !== "convex"
) {
exitWithError(
"Authentication requires a database. Please choose a database or set '--no-auth'.",
"Authentication requires a database. Please choose a database or set '--auth none'.",
);
}
if (cfg.auth && db === "none") {
if (cfg.auth !== "none" && db === "none" && cfg.backend !== "convex") {
exitWithError(
"Authentication requires a database. Please choose a database or set '--no-auth'.",
"Authentication requires a database. Please choose a database or set '--auth none'.",
);
}
@@ -178,6 +184,35 @@ export function validateBackendConstraints(
): void {
const { backend } = config;
if (config.auth === "clerk" && backend !== "convex") {
exitWithError(
"Clerk authentication is only supported with the Convex backend. Please use '--backend convex' or choose a different auth provider.",
);
}
if (backend === "convex" && config.auth === "clerk" && config.frontend) {
const incompatibleFrontends = config.frontend.filter((f) =>
["nuxt", "svelte", "solid"].includes(f),
);
if (incompatibleFrontends.length > 0) {
exitWithError(
`Clerk authentication is not compatible with the following frontends: ${incompatibleFrontends.join(
", ",
)}. Please choose a different frontend or auth provider.`,
);
}
}
if (
backend === "convex" &&
config.auth === "better-auth" &&
providedFlags.has("auth")
) {
exitWithError(
"Better-Auth is not compatible with the Convex backend. Please use '--auth clerk' or '--auth none'.",
);
}
if (
providedFlags.has("backend") &&
backend &&

View File

@@ -40,13 +40,7 @@ export function displayConfig(config: Partial<ProjectConfig>) {
}
if (config.auth !== undefined) {
const authText =
typeof config.auth === "boolean"
? config.auth
? "Yes"
: "No"
: String(config.auth);
configDisplay.push(`${pc.blue("Authentication:")} ${authText}`);
configDisplay.push(`${pc.blue("Auth:")} ${String(config.auth)}`);
}
if (config.addons !== undefined) {

View File

@@ -14,7 +14,7 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
flags.push(`--database ${config.database}`);
flags.push(`--orm ${config.orm}`);
flags.push(`--api ${config.api}`);
flags.push(config.auth ? "--auth" : "--no-auth");
flags.push(`--auth ${config.auth}`);
if (config.addons && config.addons.length > 0) {
flags.push(`--addons ${config.addons.join(" ")}`);