create-better-t-stack 2.45.5 → 2.46.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 (49) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/index.d.ts +10 -0
  3. package/dist/index.js +1 -1
  4. package/dist/{src-Cun9EO6e.js → src-NOw0j6Z9.js} +272 -132
  5. package/package.json +1 -1
  6. package/templates/auth/better-auth/server/base/src/lib/auth.ts.hbs +153 -6
  7. package/templates/auth/better-auth/web/nuxt/app/pages/dashboard.vue.hbs +36 -2
  8. package/templates/auth/better-auth/web/nuxt/app/plugins/auth-client.ts.hbs +22 -0
  9. package/templates/auth/better-auth/web/react/base/src/lib/auth-client.ts.hbs +6 -0
  10. package/templates/auth/better-auth/web/react/next/src/app/dashboard/dashboard.tsx.hbs +58 -0
  11. package/templates/auth/better-auth/web/react/next/src/app/dashboard/page.tsx.hbs +31 -41
  12. package/templates/auth/better-auth/web/react/react-router/src/routes/dashboard.tsx.hbs +37 -3
  13. package/templates/auth/better-auth/web/react/tanstack-router/src/routes/dashboard.tsx.hbs +50 -32
  14. package/templates/auth/better-auth/web/react/tanstack-start/src/routes/dashboard.tsx.hbs +51 -36
  15. package/templates/auth/better-auth/web/solid/src/lib/auth-client.ts.hbs +11 -0
  16. package/templates/auth/better-auth/web/solid/src/routes/dashboard.tsx.hbs +52 -29
  17. package/templates/auth/better-auth/web/svelte/src/lib/{auth-client.ts → auth-client.ts.hbs} +6 -0
  18. package/templates/auth/better-auth/web/svelte/src/routes/dashboard/+page.svelte.hbs +24 -3
  19. package/templates/backend/server/server-base/package.json.hbs +1 -1
  20. package/templates/extras/bunfig.toml.hbs +4 -0
  21. package/templates/frontend/react/tanstack-start/src/routes/__root.tsx.hbs +4 -0
  22. package/templates/frontend/solid/package.json.hbs +10 -10
  23. package/templates/payments/polar/server/base/src/lib/payments.ts.hbs +6 -0
  24. package/templates/payments/polar/web/nuxt/app/pages/success.vue.hbs +11 -0
  25. package/templates/payments/polar/web/react/next/src/app/success/page.tsx.hbs +15 -0
  26. package/templates/payments/polar/web/react/react-router/src/routes/success.tsx.hbs +13 -0
  27. package/templates/payments/polar/web/react/tanstack-router/src/routes/success.tsx.hbs +19 -0
  28. package/templates/payments/polar/web/react/tanstack-start/src/routes/success.tsx.hbs +19 -0
  29. package/templates/payments/polar/web/solid/src/routes/success.tsx.hbs +23 -0
  30. package/templates/payments/polar/web/svelte/src/routes/success/+page.svelte.hbs +12 -0
  31. package/templates/auth/better-auth/web/nuxt/app/plugins/auth-client.ts +0 -16
  32. package/templates/auth/better-auth/web/solid/src/lib/auth-client.ts +0 -5
  33. /package/templates/auth/better-auth/web/nuxt/app/components/{SignInForm.vue → SignInForm.vue.hbs} +0 -0
  34. /package/templates/auth/better-auth/web/nuxt/app/components/{SignUpForm.vue → SignUpForm.vue.hbs} +0 -0
  35. /package/templates/auth/better-auth/web/nuxt/app/components/{UserMenu.vue → UserMenu.vue.hbs} +0 -0
  36. /package/templates/auth/better-auth/web/nuxt/app/pages/{login.vue → login.vue.hbs} +0 -0
  37. /package/templates/auth/better-auth/web/react/next/src/app/login/{page.tsx → page.tsx.hbs} +0 -0
  38. /package/templates/auth/better-auth/web/react/next/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
  39. /package/templates/auth/better-auth/web/react/next/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
  40. /package/templates/auth/better-auth/web/react/next/src/components/{theme-provider.tsx → theme-provider.tsx.hbs} +0 -0
  41. /package/templates/auth/better-auth/web/react/next/src/components/{user-menu.tsx → user-menu.tsx.hbs} +0 -0
  42. /package/templates/auth/better-auth/web/react/react-router/src/components/{sign-in-form.tsx → sign-in-form.tsx.hbs} +0 -0
  43. /package/templates/auth/better-auth/web/react/react-router/src/components/{sign-up-form.tsx → sign-up-form.tsx.hbs} +0 -0
  44. /package/templates/auth/better-auth/web/react/react-router/src/components/{user-menu.tsx → user-menu.tsx.hbs} +0 -0
  45. /package/templates/auth/better-auth/web/react/react-router/src/routes/{login.tsx → login.tsx.hbs} +0 -0
  46. /package/templates/auth/better-auth/web/svelte/src/components/{SignInForm.svelte → SignInForm.svelte.hbs} +0 -0
  47. /package/templates/auth/better-auth/web/svelte/src/components/{SignUpForm.svelte → SignUpForm.svelte.hbs} +0 -0
  48. /package/templates/auth/better-auth/web/svelte/src/components/{UserMenu.svelte → UserMenu.svelte.hbs} +0 -0
  49. /package/templates/auth/better-auth/web/svelte/src/routes/login/{+page.svelte → +page.svelte.hbs} +0 -0
@@ -36,6 +36,7 @@ const DEFAULT_CONFIG_BASE = {
36
36
  database: "sqlite",
37
37
  orm: "drizzle",
38
38
  auth: "better-auth",
39
+ payments: "none",
39
40
  addons: ["turborepo"],
40
41
  examples: [],
41
42
  git: true,
@@ -59,8 +60,8 @@ function getDefaultConfig() {
59
60
  }
60
61
  const DEFAULT_CONFIG = getDefaultConfig();
61
62
  const dependencyVersionMap = {
62
- "better-auth": "^1.3.9",
63
- "@better-auth/expo": "^1.3.9",
63
+ "better-auth": "^1.3.10",
64
+ "@better-auth/expo": "^1.3.10",
64
65
  "@clerk/nextjs": "^6.31.5",
65
66
  "@clerk/clerk-react": "^5.45.0",
66
67
  "@clerk/tanstack-react-start": "^0.23.1",
@@ -131,9 +132,9 @@ const dependencyVersionMap = {
131
132
  "@tanstack/vue-query": "^5.83.0",
132
133
  "@tanstack/react-query-devtools": "^5.85.5",
133
134
  "@tanstack/react-query": "^5.85.5",
134
- "@tanstack/solid-query": "^5.75.0",
135
- "@tanstack/solid-query-devtools": "^5.75.0",
136
- "@tanstack/solid-router-devtools": "^1.131.25",
135
+ "@tanstack/solid-query": "^5.87.4",
136
+ "@tanstack/solid-query-devtools": "^5.87.4",
137
+ "@tanstack/solid-router-devtools": "^1.131.44",
137
138
  wrangler: "^4.23.0",
138
139
  "@cloudflare/vite-plugin": "^1.9.0",
139
140
  "@opennextjs/cloudflare": "^1.6.5",
@@ -142,7 +143,9 @@ const dependencyVersionMap = {
142
143
  "@cloudflare/workers-types": "^4.20250822.0",
143
144
  alchemy: "^0.65.1",
144
145
  nitropack: "^2.12.4",
145
- dotenv: "^17.2.1"
146
+ dotenv: "^17.2.1",
147
+ "@polar-sh/better-auth": "^1.1.3",
148
+ "@polar-sh/sdk": "^0.34.16"
146
149
  };
147
150
  const ADDON_COMPATIBILITY = {
148
151
  pwa: [
@@ -256,6 +259,7 @@ const AuthSchema = z.enum([
256
259
  "clerk",
257
260
  "none"
258
261
  ]).describe("Authentication provider");
262
+ const PaymentsSchema = z.enum(["polar", "none"]).describe("Payments provider");
259
263
  const ProjectNameSchema = z.string().min(1, "Project name cannot be empty").max(255, "Project name must be less than 255 characters").refine((name) => name === "." || !name.startsWith("."), "Project name cannot start with a dot (except for '.')").refine((name) => name === "." || !name.startsWith("-"), "Project name cannot start with a dash").refine((name) => {
260
264
  const invalidChars = [
261
265
  "<",
@@ -286,28 +290,16 @@ const DirectoryConflictSchema = z.enum([
286
290
  ]).describe("How to handle existing directory conflicts");
287
291
 
288
292
  //#endregion
289
- //#region src/utils/addon-compatibility.ts
290
- function validateAddonCompatibility(addon, frontend) {
291
- const compatibleFrontends = ADDON_COMPATIBILITY[addon];
292
- if (compatibleFrontends.length === 0) return { isCompatible: true };
293
- const hasCompatibleFrontend = frontend.some((f) => compatibleFrontends.includes(f));
294
- if (!hasCompatibleFrontend) {
295
- const frontendList = compatibleFrontends.join(", ");
296
- return {
297
- isCompatible: false,
298
- reason: `${addon} addon requires one of these frontends: ${frontendList}`
299
- };
300
- }
301
- return { isCompatible: true };
302
- }
303
- function getCompatibleAddons(allAddons, frontend, existingAddons = []) {
304
- return allAddons.filter((addon) => {
305
- if (existingAddons.includes(addon)) return false;
306
- if (addon === "none") return false;
307
- const { isCompatible } = validateAddonCompatibility(addon, frontend);
308
- return isCompatible;
309
- });
310
- }
293
+ //#region src/utils/compatibility.ts
294
+ const WEB_FRAMEWORKS = [
295
+ "tanstack-router",
296
+ "react-router",
297
+ "tanstack-start",
298
+ "next",
299
+ "nuxt",
300
+ "svelte",
301
+ "solid"
302
+ ];
311
303
 
312
304
  //#endregion
313
305
  //#region src/utils/errors.ts
@@ -331,6 +323,121 @@ function handleError(error, fallbackMessage) {
331
323
  process.exit(1);
332
324
  }
333
325
 
326
+ //#endregion
327
+ //#region src/utils/compatibility-rules.ts
328
+ function isWebFrontend(value) {
329
+ return WEB_FRAMEWORKS.includes(value);
330
+ }
331
+ function splitFrontends(values = []) {
332
+ const web = values.filter((f) => isWebFrontend(f));
333
+ const native = values.filter((f) => f === "native-nativewind" || f === "native-unistyles");
334
+ return {
335
+ web,
336
+ native
337
+ };
338
+ }
339
+ function ensureSingleWebAndNative(frontends) {
340
+ const { web, native } = splitFrontends(frontends);
341
+ if (web.length > 1) exitWithError("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
342
+ if (native.length > 1) exitWithError("Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles");
343
+ }
344
+ function validateWorkersCompatibility(providedFlags, options, config) {
345
+ if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") exitWithError(`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`);
346
+ if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") exitWithError(`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`);
347
+ if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") exitWithError("Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.");
348
+ if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker") exitWithError("Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. 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.");
349
+ if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") exitWithError("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.");
350
+ if (providedFlags.has("dbSetup") && options.dbSetup === "docker" && config.runtime === "workers") exitWithError("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.");
351
+ }
352
+ function validateApiFrontendCompatibility(api, frontends = []) {
353
+ const includesNuxt = frontends.includes("nuxt");
354
+ const includesSvelte = frontends.includes("svelte");
355
+ const includesSolid = frontends.includes("solid");
356
+ if ((includesNuxt || includesSvelte || includesSolid) && api === "trpc") exitWithError(`tRPC API is not supported with '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' frontend. Please use --api orpc or --api none or remove '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' from --frontend.`);
357
+ }
358
+ function isFrontendAllowedWithBackend(frontend, backend, auth) {
359
+ if (backend === "convex" && frontend === "solid") return false;
360
+ if (auth === "clerk" && backend === "convex") {
361
+ const incompatibleFrontends = [
362
+ "nuxt",
363
+ "svelte",
364
+ "solid"
365
+ ];
366
+ if (incompatibleFrontends.includes(frontend)) return false;
367
+ }
368
+ return true;
369
+ }
370
+ function allowedApisForFrontends(frontends = []) {
371
+ const includesNuxt = frontends.includes("nuxt");
372
+ const includesSvelte = frontends.includes("svelte");
373
+ const includesSolid = frontends.includes("solid");
374
+ const base = [
375
+ "trpc",
376
+ "orpc",
377
+ "none"
378
+ ];
379
+ if (includesNuxt || includesSvelte || includesSolid) return ["orpc", "none"];
380
+ return base;
381
+ }
382
+ function isExampleTodoAllowed(backend, database) {
383
+ return !(backend !== "convex" && backend !== "none" && database === "none");
384
+ }
385
+ function isExampleAIAllowed(_backend, frontends = []) {
386
+ const includesSolid = frontends.includes("solid");
387
+ if (includesSolid) return false;
388
+ return true;
389
+ }
390
+ function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
391
+ if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) exitWithError("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
392
+ }
393
+ function validateServerDeployRequiresBackend(serverDeploy, backend) {
394
+ if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) exitWithError("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
395
+ }
396
+ function validateAddonCompatibility(addon, frontend, _auth) {
397
+ const compatibleFrontends = ADDON_COMPATIBILITY[addon];
398
+ if (compatibleFrontends.length > 0) {
399
+ const hasCompatibleFrontend = frontend.some((f) => compatibleFrontends.includes(f));
400
+ if (!hasCompatibleFrontend) {
401
+ const frontendList = compatibleFrontends.join(", ");
402
+ return {
403
+ isCompatible: false,
404
+ reason: `${addon} addon requires one of these frontends: ${frontendList}`
405
+ };
406
+ }
407
+ }
408
+ return { isCompatible: true };
409
+ }
410
+ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth) {
411
+ return allAddons.filter((addon) => {
412
+ if (existingAddons.includes(addon)) return false;
413
+ if (addon === "none") return false;
414
+ const { isCompatible } = validateAddonCompatibility(addon, frontend, auth);
415
+ return isCompatible;
416
+ });
417
+ }
418
+ function validateAddonsAgainstFrontends(addons = [], frontends = [], auth) {
419
+ for (const addon of addons) {
420
+ if (addon === "none") continue;
421
+ const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth);
422
+ if (!isCompatible) exitWithError(`Incompatible addon/frontend combination: ${reason}`);
423
+ }
424
+ }
425
+ function validatePaymentsCompatibility(payments, auth, backend, frontends = []) {
426
+ if (!payments || payments === "none") return;
427
+ if (payments === "polar") {
428
+ if (!auth || auth === "none" || auth !== "better-auth") exitWithError("Polar payments requires Better Auth. Please use '--auth better-auth' or choose a different payments provider.");
429
+ if (backend === "convex") exitWithError("Polar payments is not compatible with Convex backend. Please use a different backend or choose a different payments provider.");
430
+ const { web } = splitFrontends(frontends);
431
+ if (web.length === 0 && frontends.length > 0) exitWithError("Polar payments requires a web frontend or no frontend. Please select a web frontend or choose a different payments provider.");
432
+ }
433
+ }
434
+ function validateExamplesCompatibility(examples, backend, database, frontend) {
435
+ const examplesArr = examples ?? [];
436
+ if (examplesArr.length === 0 || examplesArr.includes("none")) return;
437
+ if (examplesArr.includes("todo") && backend !== "convex" && backend !== "none" && database === "none") exitWithError("The 'todo' example requires a database if a backend (other than Convex) is present. Cannot use --examples todo when database is 'none' and a backend is selected.");
438
+ if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) exitWithError("The 'ai' example is not compatible with the Solid frontend.");
439
+ }
440
+
334
441
  //#endregion
335
442
  //#region src/prompts/addons.ts
336
443
  function getAddonDisplay(addon) {
@@ -401,7 +508,7 @@ const ADDON_GROUPS = {
401
508
  "husky"
402
509
  ]
403
510
  };
404
- async function getAddonsChoice(addons, frontends) {
511
+ async function getAddonsChoice(addons, frontends, auth) {
405
512
  if (addons !== void 0) return addons;
406
513
  const allAddons = AddonsSchema.options.filter((addon) => addon !== "none");
407
514
  const groupedOptions = {
@@ -411,7 +518,7 @@ async function getAddonsChoice(addons, frontends) {
411
518
  };
412
519
  const frontendsArray = frontends || [];
413
520
  for (const addon of allAddons) {
414
- const { isCompatible } = validateAddonCompatibility(addon, frontendsArray);
521
+ const { isCompatible } = validateAddonCompatibility(addon, frontendsArray, auth);
415
522
  if (!isCompatible) continue;
416
523
  const { label, hint } = getAddonDisplay(addon);
417
524
  const option = {
@@ -437,14 +544,14 @@ async function getAddonsChoice(addons, frontends) {
437
544
  if (isCancel(response)) return exitCancelled("Operation cancelled");
438
545
  return response;
439
546
  }
440
- async function getAddonsToAdd(frontend, existingAddons = []) {
547
+ async function getAddonsToAdd(frontend, existingAddons = [], auth) {
441
548
  const groupedOptions = {
442
549
  Documentation: [],
443
550
  Linting: [],
444
551
  Other: []
445
552
  };
446
553
  const frontendArray = frontend || [];
447
- const compatibleAddons = getCompatibleAddons(AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray, existingAddons);
554
+ const compatibleAddons = getCompatibleAddons(AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray, existingAddons, auth);
448
555
  for (const addon of compatibleAddons) {
449
556
  const { label, hint } = getAddonDisplay(addon);
450
557
  const option = {
@@ -470,102 +577,6 @@ async function getAddonsToAdd(frontend, existingAddons = []) {
470
577
  return response;
471
578
  }
472
579
 
473
- //#endregion
474
- //#region src/utils/compatibility.ts
475
- const WEB_FRAMEWORKS = [
476
- "tanstack-router",
477
- "react-router",
478
- "tanstack-start",
479
- "next",
480
- "nuxt",
481
- "svelte",
482
- "solid"
483
- ];
484
-
485
- //#endregion
486
- //#region src/utils/compatibility-rules.ts
487
- function isWebFrontend(value) {
488
- return WEB_FRAMEWORKS.includes(value);
489
- }
490
- function splitFrontends(values = []) {
491
- const web = values.filter((f) => isWebFrontend(f));
492
- const native = values.filter((f) => f === "native-nativewind" || f === "native-unistyles");
493
- return {
494
- web,
495
- native
496
- };
497
- }
498
- function ensureSingleWebAndNative(frontends) {
499
- const { web, native } = splitFrontends(frontends);
500
- if (web.length > 1) exitWithError("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
501
- if (native.length > 1) exitWithError("Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles");
502
- }
503
- function validateWorkersCompatibility(providedFlags, options, config) {
504
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") exitWithError(`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`);
505
- if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") exitWithError(`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`);
506
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") exitWithError("Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.");
507
- if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker") exitWithError("Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. 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.");
508
- if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") exitWithError("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle or Prisma ORM. Please use a different database or runtime.");
509
- if (providedFlags.has("dbSetup") && options.dbSetup === "docker" && config.runtime === "workers") exitWithError("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.");
510
- }
511
- function validateApiFrontendCompatibility(api, frontends = []) {
512
- const includesNuxt = frontends.includes("nuxt");
513
- const includesSvelte = frontends.includes("svelte");
514
- const includesSolid = frontends.includes("solid");
515
- if ((includesNuxt || includesSvelte || includesSolid) && api === "trpc") exitWithError(`tRPC API is not supported with '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' frontend. Please use --api orpc or --api none or remove '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' from --frontend.`);
516
- }
517
- function isFrontendAllowedWithBackend(frontend, backend, auth) {
518
- if (backend === "convex" && frontend === "solid") return false;
519
- if (auth === "clerk" && backend === "convex") {
520
- const incompatibleFrontends = [
521
- "nuxt",
522
- "svelte",
523
- "solid"
524
- ];
525
- if (incompatibleFrontends.includes(frontend)) return false;
526
- }
527
- return true;
528
- }
529
- function allowedApisForFrontends(frontends = []) {
530
- const includesNuxt = frontends.includes("nuxt");
531
- const includesSvelte = frontends.includes("svelte");
532
- const includesSolid = frontends.includes("solid");
533
- const base = [
534
- "trpc",
535
- "orpc",
536
- "none"
537
- ];
538
- if (includesNuxt || includesSvelte || includesSolid) return ["orpc", "none"];
539
- return base;
540
- }
541
- function isExampleTodoAllowed(backend, database) {
542
- return !(backend !== "convex" && backend !== "none" && database === "none");
543
- }
544
- function isExampleAIAllowed(_backend, frontends = []) {
545
- const includesSolid = frontends.includes("solid");
546
- if (includesSolid) return false;
547
- return true;
548
- }
549
- function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
550
- if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) exitWithError("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
551
- }
552
- function validateServerDeployRequiresBackend(serverDeploy, backend) {
553
- if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) exitWithError("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
554
- }
555
- function validateAddonsAgainstFrontends(addons = [], frontends = []) {
556
- for (const addon of addons) {
557
- if (addon === "none") continue;
558
- const { isCompatible, reason } = validateAddonCompatibility(addon, frontends);
559
- if (!isCompatible) exitWithError(`Incompatible addon/frontend combination: ${reason}`);
560
- }
561
- }
562
- function validateExamplesCompatibility(examples, backend, database, frontend) {
563
- const examplesArr = examples ?? [];
564
- if (examplesArr.length === 0 || examplesArr.includes("none")) return;
565
- if (examplesArr.includes("todo") && backend !== "convex" && backend !== "none" && database === "none") exitWithError("The 'todo' example requires a database if a backend (other than Convex) is present. Cannot use --examples todo when database is 'none' and a backend is selected.");
566
- if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) exitWithError("The 'ai' example is not compatible with the Solid frontend.");
567
- }
568
-
569
580
  //#endregion
570
581
  //#region src/prompts/api.ts
571
582
  async function getApiChoice(Api, frontend, backend) {
@@ -1037,6 +1048,30 @@ async function getPackageManagerChoice(packageManager) {
1037
1048
  return response;
1038
1049
  }
1039
1050
 
1051
+ //#endregion
1052
+ //#region src/prompts/payments.ts
1053
+ async function getPaymentsChoice(payments, auth, backend, frontends) {
1054
+ if (payments !== void 0) return payments;
1055
+ const isPolarCompatible = auth === "better-auth" && backend !== "convex" && (frontends?.length === 0 || splitFrontends(frontends).web.length > 0);
1056
+ if (!isPolarCompatible) return "none";
1057
+ const options = [{
1058
+ value: "polar",
1059
+ label: "Polar",
1060
+ hint: "Turn your software into a business. 6 lines of code."
1061
+ }, {
1062
+ value: "none",
1063
+ label: "None",
1064
+ hint: "No payments integration"
1065
+ }];
1066
+ const response = await select({
1067
+ message: "Select payments provider",
1068
+ options,
1069
+ initialValue: DEFAULT_CONFIG.payments
1070
+ });
1071
+ if (isCancel(response)) return exitCancelled("Operation cancelled");
1072
+ return response;
1073
+ }
1074
+
1040
1075
  //#endregion
1041
1076
  //#region src/prompts/runtime.ts
1042
1077
  async function getRuntimeChoice(runtime, backend) {
@@ -1226,7 +1261,8 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
1226
1261
  orm: ({ results }) => getORMChoice(flags.orm, results.database !== "none", results.database, results.backend, results.runtime),
1227
1262
  api: ({ results }) => getApiChoice(flags.api, results.frontend, results.backend),
1228
1263
  auth: ({ results }) => getAuthChoice(flags.auth, results.database !== "none", results.backend, results.frontend),
1229
- addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
1264
+ payments: ({ results }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend),
1265
+ addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend, results.auth),
1230
1266
  examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
1231
1267
  dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
1232
1268
  webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend),
@@ -1245,6 +1281,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
1245
1281
  database: result.database,
1246
1282
  orm: result.orm,
1247
1283
  auth: result.auth,
1284
+ payments: result.payments,
1248
1285
  addons: result.addons,
1249
1286
  examples: result.examples,
1250
1287
  git: result.git,
@@ -1388,6 +1425,7 @@ function displayConfig(config) {
1388
1425
  if (config.database !== void 0) configDisplay.push(`${pc.blue("Database:")} ${String(config.database)}`);
1389
1426
  if (config.orm !== void 0) configDisplay.push(`${pc.blue("ORM:")} ${String(config.orm)}`);
1390
1427
  if (config.auth !== void 0) configDisplay.push(`${pc.blue("Auth:")} ${String(config.auth)}`);
1428
+ if (config.payments !== void 0) configDisplay.push(`${pc.blue("Payments:")} ${String(config.payments)}`);
1391
1429
  if (config.addons !== void 0) {
1392
1430
  const addons = Array.isArray(config.addons) ? config.addons : [config.addons];
1393
1431
  const addonsText = addons.length > 0 && addons[0] !== void 0 ? addons.join(", ") : "none";
@@ -1426,6 +1464,7 @@ function generateReproducibleCommand(config) {
1426
1464
  flags.push(`--orm ${config.orm}`);
1427
1465
  flags.push(`--api ${config.api}`);
1428
1466
  flags.push(`--auth ${config.auth}`);
1467
+ flags.push(`--payments ${config.payments}`);
1429
1468
  if (config.addons && config.addons.length > 0) flags.push(`--addons ${config.addons.join(" ")}`);
1430
1469
  else flags.push("--addons none");
1431
1470
  if (config.examples && config.examples.length > 0) flags.push(`--examples ${config.examples.join(" ")}`);
@@ -1595,6 +1634,7 @@ function processFlags(options, projectName) {
1595
1634
  if (options.database) config.database = options.database;
1596
1635
  if (options.orm) config.orm = options.orm;
1597
1636
  if (options.auth !== void 0) config.auth = options.auth;
1637
+ if (options.payments !== void 0) config.payments = options.payments;
1598
1638
  if (options.git !== void 0) config.git = options.git;
1599
1639
  if (options.install !== void 0) config.install = options.install;
1600
1640
  if (options.runtime) config.runtime = options.runtime;
@@ -1751,7 +1791,7 @@ function validateFullConfig(config, providedFlags, options) {
1751
1791
  validateWorkersCompatibility(providedFlags, options, config);
1752
1792
  if (config.runtime === "workers" && config.serverDeploy === "none") exitWithError("Cloudflare Workers runtime requires a server deployment. Please choose 'wrangler' or 'alchemy' for --server-deploy.");
1753
1793
  if (config.addons && config.addons.length > 0) {
1754
- validateAddonsAgainstFrontends(config.addons, config.frontend);
1794
+ validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
1755
1795
  config.addons = [...new Set(config.addons)];
1756
1796
  }
1757
1797
  validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
@@ -1761,7 +1801,8 @@ function validateConfigForProgrammaticUse(config) {
1761
1801
  validateDatabaseOrmAuth(config);
1762
1802
  if (config.frontend && config.frontend.length > 0) ensureSingleWebAndNative(config.frontend);
1763
1803
  validateApiFrontendCompatibility(config.api, config.frontend);
1764
- if (config.addons && config.addons.length > 0) validateAddonsAgainstFrontends(config.addons, config.frontend);
1804
+ validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend);
1805
+ if (config.addons && config.addons.length > 0) validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
1765
1806
  validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
1766
1807
  } catch (error) {
1767
1808
  if (error instanceof Error) throw error;
@@ -1859,6 +1900,7 @@ async function writeBtsConfig(projectConfig) {
1859
1900
  addons: projectConfig.addons,
1860
1901
  examples: projectConfig.examples,
1861
1902
  auth: projectConfig.auth,
1903
+ payments: projectConfig.payments,
1862
1904
  packageManager: projectConfig.packageManager,
1863
1905
  dbSetup: projectConfig.dbSetup,
1864
1906
  api: projectConfig.api,
@@ -2483,6 +2525,7 @@ async function detectProjectConfig(projectDir) {
2483
2525
  addons: btsConfig.addons,
2484
2526
  examples: btsConfig.examples,
2485
2527
  auth: btsConfig.auth,
2528
+ payments: btsConfig.payments,
2486
2529
  packageManager: btsConfig.packageManager,
2487
2530
  dbSetup: btsConfig.dbSetup,
2488
2531
  api: btsConfig.api,
@@ -2863,6 +2906,49 @@ async function setupAuthTemplate(projectDir, context) {
2863
2906
  }
2864
2907
  }
2865
2908
  }
2909
+ async function setupPaymentsTemplate(projectDir, context) {
2910
+ if (!context.payments || context.payments === "none") return;
2911
+ const serverAppDir = path.join(projectDir, "apps/server");
2912
+ const webAppDir = path.join(projectDir, "apps/web");
2913
+ const serverAppDirExists = await fs.pathExists(serverAppDir);
2914
+ const webAppDirExists = await fs.pathExists(webAppDir);
2915
+ if (serverAppDirExists && context.backend !== "convex") {
2916
+ const paymentsServerSrc = path.join(PKG_ROOT, `templates/payments/${context.payments}/server/base`);
2917
+ if (await fs.pathExists(paymentsServerSrc)) await processAndCopyFiles("**/*", paymentsServerSrc, serverAppDir, context);
2918
+ }
2919
+ const hasReactWeb = context.frontend.some((f) => [
2920
+ "tanstack-router",
2921
+ "react-router",
2922
+ "tanstack-start",
2923
+ "next"
2924
+ ].includes(f));
2925
+ const hasNuxtWeb = context.frontend.includes("nuxt");
2926
+ const hasSvelteWeb = context.frontend.includes("svelte");
2927
+ const hasSolidWeb = context.frontend.includes("solid");
2928
+ if (webAppDirExists && (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb)) {
2929
+ if (hasReactWeb) {
2930
+ const reactFramework = context.frontend.find((f) => [
2931
+ "tanstack-router",
2932
+ "react-router",
2933
+ "tanstack-start",
2934
+ "next"
2935
+ ].includes(f));
2936
+ if (reactFramework) {
2937
+ const paymentsWebSrc = path.join(PKG_ROOT, `templates/payments/${context.payments}/web/react/${reactFramework}`);
2938
+ if (await fs.pathExists(paymentsWebSrc)) await processAndCopyFiles("**/*", paymentsWebSrc, webAppDir, context);
2939
+ }
2940
+ } else if (hasNuxtWeb) {
2941
+ const paymentsWebNuxtSrc = path.join(PKG_ROOT, `templates/payments/${context.payments}/web/nuxt`);
2942
+ if (await fs.pathExists(paymentsWebNuxtSrc)) await processAndCopyFiles("**/*", paymentsWebNuxtSrc, webAppDir, context);
2943
+ } else if (hasSvelteWeb) {
2944
+ const paymentsWebSvelteSrc = path.join(PKG_ROOT, `templates/payments/${context.payments}/web/svelte`);
2945
+ if (await fs.pathExists(paymentsWebSvelteSrc)) await processAndCopyFiles("**/*", paymentsWebSvelteSrc, webAppDir, context);
2946
+ } else if (hasSolidWeb) {
2947
+ const paymentsWebSolidSrc = path.join(PKG_ROOT, `templates/payments/${context.payments}/web/solid`);
2948
+ if (await fs.pathExists(paymentsWebSolidSrc)) await processAndCopyFiles("**/*", paymentsWebSolidSrc, webAppDir, context);
2949
+ }
2950
+ }
2951
+ }
2866
2952
  async function setupAddonsTemplate(projectDir, context) {
2867
2953
  if (!context.addons || context.addons.length === 0) return;
2868
2954
  for (const addon of context.addons) {
@@ -3057,6 +3143,7 @@ async function addAddonsToProject(input) {
3057
3143
  addons: input.addons,
3058
3144
  examples: detectedConfig.examples || [],
3059
3145
  auth: detectedConfig.auth || "none",
3146
+ payments: detectedConfig.payments || "none",
3060
3147
  git: false,
3061
3148
  packageManager: input.packageManager || detectedConfig.packageManager || "npm",
3062
3149
  install: input.install || false,
@@ -3843,6 +3930,7 @@ async function addDeploymentToProject(input) {
3843
3930
  addons: detectedConfig.addons || [],
3844
3931
  examples: detectedConfig.examples || [],
3845
3932
  auth: detectedConfig.auth || "none",
3933
+ payments: detectedConfig.payments || "none",
3846
3934
  git: false,
3847
3935
  packageManager: input.packageManager || detectedConfig.packageManager || "npm",
3848
3936
  install: input.install || false,
@@ -4384,6 +4472,16 @@ async function setupEnvironmentVariables(config) {
4384
4472
  key: "GOOGLE_GENERATIVE_AI_API_KEY",
4385
4473
  value: "",
4386
4474
  condition: examples?.includes("ai") || false
4475
+ },
4476
+ {
4477
+ key: "POLAR_ACCESS_TOKEN",
4478
+ value: "",
4479
+ condition: config.payments === "polar"
4480
+ },
4481
+ {
4482
+ key: "POLAR_SUCCESS_URL",
4483
+ value: `${corsOrigin}/success?checkout_id={CHECKOUT_ID}`,
4484
+ condition: config.payments === "polar"
4387
4485
  }
4388
4486
  ];
4389
4487
  await addEnvVariablesToFile(envPath, serverVars);
@@ -5944,6 +6042,39 @@ async function initializeGit(projectDir, useGit) {
5944
6042
  await $({ cwd: projectDir })`git commit -m ${"initial commit"}`;
5945
6043
  }
5946
6044
 
6045
+ //#endregion
6046
+ //#region src/helpers/core/payments-setup.ts
6047
+ async function setupPayments(config) {
6048
+ const { payments, projectDir, frontend } = config;
6049
+ if (!payments || payments === "none") return;
6050
+ const serverDir = path.join(projectDir, "apps/server");
6051
+ const clientDir = path.join(projectDir, "apps/web");
6052
+ const serverDirExists = await fs.pathExists(serverDir);
6053
+ const clientDirExists = await fs.pathExists(clientDir);
6054
+ if (!serverDirExists) return;
6055
+ if (payments === "polar") {
6056
+ await addPackageDependency({
6057
+ dependencies: ["@polar-sh/better-auth", "@polar-sh/sdk"],
6058
+ projectDir: serverDir
6059
+ });
6060
+ if (clientDirExists) {
6061
+ const hasWebFrontend$1 = frontend.some((f) => [
6062
+ "react-router",
6063
+ "tanstack-router",
6064
+ "tanstack-start",
6065
+ "next",
6066
+ "nuxt",
6067
+ "svelte",
6068
+ "solid"
6069
+ ].includes(f));
6070
+ if (hasWebFrontend$1) await addPackageDependency({
6071
+ dependencies: ["@polar-sh/better-auth"],
6072
+ projectDir: clientDir
6073
+ });
6074
+ }
6075
+ }
6076
+ }
6077
+
5947
6078
  //#endregion
5948
6079
  //#region src/utils/docker-utils.ts
5949
6080
  async function isDockerInstalled() {
@@ -6011,6 +6142,7 @@ async function displayPostInstallInstructions(config) {
6011
6142
  const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
6012
6143
  const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
6013
6144
  const clerkInstructions = isConvex && config.auth === "clerk" ? getClerkInstructions() : "";
6145
+ const polarInstructions = config.payments === "polar" && config.auth === "better-auth" ? getPolarInstructions() : "";
6014
6146
  const wranglerDeployInstructions = getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy);
6015
6147
  const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy);
6016
6148
  const hasWeb = frontend?.some((f) => [
@@ -6063,6 +6195,7 @@ async function displayPostInstallInstructions(config) {
6063
6195
  if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
6064
6196
  if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
6065
6197
  if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
6198
+ if (polarInstructions) output += `\n${polarInstructions.trim()}\n`;
6066
6199
  if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
6067
6200
  if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
6068
6201
  output += `\n${pc.bold("Update all dependencies:\n")}${pc.cyan(tazeCommand)}\n\n`;
@@ -6151,6 +6284,9 @@ function getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy) {
6151
6284
  function getClerkInstructions() {
6152
6285
  return `${pc.bold("Clerk Authentication Setup:")}\n${pc.cyan("•")} Follow the guide: ${pc.underline("https://docs.convex.dev/auth/clerk")}\n${pc.cyan("•")} Set CLERK_JWT_ISSUER_DOMAIN in Convex Dashboard\n${pc.cyan("•")} Set CLERK_PUBLISHABLE_KEY in apps/*/.env`;
6153
6286
  }
6287
+ function getPolarInstructions() {
6288
+ return `${pc.bold("Polar Payments Setup:")}\n${pc.cyan("•")} Get access token & product ID from ${pc.underline("https://sandbox.polar.sh/")}\n${pc.cyan("•")} Set POLAR_ACCESS_TOKEN in apps/server/.env`;
6289
+ }
6154
6290
  function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy) {
6155
6291
  const instructions = [];
6156
6292
  if (webDeploy === "alchemy" && serverDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy web with Alchemy:")}\n${pc.cyan("•")} Dev: ${`cd apps/web && ${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`cd apps/web && ${runCmd} destroy`}`);
@@ -6355,6 +6491,7 @@ async function createProject(options, cliInput) {
6355
6491
  await setupDockerComposeTemplates(projectDir, options);
6356
6492
  }
6357
6493
  await setupAuthTemplate(projectDir, options);
6494
+ if (options.payments && options.payments !== "none") await setupPaymentsTemplate(projectDir, options);
6358
6495
  if (options.examples.length > 0 && options.examples[0] !== "none") await setupExamplesTemplate(projectDir, options);
6359
6496
  await setupAddonsTemplate(projectDir, options);
6360
6497
  await setupDeploymentTemplates(projectDir, options);
@@ -6367,6 +6504,7 @@ async function createProject(options, cliInput) {
6367
6504
  }
6368
6505
  if (options.addons.length > 0 && options.addons[0] !== "none") await setupAddons(options);
6369
6506
  if (options.auth && options.auth !== "none") await setupAuth(options);
6507
+ if (options.payments && options.payments !== "none") await setupPayments(options);
6370
6508
  await handleExtras(projectDir, options);
6371
6509
  await setupEnvironmentVariables(options);
6372
6510
  await updatePackageConfigurations(projectDir, options);
@@ -6445,6 +6583,7 @@ async function createProjectHandler(input) {
6445
6583
  addons: [],
6446
6584
  examples: [],
6447
6585
  auth: "none",
6586
+ payments: "none",
6448
6587
  git: false,
6449
6588
  packageManager: "npm",
6450
6589
  install: false,
@@ -6552,7 +6691,7 @@ async function addAddonsHandler(input) {
6552
6691
  const detectedConfig = await detectProjectConfig(projectDir);
6553
6692
  if (!detectedConfig) exitWithError("Could not detect project configuration. Please ensure this is a valid Better-T-Stack project.");
6554
6693
  if (!input.addons || input.addons.length === 0) {
6555
- const addonsPrompt = await getAddonsToAdd(detectedConfig.frontend || [], detectedConfig.addons || []);
6694
+ const addonsPrompt = await getAddonsToAdd(detectedConfig.frontend || [], detectedConfig.addons || [], detectedConfig.auth);
6556
6695
  if (addonsPrompt.length > 0) input.addons = addonsPrompt;
6557
6696
  }
6558
6697
  if (!input.webDeploy) {
@@ -6691,6 +6830,7 @@ const router = t.router({
6691
6830
  database: DatabaseSchema.optional(),
6692
6831
  orm: ORMSchema.optional(),
6693
6832
  auth: AuthSchema.optional(),
6833
+ payments: PaymentsSchema.optional(),
6694
6834
  frontend: z.array(FrontendSchema).optional(),
6695
6835
  addons: z.array(AddonsSchema).optional(),
6696
6836
  examples: z.array(ExamplesSchema).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "2.45.5",
3
+ "version": "2.46.0",
4
4
  "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
5
5
  "type": "module",
6
6
  "license": "MIT",