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 { addPackageDependency } from "../utils/add-package-deps";
|
||||||
import { setupMongoDBAtlas } from "./mongodb-atlas-setup";
|
import { setupMongoDBAtlas } from "./mongodb-atlas-setup";
|
||||||
import { setupPrismaPostgres } from "./prisma-postgres-setup";
|
import { setupPrismaPostgres } from "./prisma-postgres-setup";
|
||||||
|
import { setupSupabase } from "./supabase-setup";
|
||||||
import { setupTurso } from "./turso-setup";
|
import { setupTurso } from "./turso-setup";
|
||||||
|
|
||||||
import { setupNeonPostgres } from "./neon-setup";
|
import { setupNeonPostgres } from "./neon-setup";
|
||||||
@@ -13,7 +14,7 @@ import { setupNeonPostgres } from "./neon-setup";
|
|||||||
import type { ProjectConfig } from "../types";
|
import type { ProjectConfig } from "../types";
|
||||||
|
|
||||||
export async function setupDatabase(config: ProjectConfig): Promise<void> {
|
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" || database === "none") {
|
||||||
if (backend !== "convex") {
|
if (backend !== "convex") {
|
||||||
@@ -75,6 +76,8 @@ export async function setupDatabase(config: ProjectConfig): Promise<void> {
|
|||||||
await setupPrismaPostgres(config);
|
await setupPrismaPostgres(config);
|
||||||
} else if (dbSetup === "neon") {
|
} else if (dbSetup === "neon") {
|
||||||
await setupNeonPostgres(config);
|
await setupNeonPostgres(config);
|
||||||
|
} else if (dbSetup === "supabase") {
|
||||||
|
await setupSupabase(config);
|
||||||
}
|
}
|
||||||
} else if (database === "mongodb" && dbSetup === "mongodb-atlas") {
|
} else if (database === "mongodb" && dbSetup === "mongodb-atlas") {
|
||||||
await setupMongoDBAtlas(config);
|
await setupMongoDBAtlas(config);
|
||||||
|
|||||||
@@ -163,7 +163,8 @@ export async function setupEnvironmentVariables(
|
|||||||
dbSetup === "turso" ||
|
dbSetup === "turso" ||
|
||||||
dbSetup === "prisma-postgres" ||
|
dbSetup === "prisma-postgres" ||
|
||||||
dbSetup === "mongodb-atlas" ||
|
dbSetup === "mongodb-atlas" ||
|
||||||
dbSetup === "neon";
|
dbSetup === "neon" ||
|
||||||
|
dbSetup === "supabase";
|
||||||
|
|
||||||
if (database !== "none" && !specializedSetup) {
|
if (database !== "none" && !specializedSetup) {
|
||||||
switch (database) {
|
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 { hideBin } from "yargs/helpers";
|
||||||
import { DEFAULT_CONFIG } from "./constants";
|
import { DEFAULT_CONFIG } from "./constants";
|
||||||
import { createProject } from "./helpers/create-project";
|
import { createProject } from "./helpers/create-project";
|
||||||
|
import { setupDatabase } from "./helpers/db-setup";
|
||||||
import { gatherConfig } from "./prompts/config-prompts";
|
import { gatherConfig } from "./prompts/config-prompts";
|
||||||
import { getProjectName } from "./prompts/project-name";
|
import { getProjectName } from "./prompts/project-name";
|
||||||
import type {
|
import type {
|
||||||
@@ -128,7 +129,14 @@ async function main() {
|
|||||||
.option("db-setup", {
|
.option("db-setup", {
|
||||||
type: "string",
|
type: "string",
|
||||||
describe: "Database setup",
|
describe: "Database setup",
|
||||||
choices: ["turso", "neon", "prisma-postgres", "mongodb-atlas", "none"],
|
choices: [
|
||||||
|
"turso",
|
||||||
|
"neon",
|
||||||
|
"prisma-postgres",
|
||||||
|
"mongodb-atlas",
|
||||||
|
"supabase",
|
||||||
|
"none",
|
||||||
|
],
|
||||||
})
|
})
|
||||||
.option("backend", {
|
.option("backend", {
|
||||||
type: "string",
|
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 pc from "picocolors";
|
||||||
import type { ProjectBackend, ProjectDBSetup, ProjectOrm } from "../types";
|
import type { ProjectBackend, ProjectDBSetup, ProjectOrm } from "../types";
|
||||||
|
|
||||||
@@ -41,6 +41,11 @@ export async function getDBSetupChoice(
|
|||||||
label: "Neon Postgres",
|
label: "Neon Postgres",
|
||||||
hint: "Serverless Postgres with branching capability",
|
hint: "Serverless Postgres with branching capability",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "supabase" as const,
|
||||||
|
label: "Supabase",
|
||||||
|
hint: "Local Supabase stack (requires Docker)",
|
||||||
|
},
|
||||||
...(orm === "prisma"
|
...(orm === "prisma"
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export type ProjectDBSetup =
|
|||||||
| "prisma-postgres"
|
| "prisma-postgres"
|
||||||
| "mongodb-atlas"
|
| "mongodb-atlas"
|
||||||
| "neon"
|
| "neon"
|
||||||
|
| "supabase"
|
||||||
| "none";
|
| "none";
|
||||||
export type ProjectApi = "trpc" | "orpc" | "none";
|
export type ProjectApi = "trpc" | "orpc" | "none";
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"start": "next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "15.3.0"
|
"next": "15.3.0",
|
||||||
|
"dotenv": "^16.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
|||||||
@@ -7,4 +7,7 @@ generator client {
|
|||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgres"
|
provider = "postgres"
|
||||||
url = env("DATABASE_URL")
|
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}'.`,
|
`No backend requires ${displayName} to be '${valueDisplay}'.`,
|
||||||
);
|
);
|
||||||
notes[catKey].hasIssue = true;
|
notes[catKey].hasIssue = true;
|
||||||
notes[catKey].hasIssue = true;
|
|
||||||
(nextStack[catKey] as string | string[]) = value;
|
(nextStack[catKey] as string | string[]) = value;
|
||||||
changed = true;
|
changed = true;
|
||||||
changes.push({
|
changes.push({
|
||||||
@@ -512,6 +511,24 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
message: "Database set to 'PostgreSQL' (required by Neon)",
|
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");
|
const isNuxt = nextStack.webFrontend.includes("nuxt");
|
||||||
@@ -961,7 +978,6 @@ const StackBuilder = () => {
|
|||||||
|
|
||||||
const disabledReasons = useMemo(() => {
|
const disabledReasons = useMemo(() => {
|
||||||
const reasons = new Map<string, string>();
|
const reasons = new Map<string, string>();
|
||||||
|
|
||||||
const addRule = (category: string, techId: string, reason: string) => {
|
const addRule = (category: string, techId: string, reason: string) => {
|
||||||
reasons.set(`${category}-${techId}`, reason);
|
reasons.set(`${category}-${techId}`, reason);
|
||||||
};
|
};
|
||||||
@@ -1219,6 +1235,14 @@ const StackBuilder = () => {
|
|||||||
"Disabled: Neon requires PostgreSQL. (Will auto-select if chosen)",
|
"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",
|
icon: "/icon/mongodb.svg",
|
||||||
color: "from-green-400 to-green-600",
|
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",
|
id: "none",
|
||||||
name: "Basic Setup",
|
name: "Basic Setup",
|
||||||
|
|||||||
Reference in New Issue
Block a user