Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/amanvarshney01/create-better-t-stack/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Better-T-Stack uses a flexible template system powered by Handlebars to generate project files based on your configuration. Templates live in packages/template-generator/templates/ and are processed at build time to create your project structure.

Template Location

All templates are stored in the monorepo at:
packages/template-generator/templates/
├── addons/           # Addon-specific templates (PWA, Tauri, etc.)
├── api/              # tRPC and oRPC setups
├── auth/             # Better Auth and Clerk
├── backend/          # Server frameworks (Hono, Express, etc.)
├── base/             # Root monorepo files
├── db/               # Database and ORM schemas
├── db-setup/         # Provider-specific setup
├── examples/         # Example apps (todo, AI)
├── extras/           # Additional utilities
├── frontend/         # Web and native frontends
├── packages/         # Shared package templates
└── payments/         # Payment integrations

Handlebars Syntax

Templates use .hbs extension and support Handlebars expressions:

Basic Variables

// package.json.hbs
{
  "name": "{{projectName}}",
  "version": "0.0.1",
  "packageManager": "{{packageManager}}"
}
With config { projectName: "my-app", packageManager: "bun" }, this generates:
{
  "name": "my-app",
  "version": "0.0.1",
  "packageManager": "bun"
}

Conditionals

Templates use custom helpers for conditional logic:
{{#if (eq backend "hono")}}
import { Hono } from "hono";
{{else if (eq backend "express")}}
import express from "express";
{{else if (eq backend "fastify")}}
import fastify from "fastify";
{{/if}}

Template Helpers

Custom helpers are registered in packages/template-generator/src/core/template-processor.ts:6-10:
Handlebars.registerHelper("eq", (a, b) => a === b);
Handlebars.registerHelper("ne", (a, b) => a !== b);
Handlebars.registerHelper("and", (...args) => args.slice(0, -1).every(Boolean));
Handlebars.registerHelper("or", (...args) => args.slice(0, -1).some(Boolean));
Handlebars.registerHelper("includes", (arr, val) => Array.isArray(arr) && arr.includes(val));
These helpers must be called with parentheses: {{#if (eq orm "drizzle")}} not {{#if eq orm "drizzle"}}

Real Template Examples

Complex Conditional Logic

From packages/template-generator/templates/packages/infra/alchemy.run.ts.hbs:1-26:
import alchemy from "alchemy";
{{#if (eq webDeploy "cloudflare")}}
{{#if (includes frontend "next")}}
import { Nextjs } from "alchemy/cloudflare";
{{else if (includes frontend "nuxt")}}
import { Nuxt } from "alchemy/cloudflare";
{{else if (includes frontend "svelte")}}
import { SvelteKit } from "alchemy/cloudflare";
{{else if (includes frontend "tanstack-start")}}
import { TanStackStart } from "alchemy/cloudflare";
{{else if (includes frontend "tanstack-router")}}
import { Vite } from "alchemy/cloudflare";
{{else if (includes frontend "react-router")}}
import { ReactRouter } from "alchemy/cloudflare";
{{else if (includes frontend "solid")}}
import { Vite } from "alchemy/cloudflare";
{{else if (includes frontend "astro")}}
import { Astro } from "alchemy/cloudflare";
{{/if}}
{{/if}}
{{#if (eq serverDeploy "cloudflare")}}
import { Worker } from "alchemy/cloudflare";
{{/if}}
{{#if (and (or (eq serverDeploy "cloudflare") (and (eq webDeploy "cloudflare") (eq backend "self"))) (eq dbSetup "d1"))}}
import { D1Database } from "alchemy/cloudflare";
{{/if}}
import { config } from "dotenv";
This template generates different imports based on deployment targets and frontend choices, demonstrating the power of conditional templating.

Nested Conditionals with Multiple Variables

From the same file (lines 54-98), showing environment variable bindings:
{{#if (includes frontend "next")}}
export const web = await Nextjs("web", {
  cwd: "../../apps/web",
  bindings: {
    {{#if (eq backend "convex")}}
    NEXT_PUBLIC_CONVEX_URL: alchemy.env.NEXT_PUBLIC_CONVEX_URL!,
    {{#if (eq auth "better-auth")}}
    NEXT_PUBLIC_CONVEX_SITE_URL: alchemy.env.NEXT_PUBLIC_CONVEX_SITE_URL!,
    {{/if}}
    {{else if (ne backend "self")}}
    NEXT_PUBLIC_SERVER_URL: alchemy.env.NEXT_PUBLIC_SERVER_URL!,
    {{/if}}
    {{#if (eq dbSetup "d1")}}
    DB: db,
    {{else if (ne database "none")}}
    DATABASE_URL: alchemy.secret.env.DATABASE_URL!,
    {{/if}}
    {{#if (ne backend "convex")}}
    CORS_ORIGIN: alchemy.env.CORS_ORIGIN!,
    {{#if (eq auth "better-auth")}}
    BETTER_AUTH_SECRET: alchemy.secret.env.BETTER_AUTH_SECRET!,
    BETTER_AUTH_URL: alchemy.env.BETTER_AUTH_URL!,
    {{/if}}
    {{/if}}
    {{#if (eq auth "clerk")}}
    CLERK_SECRET_KEY: alchemy.secret.env.CLERK_SECRET_KEY!,
    {{/if}}
    {{#if (and (includes examples "ai") (ne backend "convex"))}}
    GOOGLE_GENERATIVE_AI_API_KEY: alchemy.secret.env.GOOGLE_GENERATIVE_AI_API_KEY!,
    {{/if}}
    {{#if (eq payments "polar")}}
    POLAR_ACCESS_TOKEN: alchemy.secret.env.POLAR_ACCESS_TOKEN!,
    POLAR_SUCCESS_URL: alchemy.env.POLAR_SUCCESS_URL!,
    {{/if}}
  },
});
{{/if}}

File Naming Conventions

.hbs Extension

Files ending in .hbs are processed as Handlebars templates. The extension is removed in the output:
package.json.hbs  →  package.json
tsconfig.json.hbs →  tsconfig.json

Special Filenames

Certain filenames are transformed:
// packages/template-generator/src/core/template-processor.ts:20-28
export function transformFilename(filename: string): string {
  let result = filename.endsWith(".hbs") ? filename.slice(0, -4) : filename;
  
  const basename = result.split("/").pop() || result;
  if (basename === "_gitignore") result = result.replace(/_gitignore$/, ".gitignore");
  else if (basename === "_npmrc") result = result.replace(/_npmrc$/, ".npmrc");
  
  return result;
}
  • _gitignore.gitignore
  • _npmrc.npmrc

Escaping Handlebars

When template files need to contain literal {{ }} syntax (common in Vue, JSX, or other template languages), escape the opening braces:
<!-- From packages/template-generator/templates/frontend/nuxt/app/pages/index.vue.hbs -->
<template>
  <div>
    <!-- This won't be processed by Handlebars -->
    <h1>\{{ title }}</h1>
  </div>
</template>
The \{{ becomes {{ in the output.

Template Processing Flow

1

Configuration received

User selections are converted to ProjectConfig object with all stack choices
2

Template selection

CLI determines which template directories to include based on config (e.g., if database: "postgres" and orm: "drizzle", include templates/db/drizzle/postgres/)
3

File tree generation

Templates are read, processed through Handlebars with config as context, and assembled into virtual file tree
4

Filesystem write

Virtual tree is written to disk at the project directory

Code Reference

From apps/cli/src/helpers/core/create-project.ts:52-67:
// Generate virtual project using Result-based API
const tree = yield* Result.await(
  generate({
    config: options,
    templates: EMBEDDED_TEMPLATES,
    version: getLatestCLIVersion(),
  }).then((result) =>
    result.mapError(
      (e) =>
        new ProjectCreationError({
          phase: e.phase || "template-generation",
          message: e.message,
          cause: e,
        }),
    ),
  ),
);

// Write tree to filesystem using Result-based API
yield* Result.await(
  writeTree(tree, projectDir).then((result) =>
    result.mapError(
      (e) =>
        new ProjectCreationError({
          phase: "file-writing",
          message: e.message,
          cause: e,
        }),
    ),
  ),
);

Available Context

Templates receive a ProjectConfig object with these fields:
projectName
string
Project name
projectDir
string
Absolute path to project directory
relativePath
string
Relative path from current directory
frontend
array
Frontend frameworks (e.g., ["next"], ["tanstack-router"])
backend
string
Backend framework: hono, express, fastify, elysia, convex, self, none
runtime
string
Runtime: bun, node, workers, none
database
string
Database: sqlite, postgres, mysql, mongodb, none
orm
string
ORM: drizzle, prisma, mongoose, none
api
string
API layer: trpc, orpc, none
auth
string
Auth provider: better-auth, clerk, none
payments
string
Payment integration: polar, none
addons
array
Addons: ["turborepo", "pwa"], etc.
examples
array
Examples: ["todo", "ai"], etc.
dbSetup
string
Database hosting: turso, neon, docker, etc.
webDeploy
string
Web deployment: cloudflare, none
serverDeploy
string
Server deployment: cloudflare, none
packageManager
string
Package manager: npm, pnpm, bun
git
boolean
Initialize git repository
install
boolean
Install dependencies

Template Organization Best Practices

Share common logic across templates using Handlebars partials or by extracting to base templates.
Prefer checking the actual config value rather than complex combinations:Good:
{{#if (eq database "postgres")}}
Avoid:
{{#if (or (eq dbSetup "neon") (eq dbSetup "supabase") (eq dbSetup "prisma-postgres"))}}
Add comments to explain non-obvious conditional chains:
{{!-- Only include D1 bindings if deploying to Cloudflare and using D1 --}}
{{#if (and (or (eq serverDeploy "cloudflare") (and (eq webDeploy "cloudflare") (eq backend "self"))) (eq dbSetup "d1"))}}
Ensure templates work for all valid combinations of config values. The CLI includes tests in apps/cli/test/basic-configurations.test.ts.

Binary Files

Binary files (images, fonts, etc.) are detected and copied without template processing:
// packages/template-generator/src/core/template-processor.ts:16-18
export function isBinaryFile(filePath: string): boolean {
  return isBinaryPath(filePath);
}
Examples:
  • templates/addons/pwa/apps/web/next/public/favicon/apple-touch-icon.png
  • templates/addons/pwa/apps/web/vite/public/logo.png

Debugging Templates

To see generated output during development:
bun create better-t-stack my-app --verbose

Project Structure

See what templates generate

Contributing

Add new templates to Better-T-Stack

Build docs developers (and LLMs) love