diff --git a/README.md b/README.md index f50c5b95..3ca90b45 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ npx create-better-t-stack@latest - 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, Ruler, Ultracite, Oxlint +- Addons: Turborepo, PWA, Tauri, Biome, Husky, Starlight, Fumadocs, Ruler, Ultracite, Oxlint, T3 Env - Examples: Todo, AI - DB Setup: Turso, Neon, Supabase, Prisma PostgreSQL, MongoDB Atlas, Cloudflare D1, Docker - Web Deploy: Cloudflare Workers diff --git a/apps/cli/README.md b/apps/cli/README.md index 6001e742..cb94a59b 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -41,7 +41,7 @@ Follow the prompts to configure your project or use the `--yes` flag for default | **Database Setup** | • Turso (SQLite)
• Cloudflare D1 (SQLite)
• Neon (PostgreSQL)
• Supabase (PostgreSQL)
• Prisma Postgres (via Prisma Accelerate)
• MongoDB Atlas
• None (manual setup) | | **Authentication** | Better-Auth (email/password, with more options coming soon) | | **Styling** | Tailwind CSS with shadcn/ui components | -| **Addons** | • PWA support
• Tauri (desktop applications)
• Starlight (documentation site)
• Biome (linting and formatting)
• Husky (Git hooks)
• Turborepo (optimized builds) | +| **Addons** | • PWA support
• Tauri (desktop applications)
• Starlight (documentation site)
• Biome (linting and formatting)
• Husky (Git hooks)
• Turborepo (optimized builds)
• T3 Env (Type-safe environment variables) | | **Examples** | • Todo app
• AI Chat interface (using Vercel AI SDK) | | **Developer Experience** | • Automatic Git initialization
• Package manager choice (npm, pnpm, bun)
• Automatic dependency installation | @@ -58,7 +58,7 @@ Options: --auth Include authentication --no-auth Exclude authentication --frontend Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-nativewind, native-unistyles, none) - --addons Additional addons (pwa, tauri, starlight, biome, husky, turborepo, fumadocs, ultracite, oxlint, none) + --addons Additional addons (pwa, tauri, starlight, biome, husky, turborepo, fumadocs, ultracite, oxlint, t3env, none) --examples Examples to include (todo, ai, none) --git Initialize git repository --no-git Skip git initialization @@ -192,7 +192,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. -- **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Husky, Turborepo). +- **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Husky, Turborepo, T3 Env). - **Examples 'none'**: Skips all example implementations (todo, AI chat). - **SvelteKit, Nuxt, and SolidJS** frontends are only compatible with oRPC API layer - **PWA support** requires React with TanStack Router, React Router, or SolidJS diff --git a/apps/cli/package.json b/apps/cli/package.json index 9579d8a3..c9e4997f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -36,7 +36,8 @@ "shadcn", "pwa", "tauri", - "biome" + "biome", + "t3env" ], "repository": { "type": "git", diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index e679d3e2..1505d5a4 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -14,7 +14,7 @@ export const DEFAULT_CONFIG_BASE = { database: "sqlite", orm: "drizzle", auth: "better-auth", - addons: ["turborepo"], + addons: ["turborepo", "t3env"], examples: [], git: true, install: true, @@ -156,6 +156,11 @@ export const dependencyVersionMap = { nitropack: "^2.12.4", dotenv: "^17.2.1", + + zod: "^4.0.14", + "@t3-oss/env-core": "^0.13.8", + "@t3-oss/env-nextjs": "^0.13.8", + "@t3-oss/env-nuxt": "^0.13.8", } as const; export type AvailableDependencies = keyof typeof dependencyVersionMap; @@ -171,5 +176,6 @@ export const ADDON_COMPATIBILITY: Record = { ruler: [], oxlint: [], fumadocs: [], + t3env: [], none: [], } as const; diff --git a/apps/cli/src/helpers/addons/addons-setup.ts b/apps/cli/src/helpers/addons/addons-setup.ts index 819c567a..49e39bae 100644 --- a/apps/cli/src/helpers/addons/addons-setup.ts +++ b/apps/cli/src/helpers/addons/addons-setup.ts @@ -9,6 +9,7 @@ import { getPackageExecutionCommand } from "../../utils/package-runner"; import { setupFumadocs } from "./fumadocs-setup"; import { setupVibeRules } from "./ruler-setup"; import { setupStarlight } from "./starlight-setup"; +import { setupT3Env } from "./t3env-setup"; import { setupTauri } from "./tauri-setup"; import { setupUltracite } from "./ultracite-setup"; import { addPwaToViteConfig } from "./vite-pwa-setup"; @@ -58,6 +59,11 @@ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")} ) { await setupTauri(config); } + + if (addons.includes("t3env")) { + await setupT3Env(config); + } + const hasUltracite = addons.includes("ultracite"); const hasBiome = addons.includes("biome"); const hasHusky = addons.includes("husky"); diff --git a/apps/cli/src/helpers/addons/t3env-setup.ts b/apps/cli/src/helpers/addons/t3env-setup.ts new file mode 100644 index 00000000..f83fa5c1 --- /dev/null +++ b/apps/cli/src/helpers/addons/t3env-setup.ts @@ -0,0 +1,64 @@ +import path from "node:path"; +import { spinner } from "@clack/prompts"; +import { consola } from "consola"; +import fs from "fs-extra"; +import pc from "picocolors"; +import type { ProjectConfig } from "../../types"; +import { addPackageDependency } from "../../utils/add-package-deps"; + +export async function setupT3Env(config: ProjectConfig) { + const { frontend, backend, projectDir } = config; + const s = spinner(); + + // T3 Env requires a backend to be present + if (backend === "none") { + s.stop(pc.yellow("T3 Env requires a backend to be configured")); + return; + } + + try { + s.start("Setting up T3 Env for type-safe environment variables..."); + + // Add dependencies to server + const serverDir = path.join(projectDir, "apps/server"); + if (await fs.pathExists(serverDir)) { + await addPackageDependency({ + dependencies: ["@t3-oss/env-core", "zod"], + projectDir: serverDir, + }); + } + + // Add framework-specific dependencies to web app + const webDir = path.join(projectDir, "apps/web"); + if (await fs.pathExists(webDir)) { + const hasNext = frontend.includes("next"); + const hasNuxt = frontend.includes("nuxt"); + + if (hasNext) { + await addPackageDependency({ + dependencies: ["@t3-oss/env-nextjs"], + projectDir: webDir, + }); + } else if (hasNuxt) { + // For Nuxt, we'll use the core package + await addPackageDependency({ + dependencies: ["@t3-oss/env-nuxt", "zod"], + projectDir: webDir, + }); + } else { + // For other frameworks, use the core package + await addPackageDependency({ + dependencies: ["@t3-oss/env-core", "zod"], + projectDir: webDir, + }); + } + } + + s.stop("T3 Env configured successfully!"); + } catch (error) { + s.stop(pc.red("Failed to set up T3 Env")); + if (error instanceof Error) { + consola.error(pc.red(error.message)); + } + } +} diff --git a/apps/cli/src/helpers/core/create-readme.ts b/apps/cli/src/helpers/core/create-readme.ts index 5cb8430a..0de04078 100644 --- a/apps/cli/src/helpers/core/create-readme.ts +++ b/apps/cli/src/helpers/core/create-readme.ts @@ -480,6 +480,8 @@ function generateFeaturesList( addonsList.push("- **Starlight** - Documentation site with Astro"); } else if (addon === "turborepo") { addonsList.push("- **Turborepo** - Optimized monorepo build system"); + } else if (addon === "t3env") { + addonsList.push("- **T3 Env** - Type-safe environment variables"); } } diff --git a/apps/cli/src/prompts/addons.ts b/apps/cli/src/prompts/addons.ts index 24b2aebf..c1548dd4 100644 --- a/apps/cli/src/prompts/addons.ts +++ b/apps/cli/src/prompts/addons.ts @@ -58,6 +58,10 @@ function getAddonDisplay(addon: Addons): { label: string; hint: string } { label = "Fumadocs"; hint = "Build excellent documentation site"; break; + case "t3env": + label = "T3 Env"; + hint = "Type-safe environment variables"; + break; default: label = addon; hint = `Add ${addon}`; @@ -69,7 +73,7 @@ function getAddonDisplay(addon: Addons): { label: string; hint: string } { const ADDON_GROUPS = { Documentation: ["starlight", "fumadocs"], Linting: ["biome", "oxlint", "ultracite"], - Other: ["ruler", "turborepo", "pwa", "tauri", "husky"], + Other: ["ruler", "turborepo", "pwa", "tauri", "husky", "t3env"], }; export async function getAddonsChoice( diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 62043688..c7e21d1a 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -48,6 +48,7 @@ export const AddonsSchema = z "fumadocs", "ultracite", "oxlint", + "t3env", "none", ]) .describe("Additional addons"); diff --git a/apps/cli/templates/addons/t3env/apps/server/next/next.config.ts b/apps/cli/templates/addons/t3env/apps/server/next/next.config.ts new file mode 100644 index 00000000..077e3c62 --- /dev/null +++ b/apps/cli/templates/addons/t3env/apps/server/next/next.config.ts @@ -0,0 +1 @@ +import "@/env/server"; \ No newline at end of file diff --git a/apps/cli/templates/addons/t3env/apps/server/next/src/env/server.ts.hbs b/apps/cli/templates/addons/t3env/apps/server/next/src/env/server.ts.hbs new file mode 100644 index 00000000..664c71a7 --- /dev/null +++ b/apps/cli/templates/addons/t3env/apps/server/next/src/env/server.ts.hbs @@ -0,0 +1,15 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod"; + +export const env = createEnv({ + server: { + DATABASE_URL: z.string().url(), + BETTER_AUTH_SECRET: z.string().min(1), + BETTER_AUTH_URL: z.string().url(), + CORS_ORIGIN: z.string().url(), + {{#if (includes examples 'ai')}} + GOOGLE_GENERATIVE_AI_API_KEY: z.string().min(1), + {{/if}} + }, + experimental__runtimeEnv: process.env, +}); \ No newline at end of file diff --git a/apps/cli/templates/addons/t3env/apps/web/next/src/env/client.ts.hbs b/apps/cli/templates/addons/t3env/apps/web/next/src/env/client.ts.hbs new file mode 100644 index 00000000..93523e35 --- /dev/null +++ b/apps/cli/templates/addons/t3env/apps/web/next/src/env/client.ts.hbs @@ -0,0 +1,17 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod"; + +export const env = createEnv({ + client: { + NEXT_PUBLIC_SERVER_URL: z.string().url(), + {{#if (eq backend 'convex')}} + NEXT_PUBLIC_CONVEX_URL: z.string().url(), + {{/if}} + }, + runtimeEnv: { + NEXT_PUBLIC_SERVER_URL: process.env.NEXT_PUBLIC_SERVER_URL, + {{#if (eq backend 'convex')}} + NEXT_PUBLIC_CONVEX_URL: process.env.NEXT_PUBLIC_CONVEX_URL, + {{/if}} + }, +}); \ No newline at end of file diff --git a/apps/cli/templates/addons/t3env/apps/web/next/src/next.config.ts.hbs b/apps/cli/templates/addons/t3env/apps/web/next/src/next.config.ts.hbs new file mode 100644 index 00000000..1a77485f --- /dev/null +++ b/apps/cli/templates/addons/t3env/apps/web/next/src/next.config.ts.hbs @@ -0,0 +1 @@ +import "@/env/client"; \ No newline at end of file diff --git a/apps/cli/templates/addons/t3env/apps/web/nuxt/src/env/client.ts.hbs b/apps/cli/templates/addons/t3env/apps/web/nuxt/src/env/client.ts.hbs new file mode 100644 index 00000000..443028d7 --- /dev/null +++ b/apps/cli/templates/addons/t3env/apps/web/nuxt/src/env/client.ts.hbs @@ -0,0 +1,11 @@ +import { createEnv } from "@t3-oss/env-nuxt"; +import { z } from "zod"; + +export const env = createEnv({ + client: { + NUXT_PUBLIC_SERVER_URL: z.string().url(), + {{#if (eq backend 'convex')}} + NUXT_PUBLIC_CONVEX_URL: z.string().url(), + {{/if}} + } +}); \ No newline at end of file diff --git a/apps/cli/templates/frontend/nuxt/nuxt.config.ts.hbs b/apps/cli/templates/frontend/nuxt/nuxt.config.ts.hbs index 03447fbc..a8873d3b 100644 --- a/apps/cli/templates/frontend/nuxt/nuxt.config.ts.hbs +++ b/apps/cli/templates/frontend/nuxt/nuxt.config.ts.hbs @@ -1,3 +1,5 @@ +import "./env"; + // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ compatibilityDate: 'latest', diff --git a/apps/web/content/docs/cli/options.mdx b/apps/web/content/docs/cli/options.mdx index a32c598b..eceb3a44 100644 --- a/apps/web/content/docs/cli/options.mdx +++ b/apps/web/content/docs/cli/options.mdx @@ -253,6 +253,7 @@ Additional features to include: - `ultracite`: Ultracite configuration - `oxlint`: Oxlint fast linting - `ruler`: Centralize your AI rules with Ruler +- `t3env`: Type-safe environment variables ```bash create-better-t-stack --addons pwa biome husky diff --git a/apps/web/content/docs/index.mdx b/apps/web/content/docs/index.mdx index af4b1ca3..80e0b2f5 100644 --- a/apps/web/content/docs/index.mdx +++ b/apps/web/content/docs/index.mdx @@ -114,7 +114,7 @@ Skip prompts and use the default stack: --database sqlite \ --orm drizzle \ --auth better-auth \ - --addons turborepo + --addons turborepo t3env ``` diff --git a/apps/web/public/icon/t3env.svg b/apps/web/public/icon/t3env.svg new file mode 100644 index 00000000..a7f9d598 --- /dev/null +++ b/apps/web/public/icon/t3env.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/apps/web/src/app/(home)/_components/npm-package.tsx b/apps/web/src/app/(home)/_components/npm-package.tsx index 246361e7..36904e8e 100644 --- a/apps/web/src/app/(home)/_components/npm-package.tsx +++ b/apps/web/src/app/(home)/_components/npm-package.tsx @@ -31,4 +31,4 @@ const NpmPackage = () => { ); }; -export default NpmPackage; +export default NpmPackage; \ No newline at end of file diff --git a/apps/web/src/app/(home)/analytics/_components/types.ts b/apps/web/src/app/(home)/analytics/_components/types.ts index 9428cc1b..eaa92068 100644 --- a/apps/web/src/app/(home)/analytics/_components/types.ts +++ b/apps/web/src/app/(home)/analytics/_components/types.ts @@ -369,13 +369,17 @@ export const addonsConfig = { label: "Starlight", color: "hsl(var(--chart-5))", }, + t3env: { + label: "T3 Env", + color: "hsl(var(--chart-6))", + }, turborepo: { label: "Turborepo", - color: "hsl(var(--chart-6))", + color: "hsl(var(--chart-7))", }, none: { label: "No Addons", - color: "hsl(var(--chart-7))", + color: "hsl(var(--chart-8))", }, } satisfies ChartConfig; diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 925a5382..d71fd6f5 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -35,6 +35,7 @@ export const metadata: Metadata = { "hono", "elysia", "turborepo", + "t3env", "trpc", "orpc", "turso", diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 5f0ec837..e1b15edf 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -526,6 +526,14 @@ export const TECH_OPTIONS: Record< color: "from-violet-500 to-violet-700", default: false, }, + { + id: "t3env", + name: "T3 Env", + description: "Type-safe environment variables", + icon: "/icon/t3env.svg", + color: "from-yellow-500 to-yellow-700", + default: true, + }, { id: "turborepo", name: "Turborepo", @@ -605,7 +613,7 @@ export const PRESET_TEMPLATES = [ dbSetup: "none", auth: "better-auth", packageManager: "bun", - addons: ["turborepo"], + addons: ["turborepo", "t3env"], examples: [], git: "true", install: "true", @@ -630,7 +638,7 @@ export const PRESET_TEMPLATES = [ dbSetup: "none", auth: "none", packageManager: "bun", - addons: ["turborepo"], + addons: ["turborepo", "t3env"], examples: ["todo"], git: "true", install: "true", @@ -655,7 +663,7 @@ export const PRESET_TEMPLATES = [ dbSetup: "none", auth: "better-auth", packageManager: "bun", - addons: ["turborepo"], + addons: ["turborepo", "t3env"], examples: [], git: "true", install: "true", @@ -680,7 +688,7 @@ export const PRESET_TEMPLATES = [ dbSetup: "none", auth: "better-auth", packageManager: "bun", - addons: ["turborepo"], + addons: ["turborepo", "t3env"], examples: [], git: "true", install: "true", @@ -705,7 +713,15 @@ export const PRESET_TEMPLATES = [ dbSetup: "turso", auth: "better-auth", packageManager: "bun", - addons: ["pwa", "biome", "husky", "tauri", "starlight", "turborepo"], + addons: [ + "pwa", + "biome", + "husky", + "tauri", + "starlight", + "turborepo", + "t3env", + ], examples: ["todo", "ai"], git: "true", install: "true", @@ -749,7 +765,7 @@ export const DEFAULT_STACK: StackState = { dbSetup: "none", auth: "better-auth", packageManager: "bun", - addons: ["turborepo"], + addons: ["turborepo", "t3env"], examples: [], git: "true", install: "true", diff --git a/apps/web/src/lib/stack-utils.ts b/apps/web/src/lib/stack-utils.ts index a5d5b8e9..997d4969 100644 --- a/apps/web/src/lib/stack-utils.ts +++ b/apps/web/src/lib/stack-utils.ts @@ -111,6 +111,7 @@ export function generateStackCommand(stack: StackState): string { "fumadocs", "oxlint", "ruler", + "t3env", ].includes(addon), ) .join(" ") || "none" diff --git a/demo.gif b/demo.gif index 65a7f9f9..618928ab 100644 Binary files a/demo.gif and b/demo.gif differ