mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
add supabase database setup (#254)
This commit is contained in:
5
.changeset/neat-candies-cough.md
Normal file
5
.changeset/neat-candies-cough.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-better-t-stack": minor
|
||||
---
|
||||
|
||||
add supabase database setup
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
228
apps/cli/src/helpers/supabase-setup.ts
Normal file
228
apps/cli/src/helpers/supabase-setup.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@ export type ProjectDBSetup =
|
||||
| "prisma-postgres"
|
||||
| "mongodb-atlas"
|
||||
| "neon"
|
||||
| "supabase"
|
||||
| "none";
|
||||
export type ProjectApi = "trpc" | "orpc" | "none";
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "15.3.0"
|
||||
"next": "15.3.0",
|
||||
"dotenv": "^16.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
|
||||
@@ -7,4 +7,7 @@ generator client {
|
||||
datasource db {
|
||||
provider = "postgres"
|
||||
url = env("DATABASE_URL")
|
||||
{{#if (eq dbSetup "supabase")}}
|
||||
directUrl = env("DIRECT_URL")
|
||||
{{/if}}
|
||||
}
|
||||
15
apps/web/public/icon/supabase.svg
Normal file
15
apps/web/public/icon/supabase.svg
Normal 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 |
@@ -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)",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user