Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2cc278b
feat(cli): add SingleStore database and Helios setup support to type …
theskinnycoder Aug 15, 2025
245fb3f
feat(cli): add SingleStore compatibility rules and validation
theskinnycoder Aug 15, 2025
f5fdaa4
feat(cli): add SingleStore database prompts and setup options
theskinnycoder Aug 15, 2025
d4f6f85
feat(cli): add SingleStore Drizzle templates and example schemas
theskinnycoder Aug 15, 2025
a91f2fe
feat(cli): add SingleStore Helios provider setup and dependency manag…
theskinnycoder Aug 15, 2025
5d6f271
test(cli): add comprehensive SingleStore database tests and validation
theskinnycoder Aug 15, 2025
1fdac04
feat(web): add SingleStore database support to stack builder with opt…
theskinnycoder Aug 15, 2025
b0eae64
docs(cli): add SingleStore database setup guide and compatibility notes
theskinnycoder Aug 15, 2025
4f318a3
docs: add SingleStore database to CLI documentation and compatibility…
theskinnycoder Aug 15, 2025
da684c9
chore: update bun.lock for SingleStore dependencies
theskinnycoder Aug 15, 2025
18b6804
test(cli): fix SingleStore bigint assertion in TODO smoke test
theskinnycoder Aug 16, 2025
80da37c
fix(cli): update SingleStore connection string format and import path…
theskinnycoder Aug 16, 2025
6ad6671
refactor(cli): remove outdated comments in SingleStore setup and smok…
theskinnycoder Aug 16, 2025
66555cc
fix(cli): enforce compatibility rules for SingleStore with Cloudflare…
theskinnycoder Aug 16, 2025
7e11389
docs(cli): clarify SingleStore database description and update compat…
theskinnycoder Aug 16, 2025
fdcd32c
fix(cli): enforce SingleStore Helios setup requirement and update rel…
theskinnycoder Aug 16, 2025
d51747e
chore: update changeset configuration and add support for SingleStore…
theskinnycoder Aug 16, 2025
9943774
fix(cli): enhance compatibility checks for SingleStore with any other…
theskinnycoder Aug 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@better-t-stack/backend", "web"]
}
}
5 changes: 5 additions & 0 deletions .changeset/kind-geese-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---

Add SingleStore Helios database support
14 changes: 10 additions & 4 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ Follow the prompts to configure your project or use the `--yes` flag for default
| **Backend** | • Hono<br>• Express<br>• Elysia<br>• Next.js API routes<br>• Convex<br>• Fastify<br>• None |
| **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None |
| **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None |
| **Database** | • SQLite<br>• PostgreSQL<br>• MySQL<br>• MongoDB<br>• None |
| **Database** | • SQLite<br>• PostgreSQL<br>• MySQL<br>• MongoDB<br>• SingleStore<br>• None |
| **ORM** | • Drizzle (TypeScript-first)<br>• Prisma (feature-rich)<br>• Mongoose (for MongoDB)<br>• None |
| **Database Setup** | • Turso (SQLite)<br>• Cloudflare D1 (SQLite)<br>• Neon (PostgreSQL)<br>• Supabase (PostgreSQL)<br>• Prisma Postgres (via Prisma Accelerate)<br>• MongoDB Atlas<br>• None (manual setup) |
| **Database Setup** | • Turso (SQLite)<br>• Cloudflare D1 (SQLite)<br>• Neon (PostgreSQL)<br>• Supabase (PostgreSQL)<br>• Prisma Postgres (via Prisma Accelerate)<br>• MongoDB Atlas<br>• SingleStore Helios (cloud-hosted SingleStore)<br>• None (manual setup) |
| **Authentication** | Better-Auth (email/password, with more options coming soon) |
| **Styling** | Tailwind CSS with shadcn/ui components |
| **Addons** | • PWA support<br>• Tauri (desktop applications)<br>• Starlight (documentation site)<br>• Biome (linting and formatting)<br>• Husky (Git hooks)<br>• Turborepo (optimized builds) |
Expand All @@ -53,7 +53,7 @@ Usage: create-better-t-stack [project-directory] [options]
Options:
-V, --version Output the version number
-y, --yes Use default configuration
--database <type> Database type (none, sqlite, postgres, mysql, mongodb)
--database <type> Database type (none, sqlite, postgres, mysql, mongodb, singlestore)
--orm <type> ORM type (none, drizzle, prisma, mongoose)
--auth Include authentication
--no-auth Exclude authentication
Expand All @@ -65,7 +65,7 @@ Options:
--package-manager <pm> Package manager (npm, pnpm, bun)
--install Install dependencies
--no-install Skip installing dependencies
--db-setup <setup> Database setup (turso, d1, neon, supabase, prisma-postgres, mongodb-atlas, docker, none)
--db-setup <setup> Database setup (turso, d1, neon, supabase, prisma-postgres, mongodb-atlas, singlestore-helios, docker, none)
--web-deploy <setup> Web deployment (workers, none)
--backend <framework> Backend framework (hono, express, elysia, next, convex, fastify, none)
--runtime <runtime> Runtime (bun, node, workers, none)
Expand Down Expand Up @@ -173,6 +173,11 @@ Create a Cloudflare Workers project:

```bash
npx create-better-t-stack my-app --backend hono --runtime workers --database sqlite --orm drizzle --db-setup d1

Create a SingleStore project with Helios cloud setup:

```bash
npx create-better-t-stack my-app --backend hono --runtime node --database singlestore --orm drizzle --db-setup singlestore-helios --api trpc
```

Create a minimal API-only project:
Expand All @@ -191,6 +196,7 @@ npx create-better-t-stack my-app --frontend none --backend hono --api trpc --dat
- **ORM 'none'**: Can be used when you want to handle database operations manually or use a different ORM.
- **Runtime 'none'**: Only available with Convex backend or when backend is 'none'.
- **Cloudflare Workers runtime**: Only compatible with Hono backend, Drizzle ORM (or no ORM), and SQLite database (with D1 setup). Not compatible with MongoDB.
- **SingleStore database**: Only compatible with Drizzle ORM. Always uses SingleStore Helios setup (no manual setup option).
- **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Husky, Turborepo).
- **Examples 'none'**: Skips all example implementations (todo, AI chat).
- **SvelteKit, Nuxt, and SolidJS** frontends are only compatible with oRPC API layer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import path from "node:path";
import { log } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectConfig } from "../../types";
import {
addEnvVariablesToFile,
type EnvVariable,
} from "../project-generation/env-setup";

type SingleStoreHeliosConfig = {
connectionString: string;
};

async function writeEnvFile(
projectDir: string,
config?: SingleStoreHeliosConfig,
) {
try {
const envPath = path.join(projectDir, "apps/server", ".env");
const variables: EnvVariable[] = [
{
key: "DATABASE_URL",
value:
config?.connectionString ??
"singlestore://username:password@host:port/database?ssl={}",
condition: true,
},
];
await addEnvVariablesToFile(envPath, variables);
} catch (_error) {
log.error("Failed to update environment configuration");
}
}

function displayManualSetupInstructions() {
log.info(`
${pc.green("SingleStore Helios Manual Setup Instructions:")}

1. Sign up for SingleStore Cloud at:
${pc.blue("https://www.singlestore.com/cloud")}

2. Create a new workspace from the dashboard

3. Get your connection string from the workspace details:
Format: ${pc.dim("singlestore://USERNAME:PASSWORD@HOST:PORT/DATABASE?ssl={}")}

4. Add the connection string to your .env file:
${pc.dim('DATABASE_URL="your_connection_string"')}

${pc.yellow("Important:")}
- The connection string MUST include ${pc.bold("ssl={}")} at the end
- Use the singlestore:// protocol for SingleStore connections
- SingleStore requires SSL connections for cloud deployments`);
}

export async function setupSingleStoreHelios(config: ProjectConfig) {
const { projectDir } = config;

try {
const serverDir = path.join(projectDir, "apps/server");
await fs.ensureDir(serverDir);
await writeEnvFile(projectDir);

log.success(
pc.green(
"SingleStore Helios setup complete! Please update the connection string in .env file.",
),
);

displayManualSetupInstructions();
} catch (error) {
log.error(pc.red("SingleStore Helios setup failed"));
if (error instanceof Error) {
log.error(pc.red(error.message));
}

try {
await writeEnvFile(projectDir);
displayManualSetupInstructions();
} catch {}
}
}
1 change: 1 addition & 0 deletions apps/cli/src/helpers/project-generation/env-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
dbSetup === "mongodb-atlas" ||
dbSetup === "neon" ||
dbSetup === "supabase" ||
dbSetup === "singlestore-helios" ||
dbSetup === "d1" ||
dbSetup === "docker";

Expand Down
9 changes: 9 additions & 0 deletions apps/cli/src/helpers/setup/db-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { setupDockerCompose } from "../database-providers/docker-compose-setup";
import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
import { setupNeonPostgres } from "../database-providers/neon-setup";
import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup";
import { setupSingleStoreHelios } from "../database-providers/singlestore-helios-setup";
import { setupSupabase } from "../database-providers/supabase-setup";
import { setupTurso } from "../database-providers/turso-setup";

Expand Down Expand Up @@ -68,6 +69,12 @@ export async function setupDatabase(config: ProjectConfig) {
devDependencies: ["drizzle-kit"],
projectDir: serverDir,
});
} else if (database === "singlestore") {
await addPackageDependency({
dependencies: ["drizzle-orm", "mysql2"],
devDependencies: ["drizzle-kit"],
projectDir: serverDir,
});
}
} else if (orm === "mongoose") {
await addPackageDependency({
Expand All @@ -93,6 +100,8 @@ export async function setupDatabase(config: ProjectConfig) {
}
} else if (database === "mongodb" && dbSetup === "mongodb-atlas") {
await setupMongoDBAtlas(config);
} else if (database === "singlestore" && dbSetup === "singlestore-helios") {
await setupSingleStoreHelios(config);
}
} catch (error) {
s.stop(pc.red("Failed to set up database"));
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/prompts/database-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export async function getDBSetupChoice(
},
{ value: "none" as const, label: "None", hint: "Manual setup" },
];
} else if (databaseType === "singlestore") {
return "singlestore-helios";
} else {
return "none";
}
Expand Down
5 changes: 5 additions & 0 deletions apps/cli/src/prompts/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export async function getDatabaseChoice(
label: "MongoDB",
hint: "open-source NoSQL database that stores data in JSON-like documents called BSON",
});
databaseOptions.push({
value: "singlestore",
label: "SingleStore",
hint: "high-performance distributed SQL database for real-time analytics",
});
}

const response = await select<Database>({
Expand Down
3 changes: 2 additions & 1 deletion apps/cli/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import z from "zod";

export const DatabaseSchema = z
.enum(["none", "sqlite", "postgres", "mysql", "mongodb"])
.enum(["none", "sqlite", "postgres", "mysql", "mongodb", "singlestore"])
.describe("Database type");
export type Database = z.infer<typeof DatabaseSchema>;

Expand Down Expand Up @@ -70,6 +70,7 @@ export const DatabaseSetupSchema = z
"prisma-postgres",
"mongodb-atlas",
"supabase",
"singlestore-helios",
"d1",
"docker",
"none",
Expand Down
58 changes: 58 additions & 0 deletions apps/cli/src/utils/compatibility-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,64 @@ export function validateWorkersCompatibility(
"Docker setup (--db-setup docker) is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.",
);
}

if (
providedFlags.has("runtime") &&
options.runtime === "workers" &&
config.database === "singlestore"
) {
exitWithError(
"SingleStore is not supported on Cloudflare Workers. Use Bun or Node.js runtimes.",
);
}

if (
providedFlags.has("database") &&
config.database === "singlestore" &&
config.runtime === "workers"
) {
exitWithError(
"SingleStore is not supported on Cloudflare Workers. Use Bun or Node.js runtimes.",
);
}
}

export function validateSingleStoreCompatibility(
providedFlags: Set<string>,
options: CLIInput,
config: Partial<ProjectConfig>,
) {
if (
providedFlags.has("database") &&
options.database === "singlestore" &&
config.orm &&
config.orm !== "drizzle"
) {
exitWithError(
`SingleStore database requires Drizzle ORM. Current ORM: ${config.orm}. Please use '--orm drizzle' or choose a different database.`,
);
}

if (
providedFlags.has("orm") &&
config.orm &&
config.orm !== "drizzle" &&
config.database === "singlestore"
) {
exitWithError(
`ORM '${config.orm}' is not compatible with SingleStore database. SingleStore requires Drizzle ORM. Please use '--orm drizzle' or choose a different database.`,
);
}

if (
providedFlags.has("dbSetup") &&
options.dbSetup === "singlestore-helios" &&
config.database !== "singlestore"
) {
exitWithError(
`SingleStore Helios setup (--db-setup singlestore-helios) requires SingleStore database. Current database: ${config.database}. Please use '--database singlestore' or choose a different database setup.`,
);
}
}

export function coerceBackendPresets(config: Partial<ProjectConfig>) {
Expand Down
36 changes: 36 additions & 0 deletions apps/cli/src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
validateAddonsAgainstFrontends,
validateApiFrontendCompatibility,
validateExamplesCompatibility,
validateSingleStoreCompatibility,
validateWebDeployRequiresWebFrontend,
validateWorkersCompatibility,
} from "./utils/compatibility-rules";
Expand Down Expand Up @@ -247,6 +248,18 @@ export function processAndValidateFlags(
);
}

if (
providedFlags.has("database") &&
providedFlags.has("orm") &&
config.database === "singlestore" &&
config.orm &&
config.orm !== "drizzle"
) {
exitWithError(
"SingleStore database requires Drizzle ORM. Please use '--orm drizzle' or choose a different database.",
);
}

if (
providedFlags.has("database") &&
providedFlags.has("orm") &&
Expand Down Expand Up @@ -349,6 +362,28 @@ export function processAndValidateFlags(
);
}

if (
providedFlags.has("dbSetup") &&
(config.database ? providedFlags.has("database") : true) &&
config.dbSetup === "singlestore-helios" &&
config.database !== "singlestore"
) {
exitWithError(
"SingleStore Helios setup requires SingleStore database. Please use '--database singlestore' or choose a different setup.",
);
}

if (
providedFlags.has("database") &&
providedFlags.has("dbSetup") &&
config.database === "singlestore" &&
config.dbSetup === "none"
) {
exitWithError(
"SingleStore database requires SingleStore Helios setup. Please use '--db-setup singlestore-helios' or omit the --db-setup flag.",
);
}
Comment on lines +376 to +385
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not force Helios when database='singlestore' — conflicts with PR objective to allow 'none'.

The PR explicitly allows a basic/self-hosted 'none' option. This block prohibits it by erroring when dbSetup==='none'. Remove this constraint.

-  if (
-    providedFlags.has("database") &&
-    providedFlags.has("dbSetup") &&
-    config.database === "singlestore" &&
-    config.dbSetup === "none"
-  ) {
-    exitWithError(
-      "SingleStore database requires SingleStore Helios setup. Please use '--db-setup singlestore-helios' or omit the --db-setup flag.",
-    );
-  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
providedFlags.has("database") &&
providedFlags.has("dbSetup") &&
config.database === "singlestore" &&
config.dbSetup === "none"
) {
exitWithError(
"SingleStore database requires SingleStore Helios setup. Please use '--db-setup singlestore-helios' or omit the --db-setup flag.",
);
}
🤖 Prompt for AI Agents
In apps/cli/src/validation.ts around lines 376 to 385 the code currently rejects
the combination database==='singlestore' && dbSetup==='none', which contradicts
the PR goal to allow a basic/self-hosted "none" option; remove this constraint
by deleting the entire if-block that checks for providedFlags.has("database") &&
providedFlags.has("dbSetup") && config.database === "singlestore" &&
config.dbSetup === "none" (or change the condition so it no longer treats
dbSetup==='none' as an error), leaving no special-case error for SingleStore
when dbSetup is "none".


if (config.dbSetup === "d1") {
if (
(providedFlags.has("dbSetup") && providedFlags.has("database")) ||
Expand Down Expand Up @@ -396,6 +431,7 @@ export function processAndValidateFlags(
}

validateWorkersCompatibility(providedFlags, options, config);
validateSingleStoreCompatibility(providedFlags, options, config);

const hasWebFrontendFlag = (config.frontend ?? []).some((f) =>
isWebFrontend(f),
Expand Down
10 changes: 10 additions & 0 deletions apps/cli/templates/db/drizzle/singlestore/drizzle.config.ts.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from "drizzle-kit";

export default defineConfig({
schema: "./src/db/schema",
out: "./src/db/migrations",
dialect: "singlestore",
dbCredentials: {
url: process.env.DATABASE_URL || "",
},
});
Comment on lines +1 to +10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Load .env early and fail fast when DATABASE_URL is missing

drizzle-kit runs this file directly; without importing dotenv, DATABASE_URL often won’t be present. Falling back to "" hides config mistakes.

Apply this diff:

+import "dotenv/config";
 import { defineConfig } from "drizzle-kit";
 
-export default defineConfig({
-  schema: "./src/db/schema",
-  out: "./src/db/migrations",
-  dialect: "singlestore",
-  dbCredentials: {
-    url: process.env.DATABASE_URL || "",
-  },
-});
+const url = process.env.DATABASE_URL;
+if (!url) {
+  throw new Error("DATABASE_URL is not set. Please set it (SingleStore Helios requires TLS).");
+}
+
+export default defineConfig({
+  schema: "./src/db/schema",
+  out: "./src/db/migrations",
+  dialect: "singlestore",
+  dbCredentials: {
+    url,
+  },
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema",
out: "./src/db/migrations",
dialect: "singlestore",
dbCredentials: {
url: process.env.DATABASE_URL || "",
},
});
import "dotenv/config";
import { defineConfig } from "drizzle-kit";
const url = process.env.DATABASE_URL;
if (!url) {
throw new Error("DATABASE_URL is not set. Please set it (SingleStore Helios requires TLS).");
}
export default defineConfig({
schema: "./src/db/schema",
out: "./src/db/migrations",
dialect: "singlestore",
dbCredentials: {
url,
},
});
🤖 Prompt for AI Agents
In apps/cli/templates/db/drizzle/singlestore/drizzle.config.ts.hbs around lines
1 to 10, dotenv is not loaded before reading DATABASE_URL and the code silently
falls back to an empty string; load environment variables early (e.g., require
or import and call dotenv.config() at top of the file) and then validate
process.env.DATABASE_URL exists, throwing an error or exiting with a non-zero
status if it's missing so the process fails fast; finally set dbCredentials.url
to process.env.DATABASE_URL (no empty fallback).

6 changes: 6 additions & 0 deletions apps/cli/templates/db/drizzle/singlestore/src/db/index.ts.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import mysql from "mysql2/promise";
import { drizzle } from "drizzle-orm/singlestore";

const pool = mysql.createPool(process.env.DATABASE_URL || "");

export const db = drizzle({ client: pool });
Comment on lines +4 to +6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fail fast on missing DATABASE_URL and avoid empty-string fallback

Silently passing "" to createPool will defer failures to runtime and make debugging harder. Also, Helios requires TLS; ensure the env-provided URL includes ssl params or configure ssl in code.

Apply this diff to validate the env and add basic pool options (keeps URL-driven SSL; if you prefer code-driven SSL, I can provide that too):

-const pool = mysql.createPool(process.env.DATABASE_URL || "");
+const url = process.env.DATABASE_URL;
+if (!url) {
+  throw new Error("DATABASE_URL is not set. SingleStore Helios requires a valid TLS connection string.");
+}
+const pool = mysql.createPool({
+  uri: url,
+  waitForConnections: true,
+  connectionLimit: 10,
+  maxIdle: 10,
+  queueLimit: 0,
+  enableKeepAlive: true,
+});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const pool = mysql.createPool(process.env.DATABASE_URL || "");
export const db = drizzle({ client: pool });
const url = process.env.DATABASE_URL;
if (!url) {
throw new Error("DATABASE_URL is not set. SingleStore Helios requires a valid TLS connection string.");
}
const pool = mysql.createPool({
uri: url,
waitForConnections: true,
connectionLimit: 10,
maxIdle: 10,
queueLimit: 0,
enableKeepAlive: true,
});
export const db = drizzle({ client: pool });

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { singlestoreTable, varchar, bigint, boolean } from "drizzle-orm/singlestore-core";

export const todo = singlestoreTable("todo", {
id: bigint("id", { mode: "number" }).primaryKey().autoincrement(),
text: varchar("text", { length: 255 }).notNull(),
completed: boolean("completed").default(false).notNull(),
});
Loading