From 5c5a4b22930174ca5139e0efe69581b8c8b88ed5 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Tue, 13 May 2025 19:50:36 +0530 Subject: [PATCH] add supabase database setup (#254) --- .changeset/neat-candies-cough.md | 5 + apps/cli/src/helpers/db-setup.ts | 5 +- apps/cli/src/helpers/env-setup.ts | 3 +- apps/cli/src/helpers/supabase-setup.ts | 228 ++++++++++++++++++ apps/cli/src/index.ts | 10 +- apps/cli/src/prompts/db-setup.ts | 7 +- apps/cli/src/types.ts | 1 + .../backend/server/next/package.json | 3 +- .../{schema.prisma => schema.prisma.hbs} | 3 + apps/web/public/icon/supabase.svg | 15 ++ .../app/(home)/_components/stack-builder.tsx | 28 ++- apps/web/src/lib/constant.ts | 7 + 12 files changed, 308 insertions(+), 7 deletions(-) create mode 100644 .changeset/neat-candies-cough.md create mode 100644 apps/cli/src/helpers/supabase-setup.ts rename apps/cli/templates/db/prisma/postgres/prisma/schema/{schema.prisma => schema.prisma.hbs} (69%) create mode 100644 apps/web/public/icon/supabase.svg diff --git a/.changeset/neat-candies-cough.md b/.changeset/neat-candies-cough.md new file mode 100644 index 0000000..f315037 --- /dev/null +++ b/.changeset/neat-candies-cough.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +add supabase database setup diff --git a/apps/cli/src/helpers/db-setup.ts b/apps/cli/src/helpers/db-setup.ts index 4bb055d..1228676 100644 --- a/apps/cli/src/helpers/db-setup.ts +++ b/apps/cli/src/helpers/db-setup.ts @@ -6,6 +6,7 @@ import pc from "picocolors"; import { addPackageDependency } from "../utils/add-package-deps"; import { setupMongoDBAtlas } from "./mongodb-atlas-setup"; import { setupPrismaPostgres } from "./prisma-postgres-setup"; +import { setupSupabase } from "./supabase-setup"; import { setupTurso } from "./turso-setup"; import { setupNeonPostgres } from "./neon-setup"; @@ -13,7 +14,7 @@ import { setupNeonPostgres } from "./neon-setup"; import type { ProjectConfig } from "../types"; export async function setupDatabase(config: ProjectConfig): Promise { - const { projectName, database, orm, dbSetup, backend, projectDir } = config; + const { database, orm, dbSetup, backend, projectDir } = config; if (backend === "convex" || database === "none") { if (backend !== "convex") { @@ -75,6 +76,8 @@ export async function setupDatabase(config: ProjectConfig): Promise { await setupPrismaPostgres(config); } else if (dbSetup === "neon") { await setupNeonPostgres(config); + } else if (dbSetup === "supabase") { + await setupSupabase(config); } } else if (database === "mongodb" && dbSetup === "mongodb-atlas") { await setupMongoDBAtlas(config); diff --git a/apps/cli/src/helpers/env-setup.ts b/apps/cli/src/helpers/env-setup.ts index 7aa961c..1350907 100644 --- a/apps/cli/src/helpers/env-setup.ts +++ b/apps/cli/src/helpers/env-setup.ts @@ -163,7 +163,8 @@ export async function setupEnvironmentVariables( dbSetup === "turso" || dbSetup === "prisma-postgres" || dbSetup === "mongodb-atlas" || - dbSetup === "neon"; + dbSetup === "neon" || + dbSetup === "supabase"; if (database !== "none" && !specializedSetup) { switch (database) { diff --git a/apps/cli/src/helpers/supabase-setup.ts b/apps/cli/src/helpers/supabase-setup.ts new file mode 100644 index 0000000..042b583 --- /dev/null +++ b/apps/cli/src/helpers/supabase-setup.ts @@ -0,0 +1,228 @@ +import path from "node:path"; +import { log } from "@clack/prompts"; +import { consola } from "consola"; +import { type ExecaError, execa } from "execa"; +import fs from "fs-extra"; +import pc from "picocolors"; +import type { ProjectConfig, ProjectPackageManager } from "../types"; +import { getPackageExecutionCommand } from "../utils/get-package-execution-command"; + +async function writeSupabaseEnvFile( + projectDir: string, + databaseUrl: string, +): Promise { + try { + const envPath = path.join(projectDir, "apps/server", ".env"); + await fs.ensureDir(path.dirname(envPath)); + + let envContent = ""; + if (await fs.pathExists(envPath)) { + envContent = await fs.readFile(envPath, "utf8"); + } + + const dbUrlToUse = + databaseUrl || "postgresql://postgres:postgres@127.0.0.1:54322/postgres"; + + const databaseUrlLine = `DATABASE_URL="${dbUrlToUse}"`; + const directUrlLine = `DIRECT_URL="${dbUrlToUse}"`; + + if (!envContent.includes("DATABASE_URL=")) { + envContent += `\n${databaseUrlLine}`; + } else { + envContent = envContent.replace( + /DATABASE_URL=.*(\r?\n|$)/, + `${databaseUrlLine}$1`, + ); + } + + if (!envContent.includes("DIRECT_URL=")) { + envContent += `\n${directUrlLine}`; + } else { + envContent = envContent.replace( + /DIRECT_URL=.*(\r?\n|$)/, + `${directUrlLine}$1`, + ); + } + + await fs.writeFile(envPath, envContent.trim()); + return true; + } catch (error) { + consola.error(pc.red("Failed to update .env file for Supabase.")); + if (error instanceof Error) { + consola.error(error.message); + } + return false; + } +} + +function extractDbUrl(output: string): string | null { + const dbUrlMatch = output.match(/DB URL:\s*(postgresql:\/\/[^\s]+)/); + const url = dbUrlMatch?.[1]; + if (url) { + return url; + } + return null; +} + +async function initializeSupabase( + serverDir: string, + packageManager: ProjectPackageManager, +): Promise { + log.info("Initializing Supabase project..."); + try { + const supabaseInitCommand = getPackageExecutionCommand( + packageManager, + "supabase init", + ); + await execa(supabaseInitCommand, { + cwd: serverDir, + stdio: "inherit", + shell: true, + }); + log.success("Supabase project initialized successfully."); + return true; + } catch (error) { + consola.error(pc.red("Failed to initialize Supabase project.")); + if (error instanceof Error) { + consola.error(error.message); + } else { + consola.error(String(error)); + } + if (error instanceof Error && error.message.includes("ENOENT")) { + log.error( + pc.red( + "Supabase CLI not found. Please install it globally or ensure it's in your PATH.", + ), + ); + log.info("You can install it using: npm install -g supabase"); + } + return false; + } +} + +async function startSupabase( + serverDir: string, + packageManager: ProjectPackageManager, +): Promise { + log.info("Starting Supabase services (this may take a moment)..."); + const supabaseStartCommand = getPackageExecutionCommand( + packageManager, + "supabase start", + ); + try { + const subprocess = execa(supabaseStartCommand, { + cwd: serverDir, + shell: true, + }); + + let stdoutData = ""; + + if (subprocess.stdout) { + subprocess.stdout.on("data", (data) => { + const text = data.toString(); + process.stdout.write(text); + stdoutData += text; + }); + } + + if (subprocess.stderr) { + subprocess.stderr.pipe(process.stderr); + } + + await subprocess; + + await new Promise((resolve) => setTimeout(resolve, 100)); + + return stdoutData; + } catch (error) { + consola.error(pc.red("Failed to start Supabase services.")); + const execaError = error as ExecaError; + if (execaError?.message) { + consola.error(`Error details: ${execaError.message}`); + if (execaError.message.includes("Docker is not running")) { + log.error( + pc.red("Docker is not running. Please start Docker and try again."), + ); + } + } else { + consola.error(String(error)); + } + return null; + } +} + +function displayManualSupabaseInstructions(output?: string | null) { + log.info( + `"Manual Supabase Setup Instructions:" +1. Ensure Docker is installed and running. +2. Install the Supabase CLI (e.g., \`npm install -g supabase\`). +3. Run \`supabase init\` in your project's \`apps/server\` directory. +4. Run \`supabase start\` in your project's \`apps/server\` directory. +5. Copy the 'DB URL' from the output.${ + output + ? ` +${pc.bold("Relevant output from `supabase start`:")} +${pc.dim(output)}` + : "" + } +6. Add the DB URL to the .env file in \`apps/server/.env\` as \`DATABASE_URL\`: + ${pc.gray('DATABASE_URL="your_supabase_db_url"')}`, + ); +} + +export async function setupSupabase(config: ProjectConfig) { + const { projectDir, packageManager } = config; + + const serverDir = path.join(projectDir, "apps", "server"); + + try { + await fs.ensureDir(serverDir); + + const initialized = await initializeSupabase(serverDir, packageManager); + if (!initialized) { + displayManualSupabaseInstructions(); + return; + } + + const supabaseOutput = await startSupabase(serverDir, packageManager); + if (!supabaseOutput) { + displayManualSupabaseInstructions(); + return; + } + + const dbUrl = extractDbUrl(supabaseOutput); + + if (dbUrl) { + const envUpdated = await writeSupabaseEnvFile(projectDir, dbUrl); + + if (envUpdated) { + log.success(pc.green("Supabase local development setup complete!")); + } else { + log.error( + pc.red( + "Supabase setup completed, but failed to update .env automatically.", + ), + ); + displayManualSupabaseInstructions(supabaseOutput); + } + } else { + log.error( + pc.yellow( + "Supabase started, but could not extract DB URL automatically.", + ), + ); + displayManualSupabaseInstructions(supabaseOutput); + } + } catch (error) { + if (error instanceof Error) { + consola.error(pc.red(`Error during Supabase setup: ${error.message}`)); + } else { + consola.error( + pc.red( + `An unknown error occurred during Supabase setup: ${String(error)}`, + ), + ); + } + displayManualSupabaseInstructions(); + } +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index add84fb..1fe1853 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -15,6 +15,7 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { DEFAULT_CONFIG } from "./constants"; import { createProject } from "./helpers/create-project"; +import { setupDatabase } from "./helpers/db-setup"; import { gatherConfig } from "./prompts/config-prompts"; import { getProjectName } from "./prompts/project-name"; import type { @@ -128,7 +129,14 @@ async function main() { .option("db-setup", { type: "string", describe: "Database setup", - choices: ["turso", "neon", "prisma-postgres", "mongodb-atlas", "none"], + choices: [ + "turso", + "neon", + "prisma-postgres", + "mongodb-atlas", + "supabase", + "none", + ], }) .option("backend", { type: "string", diff --git a/apps/cli/src/prompts/db-setup.ts b/apps/cli/src/prompts/db-setup.ts index 0ce2d72..e881d60 100644 --- a/apps/cli/src/prompts/db-setup.ts +++ b/apps/cli/src/prompts/db-setup.ts @@ -1,4 +1,4 @@ -import { cancel, isCancel, log, select } from "@clack/prompts"; +import { cancel, isCancel, select } from "@clack/prompts"; import pc from "picocolors"; import type { ProjectBackend, ProjectDBSetup, ProjectOrm } from "../types"; @@ -41,6 +41,11 @@ export async function getDBSetupChoice( label: "Neon Postgres", hint: "Serverless Postgres with branching capability", }, + { + value: "supabase" as const, + label: "Supabase", + hint: "Local Supabase stack (requires Docker)", + }, ...(orm === "prisma" ? [ { diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index d123dc9..f539c1a 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -39,6 +39,7 @@ export type ProjectDBSetup = | "prisma-postgres" | "mongodb-atlas" | "neon" + | "supabase" | "none"; export type ProjectApi = "trpc" | "orpc" | "none"; diff --git a/apps/cli/templates/backend/server/next/package.json b/apps/cli/templates/backend/server/next/package.json index e39ed2e..1395919 100644 --- a/apps/cli/templates/backend/server/next/package.json +++ b/apps/cli/templates/backend/server/next/package.json @@ -8,7 +8,8 @@ "start": "next start" }, "dependencies": { - "next": "15.3.0" + "next": "15.3.0", + "dotenv": "^16.5.0" }, "devDependencies": { "@types/node": "^20", diff --git a/apps/cli/templates/db/prisma/postgres/prisma/schema/schema.prisma b/apps/cli/templates/db/prisma/postgres/prisma/schema/schema.prisma.hbs similarity index 69% rename from apps/cli/templates/db/prisma/postgres/prisma/schema/schema.prisma rename to apps/cli/templates/db/prisma/postgres/prisma/schema/schema.prisma.hbs index 2009f11..b9fd88f 100644 --- a/apps/cli/templates/db/prisma/postgres/prisma/schema/schema.prisma +++ b/apps/cli/templates/db/prisma/postgres/prisma/schema/schema.prisma.hbs @@ -7,4 +7,7 @@ generator client { datasource db { provider = "postgres" url = env("DATABASE_URL") + {{#if (eq dbSetup "supabase")}} + directUrl = env("DIRECT_URL") + {{/if}} } diff --git a/apps/web/public/icon/supabase.svg b/apps/web/public/icon/supabase.svg new file mode 100644 index 0000000..574f67e --- /dev/null +++ b/apps/web/public/icon/supabase.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/web/src/app/(home)/_components/stack-builder.tsx b/apps/web/src/app/(home)/_components/stack-builder.tsx index 4b600a0..6662901 100644 --- a/apps/web/src/app/(home)/_components/stack-builder.tsx +++ b/apps/web/src/app/(home)/_components/stack-builder.tsx @@ -289,7 +289,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { `No backend requires ${displayName} to be '${valueDisplay}'.`, ); notes[catKey].hasIssue = true; - notes[catKey].hasIssue = true; (nextStack[catKey] as string | string[]) = value; changed = true; changes.push({ @@ -512,6 +511,24 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { message: "Database set to 'PostgreSQL' (required by Neon)", }); } + } else if (nextStack.dbSetup === "supabase") { + if (nextStack.database !== "postgres") { + notes.dbSetup.notes.push( + "Supabase (local) requires PostgreSQL. It will be selected.", + ); + notes.database.notes.push( + "Supabase (local) DB setup requires PostgreSQL. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "postgres"; + changed = true; + changes.push({ + category: "dbSetup", + message: + "Database set to 'PostgreSQL' (required by Supabase setup)", + }); + } } const isNuxt = nextStack.webFrontend.includes("nuxt"); @@ -961,7 +978,6 @@ const StackBuilder = () => { const disabledReasons = useMemo(() => { const reasons = new Map(); - const addRule = (category: string, techId: string, reason: string) => { reasons.set(`${category}-${techId}`, reason); }; @@ -1219,6 +1235,14 @@ const StackBuilder = () => { "Disabled: Neon requires PostgreSQL. (Will auto-select if chosen)", ); } + } else if (techId === "supabase") { + if (stack.database !== "postgres" && stack.database !== "none") { + addRule( + category, + techId, + "Disabled: Supabase (local) requires PostgreSQL. (Will auto-select if chosen)", + ); + } } } diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 4f81e66..6e4c067 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -276,6 +276,13 @@ export const TECH_OPTIONS = { icon: "/icon/mongodb.svg", color: "from-green-400 to-green-600", }, + { + id: "supabase", + name: "Supabase", + description: "Local Supabase stack (requires Docker)", + icon: "/icon/supabase.svg", + color: "from-emerald-400 to-emerald-600", + }, { id: "none", name: "Basic Setup",