better-ts-stack 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +16 -8
  2. package/dist/args.js +2 -1
  3. package/dist/builder/buildPlanner.js +7 -1
  4. package/dist/builder/configGenerator.js +28 -3
  5. package/dist/builder/index.js +2 -1
  6. package/dist/builder/readmeGenerator.js +2 -2
  7. package/dist/output/nextSteps.js +2 -4
  8. package/dist/prompts/stack.js +3 -5
  9. package/dist/types/index.js +1 -1
  10. package/package.json +1 -1
  11. package/templates/backend/express/src/env.ts.hbs +13 -2
  12. package/templates/backend/express/src/index.ts.hbs +31 -2
  13. package/templates/backend/nestjs/src/app.module.ts.hbs +4 -1
  14. package/templates/backend/nestjs/src/env.ts.hbs +13 -2
  15. package/templates/backend/nestjs/src/health/health.controller.ts.hbs +7 -0
  16. package/templates/backend/nestjs/src/{main.ts → main.ts.hbs} +11 -0
  17. package/templates/frontend/nextjs/app/globals.css +119 -79
  18. package/templates/frontend/nextjs/app/layout.tsx +13 -11
  19. package/templates/frontend/nextjs/app/page.tsx.hbs +63 -43
  20. package/templates/frontend/nextjs/components/ui/alert.tsx +75 -0
  21. package/templates/frontend/nextjs/components/ui/badge.tsx +51 -0
  22. package/templates/frontend/nextjs/components/ui/button.tsx +33 -27
  23. package/templates/frontend/nextjs/components/ui/card.tsx +76 -34
  24. package/templates/frontend/nextjs/components/ui/field.tsx +203 -32
  25. package/templates/frontend/nextjs/components/ui/input.tsx +8 -7
  26. package/templates/frontend/nextjs/components/ui/label.tsx +19 -0
  27. package/templates/frontend/nextjs/components/ui/separator.tsx +24 -0
  28. package/templates/frontend/nextjs/components/ui/skeleton.tsx +13 -0
  29. package/templates/frontend/nextjs/components/ui/spinner.tsx +10 -0
  30. package/templates/frontend/nextjs/components.json +11 -5
  31. package/templates/frontend/nextjs/config.json +4 -3
  32. package/templates/frontend/nextjs/lib/utils.ts +1 -6
  33. package/templates/frontend/tanstack/components.json +12 -5
  34. package/templates/frontend/tanstack/config.json +5 -2
  35. package/templates/frontend/tanstack/src/components/ui/alert.tsx +75 -0
  36. package/templates/frontend/tanstack/src/components/ui/badge.tsx +51 -0
  37. package/templates/frontend/tanstack/src/components/ui/button.tsx +33 -27
  38. package/templates/frontend/tanstack/src/components/ui/card.tsx +76 -34
  39. package/templates/frontend/tanstack/src/components/ui/field.tsx +201 -32
  40. package/templates/frontend/tanstack/src/components/ui/input.tsx +8 -7
  41. package/templates/frontend/tanstack/src/components/ui/label.tsx +17 -0
  42. package/templates/frontend/tanstack/src/components/ui/separator.tsx +24 -0
  43. package/templates/frontend/tanstack/src/components/ui/skeleton.tsx +13 -0
  44. package/templates/frontend/tanstack/src/components/ui/spinner.tsx +10 -0
  45. package/templates/frontend/tanstack/src/lib/utils.ts +1 -6
  46. package/templates/frontend/tanstack/src/routes/index.tsx.hbs +64 -45
  47. package/templates/frontend/tanstack/src/styles/app.css +100 -55
  48. package/templates/modules/agents/AGENTS.md.hbs +29 -21
  49. package/templates/modules/auth/express/config.json +12 -11
  50. package/templates/modules/auth/express/src/lib/auth.ts.hbs +143 -0
  51. package/templates/modules/auth/express/src/lib/email.ts +37 -0
  52. package/templates/modules/auth/express/src/middleware/requireAuth.ts +29 -0
  53. package/templates/modules/auth/express/src/routes/me.ts +13 -0
  54. package/templates/modules/auth/nestjs/config.json +13 -11
  55. package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +64 -14
  56. package/templates/modules/auth/nestjs/src/auth/auth.ts.hbs +152 -0
  57. package/templates/modules/auth/nestjs/src/auth/email.ts +37 -0
  58. package/templates/modules/auth/nestjs/src/me/me.controller.ts +15 -0
  59. package/templates/modules/auth/nextjs/app/dashboard/page.tsx +101 -45
  60. package/templates/modules/auth/nextjs/app/forgot-password/page.tsx +11 -0
  61. package/templates/modules/auth/nextjs/app/reset-password/page.tsx +17 -0
  62. package/templates/modules/auth/nextjs/app/sign-in/page.tsx +23 -17
  63. package/templates/modules/auth/nextjs/app/sign-up/page.tsx +25 -19
  64. package/templates/modules/auth/nextjs/components/auth/change-password-form.tsx +125 -0
  65. package/templates/modules/auth/nextjs/components/auth/email-verification-card.tsx +91 -0
  66. package/templates/modules/auth/nextjs/components/auth/forgot-password-form.tsx +133 -0
  67. package/templates/modules/auth/nextjs/components/auth/reset-password-form.tsx +155 -0
  68. package/templates/modules/auth/nextjs/components/auth/sign-in-form.tsx +46 -32
  69. package/templates/modules/auth/nextjs/components/auth/sign-out-button.tsx +6 -5
  70. package/templates/modules/auth/nextjs/components/auth/sign-up-form.tsx +36 -30
  71. package/templates/modules/auth/nextjs/components/auth/social-buttons.tsx +73 -0
  72. package/templates/modules/auth/nextjs/config.json +8 -2
  73. package/templates/modules/auth/nextjs/lib/auth-schema.ts +27 -2
  74. package/templates/modules/auth/nextjs/lib/auth.ts.hbs +65 -0
  75. package/templates/modules/auth/nextjs/lib/email.ts +43 -0
  76. package/templates/modules/auth/nextjs/lib/session.ts +12 -0
  77. package/templates/modules/auth/tanstack/config.json +9 -3
  78. package/templates/modules/auth/tanstack/src/components/auth/change-password-form.tsx +123 -0
  79. package/templates/modules/auth/tanstack/src/components/auth/email-verification-card.tsx +89 -0
  80. package/templates/modules/auth/tanstack/src/components/auth/forgot-password-form.tsx +131 -0
  81. package/templates/modules/auth/tanstack/src/components/auth/reset-password-form.tsx +149 -0
  82. package/templates/modules/auth/tanstack/src/components/auth/sign-in-form.tsx +44 -35
  83. package/templates/modules/auth/tanstack/src/components/auth/sign-out-button.tsx +7 -6
  84. package/templates/modules/auth/tanstack/src/components/auth/sign-up-form.tsx +37 -34
  85. package/templates/modules/auth/tanstack/src/components/auth/social-buttons.tsx +71 -0
  86. package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +7 -1
  87. package/templates/modules/auth/tanstack/src/lib/auth-schema.ts +27 -2
  88. package/templates/modules/auth/tanstack/src/lib/auth.ts.hbs +61 -1
  89. package/templates/modules/auth/tanstack/src/lib/email.ts +43 -0
  90. package/templates/modules/auth/tanstack/src/routeTree.gen.ts +61 -3
  91. package/templates/modules/auth/tanstack/src/routes/dashboard.tsx +40 -32
  92. package/templates/modules/auth/tanstack/src/routes/forgot-password.tsx +17 -0
  93. package/templates/modules/auth/tanstack/src/routes/reset-password.tsx +25 -0
  94. package/templates/modules/auth/tanstack/src/routes/sign-in.tsx +19 -12
  95. package/templates/modules/auth/tanstack/src/routes/sign-up.tsx +21 -14
  96. package/templates/modules/drizzle/express/src/lib/schema.ts.hbs +90 -0
  97. package/templates/modules/drizzle/nestjs/src/database/schema.ts.hbs +90 -0
  98. package/templates/modules/mongoose/nestjs/config.json +2 -1
  99. package/templates/modules/prisma/express/prisma/schema.prisma.hbs +92 -0
  100. package/templates/modules/prisma/nestjs/prisma/schema.prisma.hbs +91 -0
  101. package/templates/modules/rate-limit/express/src/middleware/rateLimit.ts +0 -8
  102. package/templates/modules/auth/express/src/controllers/authController.ts.hbs +0 -116
  103. package/templates/modules/auth/express/src/lib/jwt.ts.hbs +0 -27
  104. package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +0 -25
  105. package/templates/modules/auth/express/src/routes/auth.ts.hbs +0 -27
  106. package/templates/modules/auth/express/src/services/userStore.ts.hbs +0 -117
  107. package/templates/modules/auth/nestjs/src/auth/auth.controller.ts.hbs +0 -61
  108. package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +0 -36
  109. package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +0 -35
  110. package/templates/modules/auth/nestjs/src/auth/auth.service.ts +0 -70
  111. package/templates/modules/auth/nestjs/src/auth/jwt.ts +0 -27
  112. package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +0 -144
  113. package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +0 -16
  114. package/templates/modules/drizzle/express/src/lib/schema.ts +0 -12
  115. package/templates/modules/drizzle/nestjs/src/database/schema.ts +0 -12
  116. package/templates/modules/prisma/express/prisma/schema.prisma +0 -24
  117. package/templates/modules/prisma/nestjs/prisma/schema.prisma +0 -23
@@ -1,9 +1,16 @@
1
1
  import { createFileRoute{{#if useAuth}}, Link{{/if}} } from "@tanstack/react-router";
2
2
  import { ArrowRight } from "lucide-react";
3
3
 
4
- import { buttonVariants } from "~/components/ui/button";
5
- import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
6
- import { cn } from "~/lib/utils";
4
+ import { Alert, AlertDescription } from "~/components/ui/alert";
5
+ import { Badge } from "~/components/ui/badge";
6
+ import { Button } from "~/components/ui/button";
7
+ import {
8
+ Card,
9
+ CardContent,
10
+ CardFooter,
11
+ CardHeader,
12
+ CardTitle,
13
+ } from "~/components/ui/card";
7
14
 
8
15
  export const Route = createFileRoute("/")({
9
16
  component: Home,
@@ -13,7 +20,7 @@ const featureList = [
13
20
  "TanStack Start SSR",
14
21
  "TanStack Router and Query",
15
22
  "React 19 and TypeScript",
16
- "Tailwind CSS v4",
23
+ "Tailwind CSS v4 and shadcn/ui on Base UI",
17
24
  {{#if useAuth}}"Better Auth-ready session flow",{{/if}}
18
25
  {{#if (eq database "prisma")}}"Prisma database tooling",{{/if}}
19
26
  {{#if (eq database "drizzle")}}"Drizzle database tooling",{{/if}}
@@ -22,66 +29,74 @@ const featureList = [
22
29
 
23
30
  function Home() {
24
31
  return (
25
- <main className="min-h-screen bg-background text-foreground">
26
- <div className="mx-auto flex min-h-screen max-w-6xl flex-col justify-center gap-12 px-6 py-16 sm:px-10 lg:px-12">
32
+ <main className="min-h-svh">
33
+ <div className="mx-auto flex min-h-svh max-w-6xl flex-col justify-center gap-12 px-6 py-16 sm:px-10 lg:px-12">
27
34
  <section className="grid gap-10 lg:grid-cols-[1.2fr_0.8fr] lg:items-center">
28
- <div className="space-y-6">
29
- <span className="inline-flex w-fit items-center rounded-full border border-border bg-card px-3 py-1 text-xs font-medium uppercase tracking-[0.24em] text-muted-foreground shadow-sm">
30
- Created with better-ts-stack
31
- </span>
32
- <div className="space-y-4">
35
+ <div className="flex flex-col gap-6">
36
+ <Badge variant="outline">Created with better-ts-stack</Badge>
37
+ <div className="flex flex-col gap-4">
33
38
  <h1 className="max-w-3xl text-4xl font-semibold tracking-tight sm:text-5xl">
34
39
  Ship your stack faster with a polished TypeScript foundation.
35
40
  </h1>
36
41
  <p className="max-w-2xl text-base leading-7 text-muted-foreground sm:text-lg">
37
42
  Your project already includes the core app shell, database wiring,
38
- and a shadcn-compatible UI foundation so you can keep building
39
- instead of setting up boilerplate.
43
+ and shadcn/ui components so you can keep building instead of
44
+ setting up boilerplate.
40
45
  </p>
41
46
  </div>
42
47
  <div className="flex flex-col gap-3 sm:flex-row">
43
48
  {{#if useAuth}}
44
- <Link
45
- to="/sign-up"
46
- className={cn(buttonVariants({ size: "lg" }), "gap-2")}
47
- >
49
+ <Button size="lg" nativeButton={false} render={<Link to="/sign-up" />}>
48
50
  Create an account
49
- <ArrowRight className="size-4" />
50
- </Link>
51
- <Link
52
- to="/sign-in"
53
- className={buttonVariants({ variant: "outline", size: "lg" })}
51
+ <ArrowRight data-icon="inline-end" />
52
+ </Button>
53
+ <Button
54
+ size="lg"
55
+ variant="outline"
56
+ nativeButton={false}
57
+ render={<Link to="/sign-in" />}
54
58
  >
55
59
  Login
56
- </Link>
60
+ </Button>
57
61
  {{else}}
58
- <a
59
- href="https://better-ts-stack.abdullahtech.me/docs"
60
- target="_blank"
61
- rel="noreferrer"
62
- className={cn(buttonVariants({ size: "lg" }), "gap-2")}
62
+ <Button
63
+ size="lg"
64
+ nativeButton={false}
65
+ render={
66
+ <a
67
+ href="https://better-ts-stack.abdullahtech.me/docs"
68
+ target="_blank"
69
+ rel="noreferrer"
70
+ />
71
+ }
63
72
  >
64
73
  Explore the docs
65
- <ArrowRight className="size-4" />
66
- </a>
67
- <a
68
- href="https://tanstack.com/start/latest"
69
- target="_blank"
70
- rel="noreferrer"
71
- className={buttonVariants({ variant: "outline", size: "lg" })}
74
+ <ArrowRight data-icon="inline-end" />
75
+ </Button>
76
+ <Button
77
+ size="lg"
78
+ variant="outline"
79
+ nativeButton={false}
80
+ render={
81
+ <a
82
+ href="https://tanstack.com/start/latest"
83
+ target="_blank"
84
+ rel="noreferrer"
85
+ />
86
+ }
72
87
  >
73
88
  TanStack guide
74
- </a>
89
+ </Button>
75
90
  {{/if}}
76
91
  </div>
77
92
  </div>
78
93
 
79
- <Card className="border-border bg-card shadow-sm">
94
+ <Card>
80
95
  <CardHeader>
81
96
  <CardTitle>Included in this starter</CardTitle>
82
97
  </CardHeader>
83
- <CardContent className="space-y-4">
84
- <ul className="space-y-3 text-sm text-muted-foreground">
98
+ <CardContent>
99
+ <ul className="flex flex-col gap-3 text-muted-foreground">
85
100
  {featureList.map((feature) => (
86
101
  <li key={feature} className="flex items-center gap-3">
87
102
  <span className="size-2 rounded-full bg-primary" />
@@ -89,15 +104,19 @@ function Home() {
89
104
  </li>
90
105
  ))}
91
106
  </ul>
92
- <div className="rounded-2xl border border-border bg-muted px-4 py-3 text-sm text-muted-foreground">
93
- Start editing{" "}
94
- <code className="font-mono">src/routes/index.tsx</code> and
95
- shape the project around your product instead of setup chores.
96
- </div>
97
107
  </CardContent>
108
+ <CardFooter>
109
+ <Alert>
110
+ <AlertDescription>
111
+ Start editing{" "}
112
+ <code className="font-mono">src/routes/index.tsx</code> and
113
+ shape the project around your product instead of setup chores.
114
+ </AlertDescription>
115
+ </Alert>
116
+ </CardFooter>
98
117
  </Card>
99
118
  </section>
100
119
  </div>
101
120
  </main>
102
121
  );
103
- }
122
+ }
@@ -1,49 +1,95 @@
1
1
  @import "tailwindcss" source("../");
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
+ @import "@fontsource-variable/inter";
5
+
6
+ @custom-variant dark (&:is(.dark *));
2
7
 
3
8
  @theme inline {
4
- --color-background: var(--background);
5
- --color-foreground: var(--foreground);
6
- --color-card: var(--card);
7
- --color-card-foreground: var(--card-foreground);
8
- --color-primary: var(--primary);
9
- --color-primary-foreground: var(--primary-foreground);
10
- --color-secondary: var(--secondary);
11
- --color-secondary-foreground: var(--secondary-foreground);
12
- --color-muted: var(--muted);
13
- --color-muted-foreground: var(--muted-foreground);
14
- --color-accent: var(--accent);
15
- --color-accent-foreground: var(--accent-foreground);
16
- --color-destructive: var(--destructive);
17
- --color-border: var(--border);
18
- --color-input: var(--input);
19
- --color-ring: var(--ring);
9
+ --font-heading: var(--font-sans);
10
+ --font-sans: 'Inter Variable', sans-serif;
11
+ --color-sidebar-ring: var(--sidebar-ring);
12
+ --color-sidebar-border: var(--sidebar-border);
13
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14
+ --color-sidebar-accent: var(--sidebar-accent);
15
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16
+ --color-sidebar-primary: var(--sidebar-primary);
17
+ --color-sidebar-foreground: var(--sidebar-foreground);
18
+ --color-sidebar: var(--sidebar);
19
+ --color-chart-5: var(--chart-5);
20
+ --color-chart-4: var(--chart-4);
21
+ --color-chart-3: var(--chart-3);
22
+ --color-chart-2: var(--chart-2);
23
+ --color-chart-1: var(--chart-1);
24
+ --color-ring: var(--ring);
25
+ --color-input: var(--input);
26
+ --color-border: var(--border);
27
+ --color-destructive: var(--destructive);
28
+ --color-accent-foreground: var(--accent-foreground);
29
+ --color-accent: var(--accent);
30
+ --color-muted-foreground: var(--muted-foreground);
31
+ --color-muted: var(--muted);
32
+ --color-secondary-foreground: var(--secondary-foreground);
33
+ --color-secondary: var(--secondary);
34
+ --color-primary-foreground: var(--primary-foreground);
35
+ --color-primary: var(--primary);
36
+ --color-popover-foreground: var(--popover-foreground);
37
+ --color-popover: var(--popover);
38
+ --color-card-foreground: var(--card-foreground);
39
+ --color-card: var(--card);
40
+ --color-foreground: var(--foreground);
41
+ --color-background: var(--background);
42
+ --radius-sm: calc(var(--radius) * 0.6);
43
+ --radius-md: calc(var(--radius) * 0.8);
44
+ --radius-lg: var(--radius);
45
+ --radius-xl: calc(var(--radius) * 1.4);
46
+ --radius-2xl: calc(var(--radius) * 1.8);
47
+ --radius-3xl: calc(var(--radius) * 2.2);
48
+ --radius-4xl: calc(var(--radius) * 2.6);
20
49
  }
21
50
 
22
51
  :root {
23
- --background: oklch(0.985 0 0);
24
- --foreground: oklch(0.145 0 0);
25
- --card: oklch(1 0 0);
26
- --card-foreground: oklch(0.145 0 0);
27
- --primary: oklch(0.205 0 0);
28
- --primary-foreground: oklch(0.985 0 0);
29
- --secondary: oklch(0.97 0 0);
30
- --secondary-foreground: oklch(0.205 0 0);
31
- --muted: oklch(0.97 0 0);
32
- --muted-foreground: oklch(0.556 0 0);
33
- --accent: oklch(0.97 0 0);
34
- --accent-foreground: oklch(0.205 0 0);
35
- --destructive: oklch(0.577 0.245 27.325);
36
- --border: oklch(0.922 0 0);
37
- --input: oklch(0.922 0 0);
38
- --ring: oklch(0.708 0 0);
52
+ --background: oklch(1 0 0);
53
+ --foreground: oklch(0.145 0 0);
54
+ --card: oklch(1 0 0);
55
+ --card-foreground: oklch(0.145 0 0);
56
+ --popover: oklch(1 0 0);
57
+ --popover-foreground: oklch(0.145 0 0);
58
+ --primary: oklch(0.205 0 0);
59
+ --primary-foreground: oklch(0.985 0 0);
60
+ --secondary: oklch(0.97 0 0);
61
+ --secondary-foreground: oklch(0.205 0 0);
62
+ --muted: oklch(0.97 0 0);
63
+ --muted-foreground: oklch(0.556 0 0);
64
+ --accent: oklch(0.97 0 0);
65
+ --accent-foreground: oklch(0.205 0 0);
66
+ --destructive: oklch(0.577 0.245 27.325);
67
+ --border: oklch(0.922 0 0);
68
+ --input: oklch(0.922 0 0);
69
+ --ring: oklch(0.708 0 0);
70
+ --chart-1: oklch(0.87 0 0);
71
+ --chart-2: oklch(0.556 0 0);
72
+ --chart-3: oklch(0.439 0 0);
73
+ --chart-4: oklch(0.371 0 0);
74
+ --chart-5: oklch(0.269 0 0);
75
+ --radius: 0.625rem;
76
+ --sidebar: oklch(0.985 0 0);
77
+ --sidebar-foreground: oklch(0.145 0 0);
78
+ --sidebar-primary: oklch(0.205 0 0);
79
+ --sidebar-primary-foreground: oklch(0.985 0 0);
80
+ --sidebar-accent: oklch(0.97 0 0);
81
+ --sidebar-accent-foreground: oklch(0.205 0 0);
82
+ --sidebar-border: oklch(0.922 0 0);
83
+ --sidebar-ring: oklch(0.708 0 0);
39
84
  }
40
85
 
41
- @media (prefers-color-scheme: dark) {
42
- :root {
86
+ .dark {
43
87
  --background: oklch(0.145 0 0);
44
88
  --foreground: oklch(0.985 0 0);
45
89
  --card: oklch(0.205 0 0);
46
90
  --card-foreground: oklch(0.985 0 0);
91
+ --popover: oklch(0.205 0 0);
92
+ --popover-foreground: oklch(0.985 0 0);
47
93
  --primary: oklch(0.922 0 0);
48
94
  --primary-foreground: oklch(0.205 0 0);
49
95
  --secondary: oklch(0.269 0 0);
@@ -56,30 +102,29 @@
56
102
  --border: oklch(1 0 0 / 10%);
57
103
  --input: oklch(1 0 0 / 15%);
58
104
  --ring: oklch(0.556 0 0);
59
- }
60
- }
61
-
62
- @layer base {
63
- *,
64
- ::after,
65
- ::before,
66
- ::backdrop,
67
- ::file-selector-button {
68
- border-color: var(--color-gray-200, currentcolor);
69
- }
105
+ --chart-1: oklch(0.87 0 0);
106
+ --chart-2: oklch(0.556 0 0);
107
+ --chart-3: oklch(0.439 0 0);
108
+ --chart-4: oklch(0.371 0 0);
109
+ --chart-5: oklch(0.269 0 0);
110
+ --sidebar: oklch(0.205 0 0);
111
+ --sidebar-foreground: oklch(0.985 0 0);
112
+ --sidebar-primary: oklch(0.488 0.243 264.376);
113
+ --sidebar-primary-foreground: oklch(0.985 0 0);
114
+ --sidebar-accent: oklch(0.269 0 0);
115
+ --sidebar-accent-foreground: oklch(0.985 0 0);
116
+ --sidebar-border: oklch(1 0 0 / 10%);
117
+ --sidebar-ring: oklch(0.556 0 0);
70
118
  }
71
119
 
72
120
  @layer base {
73
- html {
74
- color-scheme: light dark;
75
- }
76
-
77
121
  * {
78
- @apply border-gray-200 dark:border-gray-800;
79
- }
80
-
81
- html,
122
+ @apply border-border outline-ring/50;
123
+ }
82
124
  body {
83
- @apply bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200;
84
- }
125
+ @apply bg-background text-foreground;
126
+ }
127
+ html {
128
+ @apply font-sans;
129
+ }
85
130
  }
@@ -11,10 +11,10 @@ Instructions for AI coding agents working in {{projectName}}. Read this before m
11
11
  - NestJS 12 REST API in TypeScript, using ES modules
12
12
  {{/if}}
13
13
  {{#if (eq framework 'nextjs')}}
14
- - Next.js 16 with the App Router, React 19, Tailwind CSS 4, and shadcn/ui
14
+ - Next.js 16 with the App Router, React 19, Tailwind CSS 4, and shadcn/ui on Base UI
15
15
  {{/if}}
16
16
  {{#if (eq framework 'tanstack')}}
17
- - TanStack Start with Vite, Nitro, React 19, Tailwind CSS 4, and shadcn/ui
17
+ - TanStack Start with Vite, Nitro, React 19, Tailwind CSS 4, and shadcn/ui on Base UI
18
18
  {{/if}}
19
19
  {{#if (eq database 'prisma')}}
20
20
  - PostgreSQL through Prisma 7
@@ -26,17 +26,12 @@ Instructions for AI coding agents working in {{projectName}}. Read this before m
26
26
  - MongoDB through Mongoose
27
27
  {{/if}}
28
28
  {{#if useAuth}}
29
+ - Better Auth: email and password, email verification, password reset, and GitHub and Google sign-in when their keys are set. Emails go through Resend when `RESEND_API_KEY` is set and are printed to the log otherwise
29
30
  {{#if (eq framework 'express')}}
30
- - JWT auth: `POST /auth/register`, `POST /auth/login`, and `GET /auth/me`
31
+ - Auth routes under `/api/auth` (`POST /api/auth/sign-up/email`, `POST /api/auth/sign-in/email`, ...). Clients without cookies send `Authorization: Bearer <token>`, using the `set-auth-token` header from sign-in
31
32
  {{/if}}
32
33
  {{#if (eq framework 'nestjs')}}
33
- - JWT auth: `POST /auth/register`, `POST /auth/login`, and `GET /auth/me`
34
- {{/if}}
35
- {{#if (eq framework 'nextjs')}}
36
- - Better Auth with email and password
37
- {{/if}}
38
- {{#if (eq framework 'tanstack')}}
39
- - Better Auth with email and password
34
+ - Auth routes under `/api/auth` (`POST /api/auth/sign-up/email`, `POST /api/auth/sign-in/email`, ...). Clients without cookies send `Authorization: Bearer <token>`, using the `set-auth-token` header from sign-in
40
35
  {{/if}}
41
36
  {{/if}}
42
37
  {{#if useRateLimit}}
@@ -76,8 +71,9 @@ Run type-check and lint, and fix every error they report. Run the build too when
76
71
  - `src/env.ts`: checks the environment variables with Zod at startup, so a missing one stops the server with a clear message
77
72
  - `src/routes/`: one router per resource, mounted in `src/index.ts`
78
73
  {{#if useAuth}}
79
- - `src/controllers/`, `src/services/`: request handlers and data access for auth
80
- - `src/middleware/requireAuth.ts`: add it to a route to require a valid JWT
74
+ - `src/lib/auth.ts`: the Better Auth server, mounted on `/api/auth` in `src/index.ts`
75
+ - `src/lib/email.ts`: sends the verification and password reset emails
76
+ - `src/middleware/requireAuth.ts`: add it to a route to require a session; `getSession(res)` returns it. `src/routes/me.ts` is an example
81
77
  {{/if}}
82
78
  {{#if useRateLimit}}
83
79
  - `src/middleware/rateLimit.ts`: the rate limiters
@@ -98,7 +94,8 @@ Run type-check and lint, and fix every error they report. Run the build too when
98
94
  - `src/env.ts`: checks the environment variables with Zod at startup, so a missing one stops the server with a clear message
99
95
  - `src/health/`: `GET /health`
100
96
  {{#if useAuth}}
101
- - `src/auth/`: the auth module. Put `@UseGuards(AuthGuard)` on a route to require a valid JWT
97
+ - `src/auth/`: the Better Auth server (`auth.ts`), the emails it sends (`email.ts`), and the module that mounts it on `/api/auth` (`auth.module.ts`)
98
+ - Every route needs a session unless it has `@AllowAnonymous()` (or `@OptionalAuth()`). Read the session with `@Session()`, as `src/me/me.controller.ts` does
102
99
  {{/if}}
103
100
  {{#if (eq database 'prisma')}}
104
101
  - `src/prisma/`: `PrismaService`, injected wherever the database is needed
@@ -112,10 +109,12 @@ Run type-check and lint, and fix every error they report. Run the build too when
112
109
  {{/if}}
113
110
  {{#if (eq framework 'nextjs')}}
114
111
  - `app/`: routes, layouts, and route handlers. Pages are Server Components unless they need state, effects, or event handlers
115
- - `components/ui/`: shadcn/ui components. Add new ones with the shadcn CLI instead of writing them by hand
112
+ - `components/ui/`: shadcn/ui components built on Base UI (`@base-ui/react`). Add new ones with the shadcn CLI instead of writing them by hand. Use the `render` prop to change the rendered element, not Radix's `asChild`
116
113
  - `lib/utils.ts`: the `cn()` class name helper
114
+ - `app/globals.css`: the shadcn theme. Style with its tokens (`bg-primary`, `text-muted-foreground`), not raw Tailwind colors, and space with `flex` + `gap-*`, not `space-y-*`
117
115
  {{#if useAuth}}
118
- - `lib/auth.ts`: the Better Auth server. `lib/auth-client.ts`: the client for components
116
+ - `lib/auth.ts`: the Better Auth server. `lib/auth-client.ts`: the client for components. `lib/email.ts`: sends the verification and password reset emails
117
+ - `lib/session.ts`: `getSession()` reads the session on the server, once per request
119
118
  - `app/api/auth/[...all]/route.ts`: the Better Auth endpoints. Do not rename it
120
119
  - `proxy.ts`: sends signed-out visitors from `/dashboard` to `/sign-in`. Add other protected paths to its `matcher`
121
120
  {{/if}}
@@ -133,10 +132,11 @@ Run type-check and lint, and fix every error they report. Run the build too when
133
132
  - `src/routes/`: file-based routes. A new file becomes a route
134
133
  - `src/routeTree.gen.ts`: generated by the dev server and the build. Never edit it
135
134
  - `src/router.tsx`, `src/routes/__root.tsx`: the router and the root layout
136
- - `src/components/ui/`: shadcn/ui components. Add new ones with the shadcn CLI instead of writing them by hand
135
+ - `src/components/ui/`: shadcn/ui components built on Base UI (`@base-ui/react`). Add new ones with the shadcn CLI instead of writing them by hand. Use the `render` prop to change the rendered element, not Radix's `asChild`
137
136
  - `src/lib/utils.ts`: the `cn()` class name helper
137
+ - `src/styles/app.css`: the shadcn theme. Style with its tokens (`bg-primary`, `text-muted-foreground`), not raw Tailwind colors, and space with `flex` + `gap-*`, not `space-y-*`
138
138
  {{#if useAuth}}
139
- - `src/lib/auth.ts`: the Better Auth server. `src/lib/auth-client.ts`: the client for components
139
+ - `src/lib/auth.ts`: the Better Auth server. `src/lib/auth-client.ts`: the client for components. `src/lib/email.ts`: sends the verification and password reset emails
140
140
  - `src/routes/api/auth/$.ts`: the Better Auth endpoints. Do not rename it
141
141
  - `src/routes/dashboard.tsx`: an example of a protected route that redirects in `beforeLoad`
142
142
  {{/if}}
@@ -162,6 +162,12 @@ Run type-check and lint, and fix every error they report. Run the build too when
162
162
  - The Prisma client is generated into {{#if (eq framework 'express')}}`src/generated/`{{/if}}{{#if (eq framework 'nestjs')}}`src/generated/`{{/if}}{{#if (eq framework 'nextjs')}}`generated/`{{/if}}{{#if (eq framework 'tanstack')}}`generated/`{{/if}}. Never edit or commit it
163
163
  {{/if}}
164
164
  - Match the existing code style. ESLint settings are in `eslint.config.mjs`{{#if (eq framework 'express')}} and Prettier settings in `.prettierrc`{{/if}}{{#if (eq framework 'nestjs')}} and Prettier settings in `.prettierrc`{{/if}}
165
+ {{#if (eq framework 'nextjs')}}
166
+ - Write Tailwind classes in their canonical form. Use the theme utility when one exists (`rounded-4xl`, not `rounded-[2rem]`), and in an arbitrary value write `_` only where CSS needs a space, never after a comma: `bg-[linear-gradient(180deg,#f8fafc_0%,#eef2ff_100%)]`. The Tailwind CSS VS Code extension warns about the long forms
167
+ {{/if}}
168
+ {{#if (eq framework 'tanstack')}}
169
+ - Write Tailwind classes in their canonical form. Use the theme utility when one exists (`rounded-4xl`, not `rounded-[2rem]`), and in an arbitrary value write `_` only where CSS needs a space, never after a comma: `bg-[linear-gradient(180deg,#f8fafc_0%,#eef2ff_100%)]`. The Tailwind CSS VS Code extension warns about the long forms
170
+ {{/if}}
165
171
  - Add a dependency only when the task needs it, and install it with {{packageManager}}
166
172
 
167
173
  ## Production rules
@@ -178,6 +184,7 @@ Run type-check and lint, and fix every error they report. Run the build too when
178
184
  - Keep the security setup in `src/main.ts` and `src/app.module.ts` in place. Set `CORS_ORIGIN` to your real origins in production
179
185
  {{/if}}
180
186
  {{#if (eq framework 'nextjs')}}
187
+ - Server first: do not `await` data at the top of a page or layout, which holds back the whole page. Move each `await` into the component that needs the data and wrap that component in `<Suspense>` with a fallback, so the rest of the page is sent at once{{#if useAuth}}. `app/dashboard/page.tsx` shows the pattern with `getSession()`{{/if}}
181
188
  - Code that reads secrets or the database stays on the server. Only variables prefixed with `NEXT_PUBLIC_` reach the browser, so never put a secret in one
182
189
  {{/if}}
183
190
  {{#if (eq framework 'tanstack')}}
@@ -192,6 +199,9 @@ Run type-check and lint, and fix every error they report. Run the build too when
192
199
  {{/if}}
193
200
  {{/if}}
194
201
  {{#if (eq framework 'express')}}
202
+ {{#if useAuth}}
203
+ - Check the session on the server in every protected route with `requireAuth`
204
+ {{/if}}
195
205
  - Keep `GET /health` fast and free of auth{{#if useRateLimit}} and rate limiting{{/if}}, so hosts can check that the app is up
196
206
  {{/if}}
197
207
  {{#if (eq framework 'nestjs')}}
@@ -225,11 +235,9 @@ The libraries here may be newer than what you know. When you are not sure how an
225
235
  - Mongoose: https://mongoosejs.com/docs
226
236
  {{/if}}
227
237
  {{#if useAuth}}
228
- {{#if (eq framework 'nextjs')}}
229
- - Better Auth: https://www.better-auth.com/docs
230
- {{/if}}
231
- {{#if (eq framework 'tanstack')}}
232
238
  - Better Auth: https://www.better-auth.com/docs
239
+ {{#if (eq framework 'nestjs')}}
240
+ - NestJS Better Auth integration: https://github.com/ThallesP/nestjs-better-auth
233
241
  {{/if}}
234
242
  {{/if}}
235
243
  - Zod: https://zod.dev
@@ -1,20 +1,21 @@
1
1
  {
2
2
  "id": "auth",
3
- "name": "JWT Auth",
4
- "description": "JWT authentication with bcrypt password hashing",
3
+ "name": "Better Auth",
4
+ "description": "Better Auth for Express: email and password, email verification, password reset, GitHub and Google sign-in, and bearer tokens",
5
5
  "type": "feature",
6
6
  "dependencies": {
7
- "bcrypt": "^6.0.0",
8
- "jsonwebtoken": "^9.0.3",
9
- "zod": "^4.6.7"
10
- },
11
- "devDependencies": {
12
- "@types/bcrypt": "^6.0.0",
13
- "@types/jsonwebtoken": "^9.0.10"
7
+ "better-auth": "^1.7.18"
14
8
  },
9
+ "devDependencies": {},
15
10
  "scripts": {},
16
11
  "envVars": {
17
- "JWT_SECRET": "please-change-me",
18
- "JWT_EXPIRES_IN": "1h"
12
+ "BETTER_AUTH_SECRET": "please-change-me-to-a-random-string",
13
+ "BETTER_AUTH_URL": "http://localhost:3000",
14
+ "GITHUB_CLIENT_ID": "",
15
+ "GITHUB_CLIENT_SECRET": "",
16
+ "GOOGLE_CLIENT_ID": "",
17
+ "GOOGLE_CLIENT_SECRET": "",
18
+ "RESEND_API_KEY": "",
19
+ "EMAIL_FROM": "onboarding@resend.dev"
19
20
  }
20
21
  }
@@ -0,0 +1,143 @@
1
+ import { betterAuth } from "better-auth";
2
+ {{#if (eq database 'none')}}
3
+ import { memoryAdapter } from "better-auth/adapters/memory";
4
+ {{/if}}
5
+ {{#if (eq database 'prisma')}}
6
+ import { prismaAdapter } from "better-auth/adapters/prisma";
7
+ {{/if}}
8
+ {{#if (eq database 'drizzle')}}
9
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
10
+ {{/if}}
11
+ {{#if (eq database 'mongoose')}}
12
+ import { mongodbAdapter } from "better-auth/adapters/mongodb";
13
+ import { MongoClient } from "mongodb";
14
+ {{/if}}
15
+ import { bearer } from "better-auth/plugins";
16
+
17
+ import { env } from "../env";
18
+ {{#if (eq database 'drizzle')}}
19
+ import { db } from "./db";
20
+ {{/if}}
21
+ import { sendEmail } from "./email";
22
+ {{#if (eq database 'prisma')}}
23
+ import { prisma } from "./prisma";
24
+ {{/if}}
25
+
26
+ {{#if (eq database 'none')}}
27
+ // In-memory store for trying the API out: users and sessions are lost on
28
+ // restart, so add a database before going to production
29
+ const memoryDb = { user: [], session: [], account: [], verification: [] };
30
+
31
+ {{/if}}
32
+ {{#if (eq database 'mongoose')}}
33
+ // Better Auth talks to MongoDB through the driver, next to Mongoose
34
+ export const mongoClient = new MongoClient(env.MONGODB_URI);
35
+
36
+ {{/if}}
37
+ // Reports a failed email instead of failing the request: the sign-up or reset
38
+ // already happened, and the user can ask for the email again
39
+ function deliver(email: Parameters<typeof sendEmail>[0]): void {
40
+ sendEmail(email).catch((error: unknown) => {
41
+ console.error(`Could not send "${email.subject}" to ${email.to}:`, error);
42
+ });
43
+ }
44
+
45
+ // Points a verification link at the API itself when the client gave no page
46
+ // to return to, so opening it answers with JSON instead of a redirect to "/"
47
+ function withoutDefaultCallback(url: string): string {
48
+ const link = new URL(url);
49
+ if (link.searchParams.get("callbackURL") === "/") {
50
+ link.searchParams.delete("callbackURL");
51
+ }
52
+ return link.toString();
53
+ }
54
+
55
+ export const auth = betterAuth({
56
+ {{#if (eq database 'none')}}
57
+ database: memoryAdapter(memoryDb),
58
+ {{/if}}
59
+ {{#if (eq database 'prisma')}}
60
+ database: prismaAdapter(prisma, { provider: "postgresql" }),
61
+ {{/if}}
62
+ {{#if (eq database 'drizzle')}}
63
+ database: drizzleAdapter(db, { provider: "pg" }),
64
+ {{/if}}
65
+ {{#if (eq database 'mongoose')}}
66
+ // Without the client, writes run without transactions, which a standalone
67
+ // MongoDB server (no replica set) does not support
68
+ database: mongodbAdapter(mongoClient.db()),
69
+ {{/if}}
70
+ secret: env.BETTER_AUTH_SECRET,
71
+ baseURL: env.BETTER_AUTH_URL,
72
+ // Browser apps on these origins may call the auth routes with cookies
73
+ trustedOrigins: typeof env.CORS_ORIGIN === "string" ? [] : env.CORS_ORIGIN,
74
+ emailAndPassword: {
75
+ enabled: true,
76
+ // Set to true to block sign-in until the email address is verified
77
+ requireEmailVerification: false,
78
+ // A reset signs the user out everywhere
79
+ revokeSessionsOnPasswordReset: true,
80
+ sendResetPassword: ({ user, url, token }) => {
81
+ deliver({
82
+ to: user.email,
83
+ subject: "Reset your password",
84
+ text: `Open this link to choose a new password:\n${url}\n\nOr send this token to POST /api/auth/reset-password: ${token}`,
85
+ });
86
+ return Promise.resolve();
87
+ },
88
+ },
89
+ emailVerification: {
90
+ sendOnSignUp: true,
91
+ autoSignInAfterVerification: true,
92
+ sendVerificationEmail: ({ user, url }) => {
93
+ deliver({
94
+ to: user.email,
95
+ subject: "Verify your email address",
96
+ text: `Open this link to verify your email address:\n${withoutDefaultCallback(url)}`,
97
+ });
98
+ return Promise.resolve();
99
+ },
100
+ },
101
+ // Each provider is turned on once its client ID and secret are set
102
+ socialProviders: {
103
+ ...(env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET
104
+ ? {
105
+ github: {
106
+ clientId: env.GITHUB_CLIENT_ID,
107
+ clientSecret: env.GITHUB_CLIENT_SECRET,
108
+ },
109
+ }
110
+ : {}),
111
+ ...(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET
112
+ ? {
113
+ google: {
114
+ clientId: env.GOOGLE_CLIENT_ID,
115
+ clientSecret: env.GOOGLE_CLIENT_SECRET,
116
+ },
117
+ }
118
+ : {}),
119
+ },
120
+ // Clients without cookies send "Authorization: Bearer <token>"; the token
121
+ // comes back in the set-auth-token header of sign-in and sign-up
122
+ plugins: [bearer()],
123
+ {{#if useRateLimit}}
124
+ // Sign-up, sign-in, and password reset: 10 attempts per client IP in 15
125
+ // minutes, against password guessing
126
+ rateLimit: {
127
+ enabled: true,
128
+ window: 15 * 60,
129
+ max: 100,
130
+ customRules: {
131
+ "/sign-in/*": { window: 15 * 60, max: 10 },
132
+ "/sign-up/*": { window: 15 * 60, max: 10 },
133
+ "/request-password-reset": { window: 15 * 60, max: 10 },
134
+ },
135
+ },
136
+ {{/if}}
137
+ advanced: {
138
+ // Set by the server from req.ip on every request (see index.ts)
139
+ ipAddress: { ipAddressHeaders: ["x-client-ip"] },
140
+ },
141
+ });
142
+
143
+ export type Session = typeof auth.$Infer.Session;