Add D1 Database (#335)

This commit is contained in:
Aman Varshney
2025-06-20 09:17:32 +05:30
committed by GitHub
parent 846d70583e
commit 0c5dd2efee
14 changed files with 214 additions and 15 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
add d1 database setup

View File

@@ -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<void> {
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) {}
}

View File

@@ -8,7 +8,10 @@ import { setupAuth } from "../setup/auth-setup";
import { setupBackendDependencies } from "../setup/backend-setup"; import { setupBackendDependencies } from "../setup/backend-setup";
import { setupDatabase } from "../setup/db-setup"; import { setupDatabase } from "../setup/db-setup";
import { setupExamples } from "../setup/examples-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 { createReadme } from "./create-readme";
import { setupEnvironmentVariables } from "./env-setup"; import { setupEnvironmentVariables } from "./env-setup";
import { installDependencies } from "./install-dependencies"; import { installDependencies } from "./install-dependencies";
@@ -64,6 +67,7 @@ export async function createProject(options: ProjectConfig) {
} }
await handleExtras(projectDir, options); await handleExtras(projectDir, options);
await setupEnvironmentVariables(options); await setupEnvironmentVariables(options);
await updatePackageConfigurations(projectDir, options); await updatePackageConfigurations(projectDir, options);
await createReadme(projectDir, options); await createReadme(projectDir, options);
@@ -76,6 +80,7 @@ export async function createProject(options: ProjectConfig) {
projectDir, projectDir,
packageManager: options.packageManager, packageManager: options.packageManager,
}); });
await generateCloudflareWorkerTypes(options);
} }
displayPostInstallInstructions({ displayPostInstallInstructions({

View File

@@ -186,7 +186,8 @@ export async function setupEnvironmentVariables(
dbSetup === "prisma-postgres" || dbSetup === "prisma-postgres" ||
dbSetup === "mongodb-atlas" || dbSetup === "mongodb-atlas" ||
dbSetup === "neon" || dbSetup === "neon" ||
dbSetup === "supabase"; dbSetup === "supabase" ||
dbSetup === "d1";
if (database !== "none" && !specializedSetup) { if (database !== "none" && !specializedSetup) {
switch (database) { switch (database) {

View File

@@ -1,6 +1,12 @@
import { consola } from "consola"; import { consola } from "consola";
import pc from "picocolors"; 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"; import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
export function displayPostInstallInstructions( export function displayPostInstallInstructions(
@@ -16,6 +22,7 @@ export function displayPostInstallInstructions(
runtime, runtime,
frontend, frontend,
backend, backend,
dbSetup,
} = config; } = config;
const isConvex = backend === "convex"; const isConvex = backend === "convex";
@@ -26,7 +33,7 @@ export function displayPostInstallInstructions(
const databaseInstructions = const databaseInstructions =
!isConvex && database !== "none" !isConvex && database !== "none"
? getDatabaseInstructions(database, orm, runCmd, runtime) ? getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
: ""; : "";
const tauriInstructions = addons?.includes("tauri") const tauriInstructions = addons?.includes("tauri")
@@ -96,6 +103,11 @@ export function displayPostInstallInstructions(
} }
if (runtime === "workers") { 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++}.`)} bun dev\n`;
output += `${pc.cyan( output += `${pc.cyan(
`${stepCounter++}.`, `${stepCounter++}.`,
@@ -178,9 +190,46 @@ function getDatabaseInstructions(
orm?: ORM, orm?: ORM,
runCmd?: string, runCmd?: string,
runtime?: Runtime, runtime?: Runtime,
dbSetup?: DatabaseSetup,
): string { ): string {
const instructions = []; 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 (orm === "prisma") {
if (database === "sqlite") { if (database === "sqlite") {
instructions.push( instructions.push(
@@ -204,7 +253,7 @@ function getDatabaseInstructions(
} else if (orm === "drizzle") { } else if (orm === "drizzle") {
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`); instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`); instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
if (database === "sqlite") { if (database === "sqlite" && dbSetup !== "d1") {
instructions.push( instructions.push(
`${pc.cyan( `${pc.cyan(
"•", "•",

View File

@@ -5,6 +5,7 @@ import fs from "fs-extra";
import pc from "picocolors"; import pc from "picocolors";
import type { ProjectConfig } from "../../types"; import type { ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps"; import { addPackageDependency } from "../../utils/add-package-deps";
import { setupCloudflareD1 } from "../database-providers/d1-setup";
import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup"; import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
import { setupNeonPostgres } from "../database-providers/neon-setup"; import { setupNeonPostgres } from "../database-providers/neon-setup";
import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup"; import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup";
@@ -69,6 +70,8 @@ export async function setupDatabase(config: ProjectConfig): Promise<void> {
if (database === "sqlite" && dbSetup === "turso") { if (database === "sqlite" && dbSetup === "turso") {
await setupTurso(config); await setupTurso(config);
} else if (database === "sqlite" && dbSetup === "d1") {
await setupCloudflareD1(config);
} else if (database === "postgres") { } else if (database === "postgres") {
if (orm === "prisma" && dbSetup === "prisma-postgres") { if (orm === "prisma" && dbSetup === "prisma-postgres") {
await setupPrismaPostgres(config); await setupPrismaPostgres(config);

View File

@@ -1,5 +1,8 @@
import path from "node:path"; import path from "node:path";
import { spinner } from "@clack/prompts";
import { execa } from "execa";
import fs from "fs-extra"; import fs from "fs-extra";
import pc from "picocolors";
import type { Backend, ProjectConfig } from "../../types"; import type { Backend, ProjectConfig } from "../../types";
import { addPackageDependency } from "../../utils/add-package-deps"; import { addPackageDependency } from "../../utils/add-package-deps";
@@ -25,6 +28,43 @@ export async function setupRuntime(config: ProjectConfig): Promise<void> {
} }
} }
export async function generateCloudflareWorkerTypes(
config: ProjectConfig,
): Promise<void> {
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( async function setupBunRuntime(
serverDir: string, serverDir: string,
_backend: Backend, _backend: Backend,

View File

@@ -85,6 +85,7 @@ export async function gatherConfig(
flags.dbSetup, flags.dbSetup,
results.orm, results.orm,
results.backend, results.backend,
results.runtime,
), ),
git: () => getGitChoice(flags.git), git: () => getGitChoice(flags.git),
packageManager: () => getPackageManagerChoice(flags.packageManager), packageManager: () => getPackageManagerChoice(flags.packageManager),

View File

@@ -1,12 +1,13 @@
import { cancel, isCancel, select } from "@clack/prompts"; import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import type { Backend, DatabaseSetup, ORM } from "../types"; import type { Backend, DatabaseSetup, ORM, Runtime } from "../types";
export async function getDBSetupChoice( export async function getDBSetupChoice(
databaseType: string, databaseType: string,
dbSetup: DatabaseSetup | undefined, dbSetup: DatabaseSetup | undefined,
orm?: ORM, orm?: ORM,
backend?: Backend, backend?: Backend,
runtime?: Runtime,
): Promise<DatabaseSetup> { ): Promise<DatabaseSetup> {
if (backend === "convex") { if (backend === "convex") {
return "none"; return "none";
@@ -32,6 +33,15 @@ export async function getDBSetupChoice(
label: "Turso", label: "Turso",
hint: "SQLite for Production. Powered by libSQL", 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" }, { value: "none" as const, label: "None", hint: "Manual setup" },
]; ];
} else if (databaseType === "postgres") { } else if (databaseType === "postgres") {

View File

@@ -60,6 +60,7 @@ export const DatabaseSetupSchema = z
"prisma-postgres", "prisma-postgres",
"mongodb-atlas", "mongodb-atlas",
"supabase", "supabase",
"d1",
"none", "none",
]) ])
.describe("Database hosting setup"); .describe("Database hosting setup");

View File

@@ -358,6 +358,22 @@ export function processAndValidateFlags(
process.exit(1); 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 ( if (
providedFlags.has("runtime") && providedFlags.has("runtime") &&
options.runtime === "workers" && options.runtime === "workers" &&

View File

@@ -3,6 +3,16 @@ import { defineConfig } from "drizzle-kit";
export default defineConfig({ export default defineConfig({
schema: "./src/db/schema", schema: "./src/db/schema",
out: "./src/db/migrations", 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", dialect: "turso",
dbCredentials: { dbCredentials: {
url: process.env.DATABASE_URL || "", url: process.env.DATABASE_URL || "",
@@ -10,4 +20,5 @@ export default defineConfig({
authToken: process.env.DATABASE_AUTH_TOKEN, authToken: process.env.DATABASE_AUTH_TOKEN,
{{/if}} {{/if}}
}, },
{{/if}}
}); });

View File

@@ -13,6 +13,12 @@ export const db = drizzle({ client });
{{/if}} {{/if}}
{{#if (eq runtime "workers")}} {{#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 { drizzle } from "drizzle-orm/libsql";
import { env } from "cloudflare:workers"; import { env } from "cloudflare:workers";
import { createClient } from "@libsql/client"; import { createClient } from "@libsql/client";
@@ -26,3 +32,4 @@ const client = createClient({
export const db = drizzle({ client }); export const db = drizzle({ client });
{{/if}} {{/if}}
{{/if}}

View File

@@ -5,14 +5,30 @@
"compatibility_flags": ["nodejs_compat"], "compatibility_flags": ["nodejs_compat"],
"vars": { "vars": {
"NODE_ENV": "production" "NODE_ENV": "production"
// Non-sensitive environment variables (visible in dashboard) // Add public environment variables here
// "CORS_ORIGIN": "https://your-frontend-domain.com", // Example: "CORS_ORIGIN": "https://your-domain.com"
// "BETTER_AUTH_URL": "https://your-worker-domain.workers.dev"
} }
// ⚠️ SENSITIVE DATA: Use `wrangler secret put` instead of adding here // For sensitive data, use:
// Don't put these in "vars" - they'll be visible in the dashboard! // wrangler secret put SECRET_NAME
// - DATABASE_URL // Don't add secrets to "vars" - they're visible in the dashboard!
// - DATABASE_AUTH_TOKEN
// - GOOGLE_GENERATIVE_AI_API_KEY {{#if (eq dbSetup "d1")}},
// - BETTER_AUTH_SECRET // 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}}
} }