diff --git a/.changeset/eager-zebras-jog.md b/.changeset/eager-zebras-jog.md new file mode 100644 index 0000000..6938b6b --- /dev/null +++ b/.changeset/eager-zebras-jog.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +add d1 database setup diff --git a/apps/cli/src/helpers/database-providers/d1-setup.ts b/apps/cli/src/helpers/database-providers/d1-setup.ts new file mode 100644 index 0000000..6b9331f --- /dev/null +++ b/apps/cli/src/helpers/database-providers/d1-setup.ts @@ -0,0 +1,34 @@ +import path from "node:path"; +import type { ProjectConfig } from "../../types"; +import { + addEnvVariablesToFile, + type EnvVariable, +} from "../project-generation/env-setup"; + +export async function setupCloudflareD1(config: ProjectConfig): Promise { + const { projectDir } = config; + + const envPath = path.join(projectDir, "apps/server", ".env"); + + const variables: EnvVariable[] = [ + { + key: "CLOUDFLARE_ACCOUNT_ID", + value: "", + condition: true, + }, + { + key: "CLOUDFLARE_DATABASE_ID", + value: "", + condition: true, + }, + { + key: "CLOUDFLARE_D1_TOKEN", + value: "", + condition: true, + }, + ]; + + try { + await addEnvVariablesToFile(envPath, variables); + } catch (_err) {} +} diff --git a/apps/cli/src/helpers/project-generation/create-project.ts b/apps/cli/src/helpers/project-generation/create-project.ts index a472924..4f09a90 100644 --- a/apps/cli/src/helpers/project-generation/create-project.ts +++ b/apps/cli/src/helpers/project-generation/create-project.ts @@ -8,7 +8,10 @@ import { setupAuth } from "../setup/auth-setup"; import { setupBackendDependencies } from "../setup/backend-setup"; import { setupDatabase } from "../setup/db-setup"; import { setupExamples } from "../setup/examples-setup"; -import { setupRuntime } from "../setup/runtime-setup"; +import { + generateCloudflareWorkerTypes, + setupRuntime, +} from "../setup/runtime-setup"; import { createReadme } from "./create-readme"; import { setupEnvironmentVariables } from "./env-setup"; import { installDependencies } from "./install-dependencies"; @@ -64,6 +67,7 @@ export async function createProject(options: ProjectConfig) { } await handleExtras(projectDir, options); + await setupEnvironmentVariables(options); await updatePackageConfigurations(projectDir, options); await createReadme(projectDir, options); @@ -76,6 +80,7 @@ export async function createProject(options: ProjectConfig) { projectDir, packageManager: options.packageManager, }); + await generateCloudflareWorkerTypes(options); } displayPostInstallInstructions({ diff --git a/apps/cli/src/helpers/project-generation/env-setup.ts b/apps/cli/src/helpers/project-generation/env-setup.ts index ee14151..313ac2e 100644 --- a/apps/cli/src/helpers/project-generation/env-setup.ts +++ b/apps/cli/src/helpers/project-generation/env-setup.ts @@ -186,7 +186,8 @@ export async function setupEnvironmentVariables( dbSetup === "prisma-postgres" || dbSetup === "mongodb-atlas" || dbSetup === "neon" || - dbSetup === "supabase"; + dbSetup === "supabase" || + dbSetup === "d1"; if (database !== "none" && !specializedSetup) { switch (database) { diff --git a/apps/cli/src/helpers/project-generation/post-installation.ts b/apps/cli/src/helpers/project-generation/post-installation.ts index 638dad3..730f694 100644 --- a/apps/cli/src/helpers/project-generation/post-installation.ts +++ b/apps/cli/src/helpers/project-generation/post-installation.ts @@ -1,6 +1,12 @@ import { consola } from "consola"; import pc from "picocolors"; -import type { Database, ORM, ProjectConfig, Runtime } from "../../types"; +import type { + Database, + DatabaseSetup, + ORM, + ProjectConfig, + Runtime, +} from "../../types"; import { getPackageExecutionCommand } from "../../utils/get-package-execution-command"; export function displayPostInstallInstructions( @@ -16,6 +22,7 @@ export function displayPostInstallInstructions( runtime, frontend, backend, + dbSetup, } = config; const isConvex = backend === "convex"; @@ -26,7 +33,7 @@ export function displayPostInstallInstructions( const databaseInstructions = !isConvex && database !== "none" - ? getDatabaseInstructions(database, orm, runCmd, runtime) + ? getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) : ""; const tauriInstructions = addons?.includes("tauri") @@ -96,6 +103,11 @@ export function displayPostInstallInstructions( } if (runtime === "workers") { + if (dbSetup === "d1") { + output += `${pc.yellow( + "IMPORTANT:", + )} Complete D1 database setup first (see Database commands below)\n`; + } output += `${pc.cyan(`${stepCounter++}.`)} bun dev\n`; output += `${pc.cyan( `${stepCounter++}.`, @@ -178,9 +190,46 @@ function getDatabaseInstructions( orm?: ORM, runCmd?: string, runtime?: Runtime, + dbSetup?: DatabaseSetup, ): string { const instructions = []; + if (runtime === "workers" && dbSetup === "d1") { + const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm"; + + instructions.push( + `${pc.cyan("1.")} Login to Cloudflare: ${pc.white( + `${packageManager} wrangler login`, + )}`, + ); + instructions.push( + `${pc.cyan("2.")} Create D1 database: ${pc.white( + `${packageManager} wrangler d1 create your-database-name`, + )}`, + ); + instructions.push( + `${pc.cyan( + "3.", + )} Update apps/server/wrangler.jsonc with database_id and database_name`, + ); + instructions.push( + `${pc.cyan("4.")} Generate migrations: ${pc.white( + "cd apps/server && bun db:generate", + )}`, + ); + instructions.push( + `${pc.cyan("5.")} Apply migrations locally: ${pc.white( + `${packageManager} wrangler d1 migrations apply YOUR_DB_NAME --local`, + )}`, + ); + instructions.push( + `${pc.cyan("6.")} Apply migrations to production: ${pc.white( + `${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`, + )}`, + ); + instructions.push(""); + } + if (orm === "prisma") { if (database === "sqlite") { instructions.push( @@ -204,7 +253,7 @@ function getDatabaseInstructions( } else if (orm === "drizzle") { instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`); instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`); - if (database === "sqlite") { + if (database === "sqlite" && dbSetup !== "d1") { instructions.push( `${pc.cyan( "•", diff --git a/apps/cli/src/helpers/setup/db-setup.ts b/apps/cli/src/helpers/setup/db-setup.ts index cccd530..fc10608 100644 --- a/apps/cli/src/helpers/setup/db-setup.ts +++ b/apps/cli/src/helpers/setup/db-setup.ts @@ -5,6 +5,7 @@ import fs from "fs-extra"; import pc from "picocolors"; import type { ProjectConfig } from "../../types"; import { addPackageDependency } from "../../utils/add-package-deps"; +import { setupCloudflareD1 } from "../database-providers/d1-setup"; import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup"; import { setupNeonPostgres } from "../database-providers/neon-setup"; import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup"; @@ -69,6 +70,8 @@ export async function setupDatabase(config: ProjectConfig): Promise { if (database === "sqlite" && dbSetup === "turso") { await setupTurso(config); + } else if (database === "sqlite" && dbSetup === "d1") { + await setupCloudflareD1(config); } else if (database === "postgres") { if (orm === "prisma" && dbSetup === "prisma-postgres") { await setupPrismaPostgres(config); diff --git a/apps/cli/src/helpers/setup/runtime-setup.ts b/apps/cli/src/helpers/setup/runtime-setup.ts index 7d2238f..c8f2ab0 100644 --- a/apps/cli/src/helpers/setup/runtime-setup.ts +++ b/apps/cli/src/helpers/setup/runtime-setup.ts @@ -1,5 +1,8 @@ import path from "node:path"; +import { spinner } from "@clack/prompts"; +import { execa } from "execa"; import fs from "fs-extra"; +import pc from "picocolors"; import type { Backend, ProjectConfig } from "../../types"; import { addPackageDependency } from "../../utils/add-package-deps"; @@ -25,6 +28,43 @@ export async function setupRuntime(config: ProjectConfig): Promise { } } +export async function generateCloudflareWorkerTypes( + config: ProjectConfig, +): Promise { + if (config.runtime !== "workers") { + return; + } + + const serverDir = path.join(config.projectDir, "apps/server"); + + if (!(await fs.pathExists(serverDir))) { + return; + } + + const s = spinner(); + + try { + s.start("Generating Cloudflare Workers types..."); + + const runCmd = + config.packageManager === "npm" ? "npm" : config.packageManager; + await execa(runCmd, ["run", "cf-typegen"], { + cwd: serverDir, + }); + + s.stop("Cloudflare Workers types generated successfully!"); + } catch { + s.stop(pc.yellow("Failed to generate Cloudflare Workers types")); + const managerCmd = + config.packageManager === "npm" + ? "npm run" + : `${config.packageManager} run`; + console.warn( + `Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`, + ); + } +} + async function setupBunRuntime( serverDir: string, _backend: Backend, diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index 26245b1..5d5978d 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -85,6 +85,7 @@ export async function gatherConfig( flags.dbSetup, results.orm, results.backend, + results.runtime, ), git: () => getGitChoice(flags.git), packageManager: () => getPackageManagerChoice(flags.packageManager), diff --git a/apps/cli/src/prompts/database-setup.ts b/apps/cli/src/prompts/database-setup.ts index 62de71a..3bac4f8 100644 --- a/apps/cli/src/prompts/database-setup.ts +++ b/apps/cli/src/prompts/database-setup.ts @@ -1,12 +1,13 @@ import { cancel, isCancel, select } from "@clack/prompts"; import pc from "picocolors"; -import type { Backend, DatabaseSetup, ORM } from "../types"; +import type { Backend, DatabaseSetup, ORM, Runtime } from "../types"; export async function getDBSetupChoice( databaseType: string, dbSetup: DatabaseSetup | undefined, orm?: ORM, backend?: Backend, + runtime?: Runtime, ): Promise { if (backend === "convex") { return "none"; @@ -32,6 +33,15 @@ export async function getDBSetupChoice( label: "Turso", hint: "SQLite for Production. Powered by libSQL", }, + ...(runtime === "workers" + ? [ + { + value: "d1" as const, + label: "Cloudflare D1", + hint: "Cloudflare's managed, serverless database with SQLite's SQL semantics", + }, + ] + : []), { value: "none" as const, label: "None", hint: "Manual setup" }, ]; } else if (databaseType === "postgres") { diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 1120de7..db8b4fd 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -60,6 +60,7 @@ export const DatabaseSetupSchema = z "prisma-postgres", "mongodb-atlas", "supabase", + "d1", "none", ]) .describe("Database hosting setup"); diff --git a/apps/cli/src/validation.ts b/apps/cli/src/validation.ts index 17c9e7f..d7942a5 100644 --- a/apps/cli/src/validation.ts +++ b/apps/cli/src/validation.ts @@ -358,6 +358,22 @@ export function processAndValidateFlags( process.exit(1); } + if (config.dbSetup === "d1") { + if (config.database !== "sqlite") { + consola.fatal( + "Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.", + ); + process.exit(1); + } + + if (config.runtime !== "workers") { + consola.fatal( + "Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.", + ); + process.exit(1); + } + } + if ( providedFlags.has("runtime") && options.runtime === "workers" && diff --git a/apps/cli/templates/db/drizzle/sqlite/drizzle.config.ts.hbs b/apps/cli/templates/db/drizzle/sqlite/drizzle.config.ts.hbs index 48cfec8..ce26a92 100644 --- a/apps/cli/templates/db/drizzle/sqlite/drizzle.config.ts.hbs +++ b/apps/cli/templates/db/drizzle/sqlite/drizzle.config.ts.hbs @@ -3,6 +3,16 @@ import { defineConfig } from "drizzle-kit"; export default defineConfig({ schema: "./src/db/schema", out: "./src/db/migrations", + {{#if (eq dbSetup "d1")}} + // DOCS: https://orm.drizzle.team/docs/guides/d1-http-with-drizzle-kit + dialect: "sqlite", + driver: "d1-http", + dbCredentials: { + accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, + databaseId: process.env.CLOUDFLARE_DATABASE_ID!, + token: process.env.CLOUDFLARE_D1_TOKEN!, + }, + {{else}} dialect: "turso", dbCredentials: { url: process.env.DATABASE_URL || "", @@ -10,4 +20,5 @@ export default defineConfig({ authToken: process.env.DATABASE_AUTH_TOKEN, {{/if}} }, + {{/if}} }); diff --git a/apps/cli/templates/db/drizzle/sqlite/src/db/index.ts.hbs b/apps/cli/templates/db/drizzle/sqlite/src/db/index.ts.hbs index 18dd260..62325d1 100644 --- a/apps/cli/templates/db/drizzle/sqlite/src/db/index.ts.hbs +++ b/apps/cli/templates/db/drizzle/sqlite/src/db/index.ts.hbs @@ -13,6 +13,12 @@ export const db = drizzle({ client }); {{/if}} {{#if (eq runtime "workers")}} +{{#if (eq dbSetup "d1")}} +import { drizzle } from "drizzle-orm/d1"; +import { env } from "cloudflare:workers"; + +export const db = drizzle(env.DB); +{{else}} import { drizzle } from "drizzle-orm/libsql"; import { env } from "cloudflare:workers"; import { createClient } from "@libsql/client"; @@ -26,3 +32,4 @@ const client = createClient({ export const db = drizzle({ client }); {{/if}} +{{/if}} diff --git a/apps/cli/templates/runtime/workers/apps/server/wrangler.jsonc.hbs b/apps/cli/templates/runtime/workers/apps/server/wrangler.jsonc.hbs index 2d34090..1aabfb3 100644 --- a/apps/cli/templates/runtime/workers/apps/server/wrangler.jsonc.hbs +++ b/apps/cli/templates/runtime/workers/apps/server/wrangler.jsonc.hbs @@ -5,14 +5,30 @@ "compatibility_flags": ["nodejs_compat"], "vars": { "NODE_ENV": "production" - // Non-sensitive environment variables (visible in dashboard) - // "CORS_ORIGIN": "https://your-frontend-domain.com", - // "BETTER_AUTH_URL": "https://your-worker-domain.workers.dev" + // Add public environment variables here + // Example: "CORS_ORIGIN": "https://your-domain.com" } - // ⚠️ SENSITIVE DATA: Use `wrangler secret put` instead of adding here - // Don't put these in "vars" - they'll be visible in the dashboard! - // - DATABASE_URL - // - DATABASE_AUTH_TOKEN - // - GOOGLE_GENERATIVE_AI_API_KEY - // - BETTER_AUTH_SECRET + // For sensitive data, use: + // wrangler secret put SECRET_NAME + // Don't add secrets to "vars" - they're visible in the dashboard! + + {{#if (eq dbSetup "d1")}}, + // To set up D1 database: + // 1. Run: wrangler login + // 2. Run: wrangler d1 create your-database-name + // 3. Copy the output and paste below + // Then run migrations: + // bun db:generate + // To apply migrations locally, run: + // wrangler d1 migrations apply YOUR_DB_NAME --local + "d1_databases": [ + { + "binding": "DB", + "database_name": "YOUR_DB_NAME", + "database_id": "YOUR_DB_ID", + "preview_database_id": "local-test-db", + "migrations_dir": "./src/db/migrations" + } + ] + {{/if}} }