diff --git a/.changeset/purple-deers-pretend.md b/.changeset/purple-deers-pretend.md new file mode 100644 index 0000000000..43bcffb674 --- /dev/null +++ b/.changeset/purple-deers-pretend.md @@ -0,0 +1,5 @@ +--- +"create-t3-app": minor +--- + +Add support for Neon as Postgresql Database Provider diff --git a/cli/src/cli/index.ts b/cli/src/cli/index.ts index ce5ef6ca50..05238e0a50 100644 --- a/cli/src/cli/index.ts +++ b/cli/src/cli/index.ts @@ -44,6 +44,7 @@ interface CliResults { packages: AvailablePackages[]; flags: CliFlags; databaseProvider: DatabaseProvider; + drizzleDatabaseProvider: DatabaseProvider; } const defaultOptions: CliResults = { @@ -64,6 +65,7 @@ const defaultOptions: CliResults = { dbProvider: "sqlite", }, databaseProvider: "sqlite", + drizzleDatabaseProvider: "sqlite", }; export const runCli = async (): Promise => { @@ -296,6 +298,7 @@ export const runCli = async (): Promise => { { value: "mysql", label: "MySQL" }, { value: "postgres", label: "PostgreSQL" }, { value: "planetscale", label: "PlanetScale" }, + { value: "neon", label: "Neon" }, ], initialValue: "sqlite", }); @@ -342,11 +345,23 @@ export const runCli = async (): Promise => { if (project.database === "prisma") packages.push("prisma"); if (project.database === "drizzle") packages.push("drizzle"); + // Preserve the original databaseProvider for Prisma and other logic + const originalDatabaseProvider = project.databaseProvider; + + // Map Neon to postgres and Planetscale to mysql when Drizzle is selected + const drizzleDatabaseProvider = ( + originalDatabaseProvider === "neon" + ? "postgres" + : originalDatabaseProvider === "planetscale" + ? "mysql" + : originalDatabaseProvider + ) as DatabaseProvider; + return { appName: project.name ?? cliResults.appName, packages, databaseProvider: - (project.databaseProvider as DatabaseProvider) || "sqlite", + (originalDatabaseProvider as DatabaseProvider) || "sqlite", flags: { ...cliResults.flags, appRouter: project.appRouter ?? cliResults.flags.appRouter, @@ -354,6 +369,7 @@ export const runCli = async (): Promise => { noInstall: !project.install || cliResults.flags.noInstall, importAlias: project.importAlias ?? cliResults.flags.importAlias, }, + drizzleDatabaseProvider, // Pass this separately to the Drizzle installer }; } catch (err) { // If the user is not calling create-t3-app from an interactive terminal, inquirer will throw an IsTTYError diff --git a/cli/src/helpers/createProject.ts b/cli/src/helpers/createProject.ts index 81b385485f..85c75aeecd 100644 --- a/cli/src/helpers/createProject.ts +++ b/cli/src/helpers/createProject.ts @@ -24,6 +24,7 @@ interface CreateProjectOptions { importAlias: string; appRouter: boolean; databaseProvider: DatabaseProvider; + drizzleDatabaseProvider: DatabaseProvider; } export const createProject = async ({ @@ -33,6 +34,7 @@ export const createProject = async ({ noInstall, appRouter, databaseProvider, + drizzleDatabaseProvider, }: CreateProjectOptions) => { const pkgManager = getUserPkgManager(); const projectDir = path.resolve(process.cwd(), projectName); @@ -46,6 +48,7 @@ export const createProject = async ({ noInstall, appRouter, databaseProvider, + drizzleDatabaseProvider, }); // Install the selected packages @@ -58,6 +61,7 @@ export const createProject = async ({ noInstall, appRouter, databaseProvider, + drizzleDatabaseProvider, }); // Select necessary _app,index / layout,page files diff --git a/cli/src/index.ts b/cli/src/index.ts index 0394f25180..ead9a69115 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -40,6 +40,7 @@ const main = async () => { packages, flags: { noGit, noInstall, importAlias, appRouter }, databaseProvider, + drizzleDatabaseProvider, } = await runCli(); const usePackages = buildPkgInstallerMap(packages, databaseProvider); @@ -52,6 +53,7 @@ const main = async () => { scopedAppName, packages: usePackages, databaseProvider, + drizzleDatabaseProvider, importAlias, noInstall, appRouter, diff --git a/cli/src/installers/dependencyVersionMap.ts b/cli/src/installers/dependencyVersionMap.ts index 868214a665..f1985242b2 100644 --- a/cli/src/installers/dependencyVersionMap.ts +++ b/cli/src/installers/dependencyVersionMap.ts @@ -21,6 +21,7 @@ export const dependencyVersionMap = { "@planetscale/database": "^1.19.0", postgres: "^3.4.4", "@libsql/client": "^0.9.0", + "@neondatabase/serverless": "^0.9.4", // TailwindCSS tailwindcss: "^3.4.3", diff --git a/cli/src/installers/drizzle.ts b/cli/src/installers/drizzle.ts index d9c5930b98..193c172709 100644 --- a/cli/src/installers/drizzle.ts +++ b/cli/src/installers/drizzle.ts @@ -11,7 +11,7 @@ export const drizzleInstaller: Installer = ({ projectDir, packages, scopedAppName, - databaseProvider, + drizzleDatabaseProvider, }) => { const devPackages: AvailableDependencies[] = [ "drizzle-kit", @@ -31,10 +31,11 @@ export const drizzleInstaller: Installer = ({ { planetscale: "@planetscale/database", mysql: "mysql2", + neon: "postgres", postgres: "postgres", sqlite: "@libsql/client", } as const - )[databaseProvider], + )[drizzleDatabaseProvider], ], devMode: false, }); @@ -43,9 +44,7 @@ export const drizzleInstaller: Installer = ({ const configFile = path.join( extrasDir, - `config/drizzle-config-${ - databaseProvider === "planetscale" ? "mysql" : databaseProvider - }.ts` + `config/drizzle-config-${drizzleDatabaseProvider}.ts` ); const configDest = path.join(projectDir, "drizzle.config.ts"); @@ -53,8 +52,8 @@ export const drizzleInstaller: Installer = ({ extrasDir, "src/server/db/schema-drizzle", packages?.nextAuth.inUse - ? `with-auth-${databaseProvider}.ts` - : `base-${databaseProvider}.ts` + ? `with-auth-${drizzleDatabaseProvider}.ts` + : `base-${drizzleDatabaseProvider}.ts` ); const schemaDest = path.join(projectDir, "src/server/db/schema.ts"); @@ -71,7 +70,7 @@ export const drizzleInstaller: Installer = ({ const clientSrc = path.join( extrasDir, - `src/server/db/index-drizzle/with-${databaseProvider}.ts` + `src/server/db/index-drizzle/with-${drizzleDatabaseProvider}.ts` ); const clientDest = path.join(projectDir, "src/server/db/index.ts"); diff --git a/cli/src/installers/envVars.ts b/cli/src/installers/envVars.ts index d3108fc074..645e3e841b 100644 --- a/cli/src/installers/envVars.ts +++ b/cli/src/installers/envVars.ts @@ -38,7 +38,7 @@ export const envVariablesInstaller: Installer = ({ } else { if (usingAuth) envFile = "with-auth.js"; } - + console.log("using env file:", envFile); if (envFile !== "") { const envSchemaSrc = path.join( PKG_ROOT, @@ -112,6 +112,10 @@ DATABASE_URL='mysql://YOUR_MYSQL_URL_HERE?ssl={"rejectUnauthorized":true}'`; content = `# Get the Database URL from the "prisma" dropdown selector in PlanetScale. DATABASE_URL='mysql://YOUR_MYSQL_URL_HERE?sslaccept=strict'`; } + } else if (databaseProvider === "neon") { + content += `# Get the database connection details from the Connection Details widget on the Neon Dashboard. +# Select a branch, a compute, a database, and a role. A connection string is constructed for you +DATABASE_URL="postgresql://YOUR_POSTGRES_CONNECTION_STRING_HERE?sslmode=require"`; } else if (databaseProvider === "mysql") { content += `DATABASE_URL="mysql://root:password@localhost:3306/${scopedAppName}"`; } else if (databaseProvider === "postgres") { diff --git a/cli/src/installers/index.ts b/cli/src/installers/index.ts index 776a16ba1c..08f70ed4fc 100644 --- a/cli/src/installers/index.ts +++ b/cli/src/installers/index.ts @@ -27,6 +27,7 @@ export const databaseProviders = [ "postgres", "sqlite", "planetscale", + "neon", ] as const; export type DatabaseProvider = (typeof databaseProviders)[number]; @@ -39,6 +40,7 @@ export interface InstallerOptions { projectName: string; scopedAppName: string; databaseProvider: DatabaseProvider; + drizzleDatabaseProvider: DatabaseProvider; } export type Installer = (opts: InstallerOptions) => void; diff --git a/cli/src/installers/prisma.ts b/cli/src/installers/prisma.ts index f92da12395..c4f5c93d76 100644 --- a/cli/src/installers/prisma.ts +++ b/cli/src/installers/prisma.ts @@ -28,13 +28,24 @@ export const prismaInstaller: Installer = ({ devMode: false, }); + if (databaseProvider === "neon") + addPackageDependency({ + projectDir, + dependencies: ["@neondatabase/serverless"], + devMode: false, + }); + const extrasDir = path.join(PKG_ROOT, "template/extras"); const schemaSrc = path.join( extrasDir, "prisma/schema", `${packages?.nextAuth.inUse ? "with-auth" : "base"}${ - databaseProvider === "planetscale" ? "-planetscale" : "" + databaseProvider === "planetscale" + ? "-planetscale" + : databaseProvider === "neon" + ? "-neon" + : "" }.prisma` ); let schemaText = fs.readFileSync(schemaSrc, "utf-8"); @@ -46,6 +57,7 @@ export const prismaInstaller: Installer = ({ mysql: "mysql", postgres: "postgresql", planetscale: "mysql", + neon: "postgresql", }[databaseProvider] }"` ); diff --git a/cli/template/extras/prisma/schema/base-neon.prisma b/cli/template/extras/prisma/schema/base-neon.prisma new file mode 100644 index 0000000000..87f3de8a61 --- /dev/null +++ b/cli/template/extras/prisma/schema/base-neon.prisma @@ -0,0 +1,21 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" + previewFeatures = ["driverAdapters"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Post { + id Int @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([name]) +} diff --git a/cli/template/extras/prisma/schema/with-auth-neon.prisma b/cli/template/extras/prisma/schema/with-auth-neon.prisma new file mode 100644 index 0000000000..5571e0ca37 --- /dev/null +++ b/cli/template/extras/prisma/schema/with-auth-neon.prisma @@ -0,0 +1,74 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" + previewFeatures = ["driverAdapters"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Post { + id Int @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + createdBy User @relation(fields: [createdById], references: [id]) + createdById String + + @@index([name]) + @@index([createdById]) +} + +// Necessary for NextAuth.js +model Account { + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@index([userId]) +} + +model Session { + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) +} + +model User { + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + accounts Account[] + sessions Session[] + posts Post[] +} + +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) +} diff --git a/cli/template/extras/src/server/db/db-prisma-neon.ts b/cli/template/extras/src/server/db/db-prisma-neon.ts new file mode 100644 index 0000000000..c6f2f2c42c --- /dev/null +++ b/cli/template/extras/src/server/db/db-prisma-neon.ts @@ -0,0 +1,29 @@ +import { neon } from "@neondatabase/serverless"; +import { PrismaNeonHTTP } from "@prisma/adapter-neon"; +import { PrismaClient } from "@prisma/client"; + +import { env } from "~/env"; + +// Initialize Neon client using the DATABASE_URL from the environment variables +const sql = neon(env.DATABASE_URL); + +// Set up the Prisma adapter for Neon +const adapter = new PrismaNeonHTTP(sql); + +// Create a new Prisma client instance with the Neon adapter +const createPrismaClient = () => + new PrismaClient({ + log: + env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], + adapter, + }); + +// Global variable to store the Prisma client instance across module reloads +const globalForPrisma = globalThis as unknown as { + prisma: ReturnType | undefined; +}; + +// Export the Prisma client instance, reusing it if it already exists +export const db = globalForPrisma.prisma ?? createPrismaClient(); + +if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;