add supabase database setup (#254)

This commit is contained in:
Aman Varshney
2025-05-13 19:50:36 +05:30
committed by GitHub
parent 745dca1d6a
commit 5c5a4b2293
12 changed files with 308 additions and 7 deletions

View File

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

View File

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

View File

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

View File

@@ -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<boolean> {
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<boolean> {
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<string | null> {
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();
}
}

View File

@@ -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",

View File

@@ -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"
? [
{

View File

@@ -39,6 +39,7 @@ export type ProjectDBSetup =
| "prisma-postgres"
| "mongodb-atlas"
| "neon"
| "supabase"
| "none";
export type ProjectApi = "trpc" | "orpc" | "none";

View File

@@ -8,7 +8,8 @@
"start": "next start"
},
"dependencies": {
"next": "15.3.0"
"next": "15.3.0",
"dotenv": "^16.5.0"
},
"devDependencies": {
"@types/node": "^20",

View File

@@ -7,4 +7,7 @@ generator client {
datasource db {
provider = "postgres"
url = env("DATABASE_URL")
{{#if (eq dbSetup "supabase")}}
directUrl = env("DIRECT_URL")
{{/if}}
}

View File

@@ -0,0 +1,15 @@
<svg viewBox="0 0 109 113" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/>
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" fill-opacity="0.2"/>
<path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/>
<defs>
<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295" gradientUnits="userSpaceOnUse">
<stop stop-color="#249361"/>
<stop offset="1" stop-color="#3ECF8E"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -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<string, string>();
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)",
);
}
}
}

View File

@@ -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",