Simplify auth setup, centralize environment variable management and fix

create readme
This commit is contained in:
Aman Varshney
2025-03-19 13:33:51 +05:30
parent d0540e41cd
commit 03d9559e55
11 changed files with 170 additions and 222 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
Simplify auth setup, centralize environment variable management and fix readme

View File

@@ -1,116 +1,9 @@
import path from "node:path";
import { log } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectConfig } from "../types";
import { addPackageDependency } from "../utils/add-package-deps";
export async function setupAuth(
projectDir: string,
enableAuth: boolean,
options: ProjectConfig,
): Promise<void> {
const serverDir = path.join(projectDir, "packages/server");
const clientDir = path.join(projectDir, "packages/client");
try {
if (!enableAuth) {
return;
}
addPackageDependency({
dependencies: ["better-auth"],
devDependencies: false,
projectDir: serverDir,
});
const envPath = path.join(serverDir, ".env");
// Create or update the .env file directly with required variables
let envContent = "";
if (await fs.pathExists(envPath)) {
envContent = await fs.readFile(envPath, "utf8");
}
// Only add variables that don't already exist
if (!envContent.includes("BETTER_AUTH_SECRET")) {
envContent += `\nBETTER_AUTH_SECRET=${generateAuthSecret()}`;
}
if (!envContent.includes("BETTER_AUTH_URL")) {
envContent += "\nBETTER_AUTH_URL=http://localhost:3000";
}
if (!envContent.includes("CORS_ORIGIN")) {
envContent += "\nCORS_ORIGIN=http://localhost:3001";
}
if (
options.database === "sqlite" &&
!envContent.includes("TURSO_CONNECTION_URL")
) {
envContent += "\nTURSO_CONNECTION_URL=http://127.0.0.1:8080";
}
if (options.orm === "prisma" && !envContent.includes("DATABASE_URL")) {
if (options.database === "sqlite") {
envContent += '\nDATABASE_URL="file:./dev.db"';
} else if (options.database === "postgres") {
envContent +=
'\nDATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"';
}
}
// Write the updated content
await fs.writeFile(envPath, envContent.trim());
// Create client .env file if it doesn't exist
const clientEnvPath = path.join(clientDir, ".env");
if (!(await fs.pathExists(clientEnvPath))) {
const clientEnvContent = "VITE_SERVER_URL=http://localhost:3000\n";
await fs.writeFile(clientEnvPath, clientEnvContent);
}
if (options.orm === "prisma") {
const packageJsonPath = path.join(projectDir, "package.json");
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJson(packageJsonPath);
packageJson.scripts["prisma:generate"] =
"cd packages/server && npx prisma generate";
packageJson.scripts["prisma:push"] =
"cd packages/server && npx prisma db push";
packageJson.scripts["prisma:studio"] =
"cd packages/server && npx prisma studio";
packageJson.scripts["db:setup"] =
"npm run auth:generate && npm run prisma:generate && npm run prisma:push";
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
} else if (options.orm === "drizzle") {
const packageJsonPath = path.join(projectDir, "package.json");
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJson(packageJsonPath);
packageJson.scripts["db:push"] =
"cd packages/server && npx @better-auth/cli migrate";
packageJson.scripts["db:setup"] =
"npm run auth:generate && npm run db:push";
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
}
} catch (error) {
log.error(pc.red("Failed to configure authentication"));
if (error instanceof Error) {
log.error(pc.red(error.message));
}
throw error;
}
}
function generateAuthSecret(length = 32): string {
export function generateAuthSecret(length = 32): string {
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
@@ -120,3 +13,33 @@ function generateAuthSecret(length = 32): string {
}
return result;
}
export async function setupAuth(
projectDir: string,
enableAuth: boolean,
): Promise<void> {
const serverDir = path.join(projectDir, "packages/server");
const clientDir = path.join(projectDir, "packages/client");
try {
if (!enableAuth) {
return;
}
addPackageDependency({
dependencies: ["better-auth"],
devDependencies: false,
projectDir: serverDir,
});
addPackageDependency({
dependencies: ["better-auth"],
devDependencies: false,
projectDir: clientDir,
});
} catch (error) {
log.error(pc.red("Failed to configure authentication"));
if (error instanceof Error) {
log.error(pc.red(error.message));
}
throw error;
}
}

View File

@@ -9,6 +9,7 @@ import { setupAddons } from "./addons-setup";
import { setupAuth } from "./auth-setup";
import { createReadme } from "./create-readme";
import { setupDatabase } from "./db-setup";
import { setupEnvironmentVariables } from "./env-setup";
import { displayPostInstallInstructions } from "./post-installation";
export async function createProject(options: ProjectConfig): Promise<string> {
@@ -49,7 +50,9 @@ export async function createProject(options: ProjectConfig): Promise<string> {
options.turso ?? options.database === "sqlite",
);
await setupAuth(projectDir, options.auth, options);
await setupAuth(projectDir, options.auth);
await setupEnvironmentVariables(projectDir, options);
if (options.git) {
await $({ cwd: projectDir })`git init`;
@@ -59,51 +62,63 @@ export async function createProject(options: ProjectConfig): Promise<string> {
await setupAddons(projectDir, options.addons);
}
const packageJsonPath = path.join(projectDir, "package.json");
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJson(packageJsonPath);
const rootPackageJsonPath = path.join(projectDir, "package.json");
if (await fs.pathExists(rootPackageJsonPath)) {
const packageJson = await fs.readJson(rootPackageJsonPath);
packageJson.name = options.projectName;
if (options.packageManager !== "bun") {
packageJson.packageManager =
options.packageManager === "npm"
? "npm@10.2.4"
? "npm@10.9.2"
: options.packageManager === "pnpm"
? "pnpm@8.15.4"
? "pnpm@10.6.4"
: options.packageManager === "yarn"
? "yarn@4.1.0"
: "bun@1.2.4";
: "bun@1.2.5";
}
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
}
const serverPackageJsonPath = path.join(
projectDir,
"packages/server/package.json",
);
if (await fs.pathExists(serverPackageJsonPath)) {
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
if (options.database !== "none") {
if (options.database === "sqlite") {
packageJson.scripts["db:local"] =
"cd packages/server && turso dev --db-file local.db";
if (options.database === "sqlite" && options.turso) {
serverPackageJson.scripts["db:local"] =
"turso dev --db-file local.db";
}
if (options.auth) {
packageJson.scripts["auth:generate"] =
"cd packages/server && npx @better-auth/cli generate --output ./src/db/auth-schema.ts";
serverPackageJson.scripts["auth:generate"] =
"npx @better-auth/cli generate --output ./src/db/auth-schema.ts";
if (options.orm === "prisma") {
packageJson.scripts["prisma:generate"] =
"cd packages/server && npx prisma generate";
packageJson.scripts["prisma:push"] =
"cd packages/server && npx prisma db push";
packageJson.scripts["prisma:studio"] =
"cd packages/server && npx prisma studio";
packageJson.scripts["db:setup"] =
"npm run auth:generate && npm run prisma:generate && npm run prisma:push";
serverPackageJson.scripts["db:push"] = "npx prisma db push";
serverPackageJson.scripts["db:studio"] = "npx prisma studio";
} else if (options.orm === "drizzle") {
packageJson.scripts["drizzle:migrate"] =
"cd packages/server && npx @better-auth/cli migrate";
packageJson.scripts["db:setup"] =
"npm run auth:generate && npm run drizzle:migrate";
serverPackageJson.scripts["db:push"] = "npx drizzle-kit push";
serverPackageJson.scripts["db:studio"] = "npx drizzle-kit studio";
}
} else {
if (options.orm === "prisma") {
serverPackageJson.scripts["db:push"] = "npx prisma db push";
serverPackageJson.scripts["db:studio"] = "npx prisma studio";
} else if (options.orm === "drizzle") {
serverPackageJson.scripts["db:push"] = "npx drizzle-kit push";
serverPackageJson.scripts["db:studio"] = "npx drizzle-kit studio";
}
}
}
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
await fs.writeJson(serverPackageJsonPath, serverPackageJson, {
spaces: 2,
});
}
await createReadme(projectDir, options);

View File

@@ -126,10 +126,10 @@ function generateDatabaseSetup(
1. Start the local SQLite database:
\`\`\`bash
${packageManagerRunCmd} db:local
cd packages/server && ${packageManagerRunCmd} db:local
\`\`\`
2. Update your \`.env\` file with the appropriate connection details if needed.
2. Update your \`.env\` file in the \`packages/server\` directory with the appropriate connection details if needed.
`;
} else if (database === "postgres") {
setup += `This project uses PostgreSQL${orm === "drizzle" ? " with Drizzle ORM" : " with Prisma"}.
@@ -143,24 +143,25 @@ ${packageManagerRunCmd} db:local
setup += `
3. Generate the authentication schema:
\`\`\`bash
${packageManagerRunCmd} auth:generate
cd packages/server && ${packageManagerRunCmd} auth:generate
\`\`\`
4. ${
orm === "prisma"
? `Generate the Prisma client and push the schema:
\`\`\`bash
${packageManagerRunCmd} prisma:generate
${packageManagerRunCmd} db:push
\`\`\``
: `Apply the Drizzle migrations:
\`\`\`bash
${packageManagerRunCmd} db:push
\`\`\``
}
`;
}
setup += `
${auth ? "4" : "3"}. ${
orm === "prisma"
? `Generate the Prisma client and push the schema:
\`\`\`bash
${packageManagerRunCmd} db:push
\`\`\``
: `Apply the schema to your database:
\`\`\`bash
${packageManagerRunCmd} db:push
\`\`\``
}
`;
return setup;
}
@@ -173,32 +174,21 @@ function generateScriptsList(
let scripts = `- \`${packageManagerRunCmd} dev\`: Start both client and server in development mode
- \`${packageManagerRunCmd} build\`: Build both client and server
- \`${packageManagerRunCmd} dev:client\`: Start only the client
- \`${packageManagerRunCmd} dev:server\`: Start only the server`;
- \`${packageManagerRunCmd} dev:server\`: Start only the server
- \`${packageManagerRunCmd} check-types\`: Check TypeScript types across all packages`;
if (database !== "none") {
if (database === "sqlite") {
scripts += `\n- \`${packageManagerRunCmd} db:local\`: Start the local SQLite database`;
}
scripts += `
- \`${packageManagerRunCmd} db:push\`: Push schema changes to database
- \`${packageManagerRunCmd} db:studio\`: Open database studio UI`;
if (orm === "prisma") {
scripts += `
- \`${packageManagerRunCmd} prisma:generate\`: Generate Prisma client
- \`${packageManagerRunCmd} db:push\`: Push schema changes to database
- \`${packageManagerRunCmd} prisma:studio\`: Open Prisma Studio`;
} else if (orm === "drizzle") {
scripts += `
- \`${packageManagerRunCmd} db:generate\`: Generate database schema
- \`${packageManagerRunCmd} db:push\`: Push schema changes to database
- \`${packageManagerRunCmd} db:studio\`: Open Drizzle Studio`;
if (database === "sqlite" && orm === "drizzle") {
scripts += `\n- \`cd packages/server && ${packageManagerRunCmd} db:local\`: Start the local SQLite database`;
}
}
if (auth) {
scripts += `\n- \`${packageManagerRunCmd} auth:generate\`: Generate authentication schema`;
}
if (auth && database !== "none") {
scripts += `\n- \`${packageManagerRunCmd} db:setup\`: Complete database setup for auth`;
scripts += `\n- \`cd packages/server && ${packageManagerRunCmd} auth:generate\`: Generate authentication schema`;
}
return scripts;

View File

@@ -74,43 +74,6 @@ export async function setupDatabase(
});
}
}
const packageJsonPath = path.join(serverDir, "package.json");
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJSON(packageJsonPath);
if (orm === "drizzle") {
packageJson.scripts = {
...packageJson.scripts,
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
};
} else if (orm === "prisma") {
packageJson.scripts = {
...packageJson.scripts,
"prisma:generate": "prisma generate",
"prisma:push": "prisma db push",
"prisma:studio": "prisma studio",
};
}
await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
}
if (orm === "prisma") {
const envPath = path.join(serverDir, ".env");
if (await fs.pathExists(envPath)) {
const envContent = await fs.readFile(envPath, "utf8");
if (!envContent.includes("DATABASE_URL")) {
const databaseUrlLine =
databaseType === "sqlite"
? `\nDATABASE_URL="file:./dev.db"`
: `\nDATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`;
await fs.appendFile(envPath, databaseUrlLine);
}
}
}
} catch (error) {
s.stop(pc.red("Failed to set up database"));
if (error instanceof Error) {

View File

@@ -0,0 +1,65 @@
import path from "node:path";
import fs from "fs-extra";
import type { ProjectConfig } from "../types";
import { generateAuthSecret } from "./auth-setup";
export async function setupEnvironmentVariables(
projectDir: string,
options: ProjectConfig,
): Promise<void> {
const serverDir = path.join(projectDir, "packages/server");
const clientDir = path.join(projectDir, "packages/client");
// Set up server env variables
const envPath = path.join(serverDir, ".env");
let envContent = "";
if (await fs.pathExists(envPath)) {
envContent = await fs.readFile(envPath, "utf8");
}
// Auth environment variables
if (options.auth) {
if (!envContent.includes("BETTER_AUTH_SECRET")) {
envContent += `\nBETTER_AUTH_SECRET=${generateAuthSecret()}`;
}
if (!envContent.includes("BETTER_AUTH_URL")) {
envContent += "\nBETTER_AUTH_URL=http://localhost:3000";
}
if (!envContent.includes("CORS_ORIGIN")) {
envContent += "\nCORS_ORIGIN=http://localhost:3001";
}
}
// Database environment variables
if (options.database !== "none") {
if (options.orm === "prisma" && !envContent.includes("DATABASE_URL")) {
const databaseUrlLine =
options.database === "sqlite"
? `\nDATABASE_URL="file:./dev.db"`
: `\nDATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`;
envContent += databaseUrlLine;
}
if (
options.database === "sqlite" &&
options.turso &&
!envContent.includes("TURSO_CONNECTION_URL")
) {
envContent += "\nTURSO_CONNECTION_URL=http://127.0.0.1:8080";
}
}
await fs.writeFile(envPath, envContent.trim());
// Set up client env variables
if (options.auth) {
const clientEnvPath = path.join(clientDir, ".env");
if (!(await fs.pathExists(clientEnvPath))) {
const clientEnvContent = "VITE_SERVER_URL=http://localhost:3000\n";
await fs.writeFile(clientEnvPath, clientEnvContent);
}
}
}

View File

@@ -10,8 +10,8 @@
"check-types": "turbo check-types",
"dev:client": "turbo -F @better-t/client dev",
"dev:server": "turbo -F @better-t/server dev",
"db:local": "turbo -F @better-t/server db:local",
"db:push": "turbo -F @better-t/server db:push"
"db:push": "turbo -F @better-t/server db:push",
"db:studio": "turbo -F @better-t/server db:studio"
},
"packageManager": "bun@1.2.4",
"devDependencies": {

View File

@@ -6,12 +6,7 @@
"dev": "tsx watch src/index.ts",
"build": "tsc",
"dev:bun": "bun run --hot src/index.ts",
"db:local": "turso dev --db-file local.db",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"check-types": "tsc --noEmit",
"wrangler:dev": "wrangler dev",
"wrangler:deploy": "wrangler deploy --minify",
"compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server"
},
"dependencies": {

View File

@@ -17,10 +17,10 @@
"cache": false,
"persistent": true
},
"db:local": {
"db:push": {
"cache": false
},
"db:push": {
"db:studio": {
"cache": false
}
}

View File

@@ -1,4 +0,0 @@
BETTER_AUTH_SECRET=jdUstNuiIZLVh897KOMMS8EmTP0QkD32
BETTER_AUTH_URL=http://localhost:3000
TURSO_CONNECTION_URL=http://127.0.0.1:8080
CORS_ORIGIN=http://localhost:3001

View File

@@ -1,4 +0,0 @@
BETTER_AUTH_SECRET=jdUstNuiIZLVh897KOMMS8EmTP0QkD32
BETTER_AUTH_URL=http://localhost:3000
TURSO_CONNECTION_URL=http://127.0.0.1:8080
CORS_ORIGIN=http://localhost:3001