Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 32 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,58 @@ A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with

![demo](https://cdn.jsdelivr.net/gh/amanvarshney01/create-better-t-stack/demo.gif)

## Philosophy

- Roll your own stack: you pick only the parts you need, nothing extra.
- Minimal templates: bare-bones scaffolds with zero bloat.
- Latest dependencies: always use current, stable versions by default.
- Free and open source: forever.

## Quick Start

```bash
# Using npm
npx create-better-t-stack@latest

# Using bun
# Using bun (recommended)
bun create better-t-stack@latest

# Using pnpm
pnpm create better-t-stack@latest

# Using npm
npx create-better-t-stack@latest
```

## Features

- **Zero-config setup** with interactive CLI wizard
- **End-to-end type safety** from database to frontend via tRPC
- **Modern stack** with React, Hono/Elysia, and TanStack libraries
- **Multi-platform** supporting web, mobile (Expo), and desktop applications
- **Database flexibility** with SQLite (Turso) or PostgreSQL options
- **ORM choice** between Drizzle or Prisma
- **Built-in authentication** with Better-Auth
- **Optional PWA support** for installable web applications
- **Desktop app capabilities** with Tauri integration
- **Monorepo architecture** powered by Turborepo
- Frontend: React (TanStack Router, React Router, TanStack Start), Next.js, Nuxt, Svelte, Solid, React Native (NativeWind/Unistyles), or none
- Backend: Hono, Express, Fastify, Elysia, Next API Routes, Convex, or none
- API: tRPC or oRPC (or none)
- Runtime: Bun, Node.js, or Cloudflare Workers
- Databases: SQLite, PostgreSQL, MySQL, MongoDB (or none)
- ORMs: Drizzle, Prisma, Mongoose (or none)
- Auth: Better-Auth (optional)
- Addons: Turborepo, PWA, Tauri, Biome, Husky, Starlight, Fumadocs, Ultracite, Oxlint
- Examples: Todo, AI
- DB Setup: Turso, Neon, Supabase, Prisma PostgreSQL, MongoDB Atlas, Cloudflare D1, Docker
- Web Deploy: Cloudflare Workers

Type safety end-to-end, clean monorepo layout, and zero lock-in: you choose only what you need.

## Repository Structure

This repository is organized as a monorepo containing:

- **CLI**: [`create-better-t-stack`](apps/cli) - The scaffolding CLI tool
- **Documentation**: [`web`](apps/web) - Official website and documentation
- **CLI**: [`apps/cli`](apps/cli) - The scaffolding CLI tool
- **Documentation**: [`apps/web`](apps/web) - Official website and documentation

## Documentation

Visit [better-t-stack.dev](https://better-t-stack.dev) for full documentation, guides, and examples.
Visit [better-t-stack.dev](https://better-t-stack.dev) for full documentation, guides, and examples. You can also use the visual Stack Builder at `https://better-t-stack.dev/new` to generate a command for your stack.

## Development

```bash
# Clone the repository
git clone https://github.com/better-t-stack/create-better-t-stack.git
git clone https://github.com/AmanVarshney01/create-better-t-stack.git

# Install dependencies
bun install
Expand All @@ -65,7 +75,10 @@ bun dev:web

## Want to contribute?

Just fork the repository and submit a pull request!
Please read the Contribution Guide first and open an issue before starting new features to ensure alignment with project goals.

- Docs: [`Contributing`](/apps/web/content/docs/contributing.mdx)
- Repo guide: [`.github/CONTRIBUTING.md`](.github/CONTRIBUTING.md)

## Star History

Expand Down
23 changes: 10 additions & 13 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with
Run without installing globally:

```bash
# Using npm
npx create-better-t-stack@latest

# Using bun
# Using bun (recommended)
bun create better-t-stack@latest

# Using pnpm
pnpm create better-t-stack@latest

# Using npm
npx create-better-t-stack@latest
```

Follow the prompts to configure your project or use the `--yes` flag for defaults.
Expand Down Expand Up @@ -58,14 +58,15 @@ Options:
--auth Include authentication
--no-auth Exclude authentication
--frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-nativewind, native-unistyles, none)
--addons <types...> Additional addons (pwa, tauri, starlight, biome, husky, turborepo, none)
--addons <types...> Additional addons (pwa, tauri, starlight, biome, husky, turborepo, fumadocs, ultracite, oxlint, none)
--examples <types...> Examples to include (todo, ai, none)
--git Initialize git repository
--no-git Skip git initialization
--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, none)
--db-setup <setup> Database setup (turso, d1, neon, supabase, prisma-postgres, mongodb-atlas, 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)
--api <type> API type (trpc, orpc, none)
Expand All @@ -84,7 +85,7 @@ This CLI collects anonymous usage data to help improve the tool. The data collec

### Disabling Telemetry

You can disable telemetry by setting the `BTS_TELEMETRY` environment variable:
You can disable telemetry by setting the `BTS_TELEMETRY_DISABLED` environment variable:

```bash
# Disable telemetry for a single run
Expand All @@ -94,10 +95,6 @@ BTS_TELEMETRY_DISABLED=1 npx create-better-t-stack my-app
export BTS_TELEMETRY_DISABLED=1
```

### Development

During development, telemetry is automatically disabled when `NODE_ENV=development`.

## Examples

Create a project with default configuration:
Expand All @@ -118,10 +115,10 @@ Create a project with Elysia backend and Node.js runtime:
npx create-better-t-stack my-app --backend elysia --runtime node
```

Create a project with multiple frontend options:
Create a project with multiple frontend options (one web + one native):

```bash
npx create-better-t-stack my-app --frontend tanstack-router native
npx create-better-t-stack my-app --frontend tanstack-router native-nativewind
```

Create a project with examples:
Expand Down
11 changes: 0 additions & 11 deletions apps/cli/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,3 @@ export const ADDON_COMPATIBILITY: Record<Addons, readonly Frontend[]> = {
fumadocs: [],
none: [],
} as const;

// TODO: need to refactor this
export const WEB_FRAMEWORKS: readonly Frontend[] = [
"tanstack-router",
"react-router",
"tanstack-start",
"next",
"nuxt",
"svelte",
"solid",
];
61 changes: 23 additions & 38 deletions apps/cli/src/prompts/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import type { API, Backend, Frontend } from "../types";
import { allowedApisForFrontends } from "../utils/compatibility-rules";

export async function getApiChoice(
Api?: API | undefined,
Expand All @@ -11,46 +12,30 @@ export async function getApiChoice(
return "none";
}

if (Api) return Api;
const allowed = allowedApisForFrontends(frontend ?? []);

const includesNuxt = frontend?.includes("nuxt");
const includesSvelte = frontend?.includes("svelte");
const includesSolid = frontend?.includes("solid");

let apiOptions = [
{
value: "trpc" as const,
label: "tRPC",
hint: "End-to-end typesafe APIs made easy",
},
{
value: "orpc" as const,
label: "oRPC",
hint: "End-to-end type-safe APIs that adhere to OpenAPI standards",
},
{
value: "none" as const,
label: "None",
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)",
},
];

if (includesNuxt || includesSvelte || includesSolid) {
apiOptions = [
{
value: "orpc" as const,
label: "oRPC",
hint: `End-to-end type-safe APIs (Recommended for ${
includesNuxt ? "Nuxt" : includesSvelte ? "Svelte" : "Solid"
} frontend)`,
},
{
value: "none" as const,
label: "None",
hint: "No API layer",
},
];
if (Api) {
return allowed.includes(Api) ? Api : allowed[0];
}
const apiOptions = allowed.map((a) =>
a === "trpc"
? {
value: "trpc" as const,
label: "tRPC",
hint: "End-to-end typesafe APIs made easy",
}
: a === "orpc"
? {
value: "orpc" as const,
label: "oRPC",
hint: "End-to-end type-safe APIs that adhere to OpenAPI standards",
}
: {
value: "none" as const,
label: "None",
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)",
},
);

const apiType = await select<API>({
message: "Select API type",
Expand Down
22 changes: 16 additions & 6 deletions apps/cli/src/prompts/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { cancel, isCancel, multiselect } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import type { API, Backend, Database, Examples, Frontend } from "../types";
import {
isExampleAIAllowed,
isExampleTodoAllowed,
} from "../utils/compatibility-rules";

export async function getExamplesChoice(
examples?: Examples[],
Expand Down Expand Up @@ -30,27 +34,33 @@ export async function getExamplesChoice(
if (noFrontendSelected) return [];

let response: Examples[] | symbol = [];
const options: { value: Examples; label: string; hint: string }[] = [
{
const options: { value: Examples; label: string; hint: string }[] = [];

if (isExampleTodoAllowed(backend, database)) {
options.push({
value: "todo" as const,
label: "Todo App",
hint: "A simple CRUD example app",
},
];
});
}

if (backend !== "elysia" && !frontends?.includes("solid")) {
if (isExampleAIAllowed(backend, frontends ?? [])) {
options.push({
value: "ai" as const,
label: "AI Chat",
hint: "A simple AI chat interface using AI SDK",
});
}

if (options.length === 0) return [];

response = await multiselect<Examples>({
message: "Include examples",
options: options,
required: false,
initialValues: DEFAULT_CONFIG.examples,
initialValues: DEFAULT_CONFIG.examples?.filter((ex) =>
options.some((o) => o.value === ex),
),
});

if (isCancel(response)) {
Expand Down
10 changes: 4 additions & 6 deletions apps/cli/src/prompts/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { cancel, isCancel, multiselect, select } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import type { Backend, Frontend } from "../types";
import { isFrontendAllowedWithBackend } from "../utils/compatibility-rules";

export async function getFrontendChoice(
frontendOptions?: Frontend[],
Expand Down Expand Up @@ -73,12 +74,9 @@ export async function getFrontendChoice(
},
];

const webOptions = allWebOptions.filter((option) => {
if (backend === "convex") {
return option.value !== "solid";
}
return true;
});
const webOptions = allWebOptions.filter((option) =>
isFrontendAllowedWithBackend(option.value, backend),
);

const webFramework = await select<Frontend>({
message: "Choose web",
Expand Down
15 changes: 13 additions & 2 deletions apps/cli/src/prompts/project-name.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import path from "node:path";
import { cancel, isCancel, text } from "@clack/prompts";
import consola from "consola";
import fs from "fs-extra";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import { ProjectNameSchema } from "../types";

function isPathWithinCwd(targetPath: string): boolean {
const resolved = path.resolve(targetPath);
const rel = path.relative(process.cwd(), resolved);
return !rel.startsWith("..") && !path.isAbsolute(rel);
}

function validateDirectoryName(name: string): string | undefined {
if (name === ".") return undefined;

Expand All @@ -23,7 +30,11 @@ export async function getProjectName(initialName?: string): Promise<string> {
const finalDirName = path.basename(initialName);
const validationError = validateDirectoryName(finalDirName);
if (!validationError) {
return initialName;
const projectDir = path.resolve(process.cwd(), initialName);
if (isPathWithinCwd(projectDir)) {
return initialName;
}
consola.error(pc.red("Project path must be within current directory"));
}
}

Expand Down Expand Up @@ -56,7 +67,7 @@ export async function getProjectName(initialName?: string): Promise<string> {

if (nameToUse !== ".") {
const projectDir = path.resolve(process.cwd(), nameToUse);
if (!projectDir.startsWith(process.cwd())) {
if (!isPathWithinCwd(projectDir)) {
return "Project path must be within current directory";
}
}
Expand Down
3 changes: 2 additions & 1 deletion apps/cli/src/prompts/web-deploy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG, WEB_FRAMEWORKS } from "../constants";
import { DEFAULT_CONFIG } from "../constants";
import type { Backend, Frontend, Runtime, WebDeploy } from "../types";
import { WEB_FRAMEWORKS } from "../utils/compatibility";

function hasWebFrontend(frontends: Frontend[]): boolean {
return frontends.some((f) => WEB_FRAMEWORKS.includes(f));
Expand Down
6 changes: 1 addition & 5 deletions apps/cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ export type Backend = z.infer<typeof BackendSchema>;

export const RuntimeSchema = z
.enum(["bun", "node", "workers", "none"])
.describe(
"Runtime environment (workers only available with hono backend and drizzle orm)",
);
.describe("Runtime environment");
export type Runtime = z.infer<typeof RuntimeSchema>;

export const FrontendSchema = z
Expand Down Expand Up @@ -176,5 +174,3 @@ export interface BetterTStackConfig {
api: API;
webDeploy: WebDeploy;
}

export type AvailablePackageManagers = "npm" | "pnpm" | "bun";
Loading