create-ej-app 26.0.0 → 28.0.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 (357) hide show
  1. package/README.md +125 -226
  2. package/cli.ts +79 -29
  3. package/package.json +2 -2
  4. package/template/api/.vscode/settings.json +1 -1
  5. package/template/api/{.github/copilot-instructions.md → AGENTS.md} +0 -2
  6. package/template/api/CLAUDE.md +1 -0
  7. package/template/api/package.json +10 -10
  8. package/template/nextjs/.dockerignore +1 -0
  9. package/template/nextjs/AGENTS.md +260 -0
  10. package/template/nextjs/CLAUDE.md +1 -0
  11. package/template/nextjs/README.md +109 -0
  12. package/template/nextjs/bunfig.toml +3 -0
  13. package/template/nextjs/eslint.config.mjs +6 -5
  14. package/template/nextjs/gitignore +6 -1
  15. package/template/nextjs/next.config.ts +8 -1
  16. package/template/nextjs/package.json +47 -39
  17. package/template/nextjs/public/logo-dark.svg +4 -0
  18. package/template/nextjs/public/logo.svg +4 -0
  19. package/template/nextjs/src/app/(admin)/actions/users.ts +183 -0
  20. package/template/nextjs/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
  21. package/template/nextjs/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
  22. package/template/nextjs/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
  23. package/template/nextjs/src/app/(admin)/admin/layout.tsx +53 -0
  24. package/template/nextjs/src/app/(admin)/admin/page.tsx +11 -2
  25. package/template/nextjs/src/app/(admin)/admin/settings/layout.tsx +24 -0
  26. package/template/nextjs/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
  27. package/template/nextjs/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
  28. package/template/nextjs/src/app/(admin)/admin/settings/users/page.tsx +377 -0
  29. package/template/nextjs/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
  30. package/template/nextjs/src/app/(admin)/admin/settings/users/validation.ts +23 -0
  31. package/template/nextjs/src/app/(app)/app/layout.tsx +44 -0
  32. package/template/nextjs/src/app/(app)/app/page.tsx +14 -2
  33. package/template/nextjs/src/app/(auth)/(account)/layout.tsx +64 -0
  34. package/template/nextjs/src/app/(auth)/(account)/profile/page.tsx +16 -0
  35. package/template/nextjs/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
  36. package/template/nextjs/src/app/(auth)/login/email-login-form.tsx +56 -29
  37. package/template/nextjs/src/app/(auth)/login/page.tsx +45 -12
  38. package/template/nextjs/src/app/(auth)/login/social-login-buttons.tsx +85 -17
  39. package/template/nextjs/src/app/(marketing)/layout.tsx +35 -4
  40. package/template/nextjs/src/app/(marketing)/legal.tsx +56 -0
  41. package/template/nextjs/src/app/(marketing)/page.tsx +20 -2
  42. package/template/nextjs/src/app/(marketing)/privacy/page.tsx +43 -2
  43. package/template/nextjs/src/app/(marketing)/terms/page.tsx +33 -2
  44. package/template/nextjs/src/app/error.tsx +37 -0
  45. package/template/nextjs/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
  46. package/template/nextjs/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
  47. package/template/nextjs/src/app/fonts/index.ts +60 -0
  48. package/template/nextjs/src/app/global-error.tsx +38 -0
  49. package/template/nextjs/src/app/globals.css +29 -0
  50. package/template/nextjs/src/app/layout.tsx +45 -18
  51. package/template/nextjs/src/app/not-found.tsx +25 -3
  52. package/template/nextjs/src/app/robots.ts +29 -0
  53. package/template/nextjs/src/app/sitemap.ts +21 -0
  54. package/template/nextjs/src/components/confirm-dialog.tsx +79 -0
  55. package/template/nextjs/src/components/dashboard-header.tsx +70 -0
  56. package/template/nextjs/src/components/data-table/enum.ts +5 -0
  57. package/template/nextjs/src/components/data-table/features.ts +49 -0
  58. package/template/nextjs/src/components/data-table/index.tsx +961 -0
  59. package/template/nextjs/src/components/data-table/types.ts +76 -0
  60. package/template/nextjs/src/components/error.tsx +71 -0
  61. package/template/nextjs/src/components/loader.tsx +18 -2
  62. package/template/nextjs/src/components/logo.tsx +49 -0
  63. package/template/nextjs/src/components/navigation-search.tsx +140 -0
  64. package/template/nextjs/src/components/navigation-sidebar.tsx +241 -0
  65. package/template/nextjs/src/components/page-heading.tsx +38 -0
  66. package/template/nextjs/src/components/react-query-provider.tsx +41 -2
  67. package/template/nextjs/src/components/route-error.tsx +55 -0
  68. package/template/nextjs/src/components/screen-size.tsx +36 -0
  69. package/template/nextjs/src/components/site-footer.tsx +20 -4
  70. package/template/nextjs/src/components/theme-provider.tsx +48 -0
  71. package/template/nextjs/src/components/theme-toggle.tsx +58 -5
  72. package/template/nextjs/src/components/ui/alert-dialog.tsx +199 -0
  73. package/template/nextjs/src/components/ui/avatar.tsx +112 -0
  74. package/template/nextjs/src/components/ui/badge.tsx +54 -0
  75. package/template/nextjs/src/components/ui/button.tsx +3 -1
  76. package/template/nextjs/src/components/ui/card.tsx +4 -4
  77. package/template/nextjs/src/components/ui/collapsible.tsx +33 -0
  78. package/template/nextjs/src/components/ui/command.tsx +192 -0
  79. package/template/nextjs/src/components/ui/dialog.tsx +164 -0
  80. package/template/nextjs/src/components/ui/dropdown-menu.tsx +273 -0
  81. package/template/nextjs/src/components/ui/input-group.tsx +156 -0
  82. package/template/nextjs/src/components/ui/select.tsx +195 -0
  83. package/template/nextjs/src/components/ui/sheet.tsx +143 -0
  84. package/template/nextjs/src/components/ui/sidebar.tsx +702 -0
  85. package/template/nextjs/src/components/ui/skeleton.tsx +13 -0
  86. package/template/nextjs/src/components/ui/table.tsx +116 -0
  87. package/template/nextjs/src/components/ui/textarea.tsx +18 -0
  88. package/template/nextjs/src/components/ui/tooltip.tsx +57 -0
  89. package/template/nextjs/src/components/user-menu.tsx +206 -0
  90. package/template/nextjs/src/db/common.ts +10 -0
  91. package/template/nextjs/src/db/index.ts +16 -6
  92. package/template/nextjs/src/db/relations.ts +21 -0
  93. package/template/nextjs/src/db/schema.ts +85 -46
  94. package/template/nextjs/src/emails/components/email-layout.tsx +27 -11
  95. package/template/nextjs/src/emails/magic-link.tsx +15 -13
  96. package/template/nextjs/src/emails/test.tsx +11 -9
  97. package/template/nextjs/src/emails/welcome.tsx +12 -9
  98. package/template/nextjs/src/env.ts +1 -1
  99. package/template/nextjs/src/hooks/use-mobile.ts +14 -12
  100. package/template/nextjs/src/instrumentation.ts +20 -2
  101. package/template/nextjs/src/lib/actions.ts +55 -0
  102. package/template/nextjs/src/lib/admin-navigation.ts +43 -0
  103. package/template/nextjs/src/lib/auth-client.ts +20 -9
  104. package/template/nextjs/src/lib/auth-permissions.ts +61 -0
  105. package/template/nextjs/src/lib/auth.ts +33 -3
  106. package/template/nextjs/src/lib/callback-url.ts +28 -0
  107. package/template/nextjs/src/lib/constants.ts +21 -0
  108. package/template/nextjs/src/lib/delay.ts +10 -0
  109. package/template/nextjs/src/lib/email-service.ts +21 -0
  110. package/template/nextjs/src/lib/errors.ts +74 -0
  111. package/template/nextjs/src/lib/format-currency.ts +22 -24
  112. package/template/nextjs/src/lib/format-date.ts +69 -6
  113. package/template/nextjs/src/lib/nodemailer.ts +27 -3
  114. package/template/nextjs/src/lib/otel.ts +92 -2
  115. package/template/nextjs/src/lib/safe-action-client.ts +60 -9
  116. package/template/nextjs/src/lib/safe-action.ts +152 -16
  117. package/template/nextjs/src/lib/search.ts +16 -0
  118. package/template/nextjs/src/lib/seo.ts +7 -0
  119. package/template/nextjs/src/lib/siteConfig.ts +17 -1
  120. package/template/nextjs/src/lib/toast-message.ts +87 -1
  121. package/template/nextjs/src/lib/validations.ts +30 -0
  122. package/template/nextjs/src/proxy.ts +48 -0
  123. package/template/nextjs/src/scripts/reset-db.ts +22 -0
  124. package/template/nextjs/src/scripts/seed.ts +59 -8
  125. package/template/nextjs/src/scripts/test-mail.ts +28 -20
  126. package/template/nextjs/tests/admin/users.test.ts +64 -0
  127. package/template/nextjs/tests/db/fk-indexes.test.ts +62 -0
  128. package/template/nextjs/tests/db/seed.test.ts +49 -0
  129. package/template/nextjs/tests/helpers/action.ts +71 -0
  130. package/template/nextjs/tests/helpers/fixtures.ts +78 -0
  131. package/template/nextjs/tests/helpers/index.ts +2 -0
  132. package/template/nextjs/tests/lib/callback-url.test.ts +48 -0
  133. package/template/nextjs/tests/lib/client-server-boundary.test.ts +57 -0
  134. package/template/nextjs/tests/lib/errors.test.ts +10 -0
  135. package/template/nextjs/tests/lib/search.test.ts +23 -0
  136. package/template/nextjs/tests/setup/pglite-db.ts +63 -0
  137. package/template/nextjs/tests/setup/preload.ts +95 -0
  138. package/template/nextjs/tsconfig.json +1 -0
  139. package/template/saas-starter/.dockerignore +1 -0
  140. package/template/saas-starter/.env.example +15 -15
  141. package/template/saas-starter/AGENTS.md +302 -0
  142. package/template/saas-starter/CLAUDE.md +1 -0
  143. package/template/saas-starter/README.md +124 -2
  144. package/template/saas-starter/bunfig.toml +3 -0
  145. package/template/saas-starter/eslint.config.mjs +6 -5
  146. package/template/saas-starter/gitignore +6 -1
  147. package/template/saas-starter/next.config.ts +19 -1
  148. package/template/saas-starter/package.json +50 -44
  149. package/template/saas-starter/public/logo-dark.svg +4 -0
  150. package/template/saas-starter/public/logo.svg +4 -0
  151. package/template/saas-starter/src/app/(admin)/actions/contact-leads.ts +51 -0
  152. package/template/saas-starter/src/app/(admin)/actions/feedbacks.ts +91 -0
  153. package/template/saas-starter/src/app/(admin)/actions/users.ts +145 -59
  154. package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +4 -1
  155. package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +12 -10
  156. package/template/saas-starter/src/app/(admin)/admin/contact-leads/page.tsx +123 -0
  157. package/template/saas-starter/src/app/(admin)/admin/feedbacks/page.tsx +161 -0
  158. package/template/saas-starter/src/app/(admin)/admin/layout.tsx +24 -28
  159. package/template/saas-starter/src/app/(admin)/admin/page.tsx +59 -2
  160. package/template/saas-starter/src/app/(admin)/admin/settings/layout.tsx +24 -0
  161. package/template/saas-starter/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
  162. package/template/saas-starter/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
  163. package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +372 -10
  164. package/template/saas-starter/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
  165. package/template/saas-starter/src/app/(admin)/admin/settings/users/validation.ts +23 -0
  166. package/template/saas-starter/src/app/(admin)/admin/support-tickets/[ticketId]/page.tsx +17 -0
  167. package/template/saas-starter/src/app/(admin)/admin/support-tickets/page.tsx +5 -0
  168. package/template/saas-starter/src/app/(app)/actions/billing.ts +171 -107
  169. package/template/saas-starter/src/app/(app)/actions/feedbacks.ts +28 -0
  170. package/template/saas-starter/src/app/(app)/actions/feedbacks.validation.ts +12 -0
  171. package/template/saas-starter/src/app/(app)/actions/members.ts +149 -123
  172. package/template/saas-starter/src/app/(app)/actions/organizations.ts +230 -0
  173. package/template/saas-starter/src/app/(app)/actions/organizations.validation.ts +17 -0
  174. package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +6 -9
  175. package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +6 -9
  176. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +45 -53
  177. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +40 -47
  178. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +80 -105
  179. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +72 -82
  180. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +51 -52
  181. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/layout.tsx +24 -0
  182. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +15 -16
  183. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +77 -44
  184. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +11 -18
  185. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +14 -28
  186. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/[ticketId]/page.tsx +25 -0
  187. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/page.tsx +25 -0
  188. package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +15 -13
  189. package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +15 -6
  190. package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +6 -3
  191. package/template/saas-starter/src/app/(app)/app/_components/feedback-dialog.tsx +108 -0
  192. package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +5 -9
  193. package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +68 -8
  194. package/template/saas-starter/src/app/(app)/app/layout.tsx +44 -32
  195. package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +75 -87
  196. package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +10 -12
  197. package/template/saas-starter/src/app/(app)/app/page.tsx +16 -21
  198. package/template/saas-starter/src/app/(auth)/(account)/layout.tsx +64 -0
  199. package/template/saas-starter/src/app/(auth)/(account)/profile/page.tsx +16 -0
  200. package/template/saas-starter/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
  201. package/template/saas-starter/src/app/(auth)/(account)/support/[ticketId]/page.tsx +19 -0
  202. package/template/saas-starter/src/app/(auth)/(account)/support/page.tsx +9 -0
  203. package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +49 -27
  204. package/template/saas-starter/src/app/(auth)/login/page.tsx +39 -21
  205. package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +80 -18
  206. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +16 -10
  207. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +28 -42
  208. package/template/saas-starter/src/app/(invitation)/actions/invitations.ts +32 -0
  209. package/template/saas-starter/src/app/(marketing)/contact-us/actions.ts +23 -0
  210. package/template/saas-starter/src/app/(marketing)/contact-us/contact-form.tsx +118 -0
  211. package/template/saas-starter/src/app/(marketing)/contact-us/page.tsx +20 -0
  212. package/template/saas-starter/src/app/(marketing)/contact-us/validation.ts +22 -0
  213. package/template/saas-starter/src/app/(marketing)/layout.tsx +38 -4
  214. package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
  215. package/template/saas-starter/src/app/(marketing)/page.tsx +23 -2
  216. package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +62 -2
  217. package/template/saas-starter/src/app/(marketing)/terms/page.tsx +40 -2
  218. package/template/saas-starter/src/app/actions/support-tickets.ts +510 -0
  219. package/template/saas-starter/src/app/actions/support-tickets.validation.ts +112 -0
  220. package/template/saas-starter/src/app/actions/uploads.ts +36 -0
  221. package/template/saas-starter/src/app/actions/uploads.validation.ts +24 -0
  222. package/template/saas-starter/src/app/error.tsx +37 -0
  223. package/template/saas-starter/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
  224. package/template/saas-starter/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
  225. package/template/saas-starter/src/app/fonts/index.ts +60 -0
  226. package/template/saas-starter/src/app/global-error.tsx +38 -0
  227. package/template/saas-starter/src/app/globals.css +29 -0
  228. package/template/saas-starter/src/app/layout.tsx +43 -19
  229. package/template/saas-starter/src/app/not-found.tsx +25 -3
  230. package/template/saas-starter/src/app/robots.ts +14 -10
  231. package/template/saas-starter/src/app/sitemap.ts +2 -1
  232. package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
  233. package/template/saas-starter/src/components/dashboard-header.tsx +7 -1
  234. package/template/saas-starter/src/components/data-table/features.ts +49 -0
  235. package/template/saas-starter/src/components/data-table/index.tsx +65 -64
  236. package/template/saas-starter/src/components/data-table/types.ts +5 -4
  237. package/template/saas-starter/src/components/error.tsx +45 -0
  238. package/template/saas-starter/src/components/file-upload.tsx +65 -26
  239. package/template/saas-starter/src/components/loader.tsx +18 -2
  240. package/template/saas-starter/src/components/logo.tsx +49 -0
  241. package/template/saas-starter/src/components/navigation-search.tsx +2 -2
  242. package/template/saas-starter/src/components/page-heading.tsx +6 -0
  243. package/template/saas-starter/src/components/react-query-provider.tsx +41 -2
  244. package/template/saas-starter/src/components/route-error.tsx +55 -0
  245. package/template/saas-starter/src/components/screen-size.tsx +36 -0
  246. package/template/saas-starter/src/components/site-footer.tsx +20 -4
  247. package/template/saas-starter/src/components/support/create-support-ticket-dialog.tsx +122 -0
  248. package/template/saas-starter/src/components/support/support-ticket-conversation.tsx +236 -0
  249. package/template/saas-starter/src/components/support/support-ticket-shared.tsx +58 -0
  250. package/template/saas-starter/src/components/support/support-tickets-table.tsx +281 -0
  251. package/template/saas-starter/src/components/theme-provider.tsx +48 -0
  252. package/template/saas-starter/src/components/theme-toggle.tsx +58 -5
  253. package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
  254. package/template/saas-starter/src/components/ui/badge.tsx +6 -1
  255. package/template/saas-starter/src/components/ui/button.tsx +2 -0
  256. package/template/saas-starter/src/components/ui/command.tsx +1 -1
  257. package/template/saas-starter/src/components/user-menu.tsx +147 -24
  258. package/template/saas-starter/src/db/common.ts +33 -1
  259. package/template/saas-starter/src/db/enums.ts +24 -0
  260. package/template/saas-starter/src/db/index.ts +14 -5
  261. package/template/saas-starter/src/db/relations.ts +40 -3
  262. package/template/saas-starter/src/db/schema.ts +186 -35
  263. package/template/saas-starter/src/emails/components/email-layout.tsx +27 -11
  264. package/template/saas-starter/src/emails/invitation.tsx +34 -21
  265. package/template/saas-starter/src/emails/magic-link.tsx +15 -13
  266. package/template/saas-starter/src/emails/test.tsx +11 -9
  267. package/template/saas-starter/src/emails/welcome.tsx +12 -9
  268. package/template/saas-starter/src/env.ts +1 -1
  269. package/template/saas-starter/src/hooks/use-mobile.ts +14 -12
  270. package/template/saas-starter/src/instrumentation.ts +20 -2
  271. package/template/saas-starter/src/lib/actions.ts +34 -0
  272. package/template/saas-starter/src/lib/admin-navigation.ts +33 -1
  273. package/template/saas-starter/src/lib/app-navigation.ts +45 -41
  274. package/template/saas-starter/src/lib/auth-client.ts +8 -3
  275. package/template/saas-starter/src/lib/auth-permissions.ts +50 -11
  276. package/template/saas-starter/src/lib/auth.ts +122 -21
  277. package/template/saas-starter/src/lib/billing.ts +59 -24
  278. package/template/saas-starter/src/lib/callback-url.ts +28 -0
  279. package/template/saas-starter/src/lib/constants.ts +78 -9
  280. package/template/saas-starter/src/lib/delay.ts +10 -0
  281. package/template/saas-starter/src/lib/email-service.ts +30 -1
  282. package/template/saas-starter/src/lib/errors.ts +74 -0
  283. package/template/saas-starter/src/lib/format-currency.ts +22 -24
  284. package/template/saas-starter/src/lib/format-date.ts +69 -6
  285. package/template/saas-starter/src/lib/image-url.ts +54 -0
  286. package/template/saas-starter/src/lib/nodemailer.ts +27 -3
  287. package/template/saas-starter/src/lib/organization-access.ts +56 -76
  288. package/template/saas-starter/src/lib/otel.ts +92 -2
  289. package/template/saas-starter/src/lib/plan-limits.ts +37 -0
  290. package/template/saas-starter/src/lib/plans.ts +40 -13
  291. package/template/saas-starter/src/lib/rbac.ts +44 -0
  292. package/template/saas-starter/src/lib/safe-action-client.ts +60 -9
  293. package/template/saas-starter/src/lib/safe-action.ts +137 -17
  294. package/template/saas-starter/src/lib/search.ts +16 -0
  295. package/template/saas-starter/src/lib/seo.ts +7 -0
  296. package/template/saas-starter/src/lib/siteConfig.ts +17 -1
  297. package/template/saas-starter/src/lib/storage.ts +176 -46
  298. package/template/saas-starter/src/lib/stripe.ts +4 -1
  299. package/template/saas-starter/src/lib/toast-message.ts +87 -1
  300. package/template/saas-starter/src/lib/validations.ts +85 -2
  301. package/template/saas-starter/src/proxy.ts +48 -0
  302. package/template/saas-starter/src/scripts/cleanup-temp-uploads.ts +24 -0
  303. package/template/saas-starter/src/scripts/seed.ts +82 -8
  304. package/template/saas-starter/src/scripts/setup-db.ts +72 -0
  305. package/template/saas-starter/src/scripts/setup-stripe-plans.ts +13 -7
  306. package/template/saas-starter/src/scripts/test-mail.ts +28 -20
  307. package/template/saas-starter/tests/admin/users.test.ts +64 -0
  308. package/template/saas-starter/tests/db/fk-indexes.test.ts +62 -0
  309. package/template/saas-starter/tests/db/seed.test.ts +57 -0
  310. package/template/saas-starter/tests/helpers/action.ts +71 -0
  311. package/template/saas-starter/tests/helpers/fixtures.ts +151 -0
  312. package/template/saas-starter/tests/helpers/index.ts +2 -0
  313. package/template/saas-starter/tests/lib/callback-url.test.ts +48 -0
  314. package/template/saas-starter/tests/lib/client-server-boundary.test.ts +57 -0
  315. package/template/saas-starter/tests/lib/errors.test.ts +10 -0
  316. package/template/saas-starter/tests/lib/image-url.test.ts +31 -0
  317. package/template/saas-starter/tests/lib/search.test.ts +23 -0
  318. package/template/saas-starter/tests/marketing/contact-and-feedback.test.ts +86 -0
  319. package/template/saas-starter/tests/organizations/organizations.test.ts +175 -0
  320. package/template/saas-starter/tests/setup/pglite-db.ts +67 -0
  321. package/template/saas-starter/tests/setup/preload.ts +108 -0
  322. package/template/saas-starter/tests/support/support-tickets.test.ts +145 -0
  323. package/template/saas-starter/tsconfig.json +1 -0
  324. package/cli.js +0 -147
  325. package/template/nextjs/src/app/(admin)/layout.tsx +0 -20
  326. package/template/nextjs/src/app/(app)/layout.tsx +0 -20
  327. package/template/nextjs/terraform/artifact-registry.tf +0 -5
  328. package/template/nextjs/terraform/backend-service.tf +0 -13
  329. package/template/nextjs/terraform/cleanup.sh +0 -9
  330. package/template/nextjs/terraform/cloudbuild.tf +0 -70
  331. package/template/nextjs/terraform/cloudrun.tf +0 -68
  332. package/template/nextjs/terraform/database.tf +0 -60
  333. package/template/nextjs/terraform/forwarding-rule.tf +0 -43
  334. package/template/nextjs/terraform/gitignore +0 -4
  335. package/template/nextjs/terraform/ip-address.tf +0 -11
  336. package/template/nextjs/terraform/network-endpoint-group.tf +0 -11
  337. package/template/nextjs/terraform/network.tf +0 -21
  338. package/template/nextjs/terraform/pre.sh +0 -3
  339. package/template/nextjs/terraform/providers.tf +0 -18
  340. package/template/nextjs/terraform/secret.txt +0 -11
  341. package/template/nextjs/terraform/setup.sh +0 -25
  342. package/template/nextjs/terraform/ssl-certs.tf +0 -32
  343. package/template/nextjs/terraform/target-https-proxy copy.tf +0 -14
  344. package/template/nextjs/terraform/target-https-proxy.tf +0 -14
  345. package/template/nextjs/terraform/terraform.tfvars +0 -4
  346. package/template/nextjs/terraform/url-map.tf +0 -23
  347. package/template/nextjs/terraform/variables.tf +0 -21
  348. package/template/saas-starter/next-env.d.ts +0 -6
  349. package/template/saas-starter/src/actions/uploads.ts +0 -28
  350. package/template/saas-starter/src/actions/uploads.validation.ts +0 -14
  351. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +0 -183
  352. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +0 -129
  353. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +0 -415
  354. package/template/saas-starter/src/app/(app)/actions/organization.ts +0 -92
  355. package/template/saas-starter/src/app/proxy.ts +0 -28
  356. package/template/saas-starter/src/components/impersonation-banner.tsx +0 -44
  357. package/template/saas-starter/src/lib/organization-plan-limits.ts +0 -38
@@ -0,0 +1,37 @@
1
+ 'use client'
2
+
3
+ import { usePathname } from 'next/navigation'
4
+ import { useEffect } from 'react'
5
+ import { RouteError } from '~/components/route-error'
6
+
7
+ /**
8
+ * Root segment error boundary.
9
+ *
10
+ * Catches runtime errors in pages and nested layouts under the root layout.
11
+ * Does not catch errors thrown in the root layout itself; see `global-error.tsx`.
12
+ */
13
+ export default function Error({
14
+ error,
15
+ unstable_retry
16
+ }: {
17
+ error: Error & { digest?: string }
18
+ unstable_retry: () => void
19
+ }) {
20
+ const pathname = usePathname()
21
+ const homeHref =
22
+ pathname?.startsWith('/admin') && pathname !== '/admin' ? '/admin' : '/'
23
+ const homeLabel = homeHref === '/admin' ? 'Back to Admin' : 'Go Home'
24
+
25
+ useEffect(() => {
26
+ console.error('Route error boundary:', error)
27
+ }, [error])
28
+
29
+ return (
30
+ <RouteError
31
+ error={error}
32
+ onRetry={unstable_retry}
33
+ homeHref={homeHref}
34
+ homeLabel={homeLabel}
35
+ />
36
+ )
37
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @fileoverview Global font definitions shared across all route groups.
3
+ *
4
+ * Fonts are defined here in a single module so that `next/font/local` generates
5
+ * a stable CSS module hash regardless of which layout imports them. Defining
6
+ * fonts inside individual route-group layouts (e.g. `(marketing)` or `(admin)`)
7
+ * causes the server and client to produce different class name hashes, leading
8
+ * to React hydration mismatches.
9
+ *
10
+ * Usage:
11
+ * import { geistMono, geistSans } from '~/app/fonts'
12
+ * <body className={`${geistSans.variable} ${geistMono.variable}`}>
13
+ *
14
+ * @module app/fonts/index
15
+ */
16
+
17
+ import localFont from 'next/font/local'
18
+
19
+ /**
20
+ * Geist Sans - primary variable font for body and UI text.
21
+ *
22
+ * Exposed as the CSS custom property `--font-geist-sans` so it can be
23
+ * referenced in Tailwind via `font-sans`.
24
+ *
25
+ * @see https://vercel.com/font/sans
26
+ */
27
+ export const geistSans = localFont({
28
+ src: '../fonts/geist-sans/Geist-Variable.woff2',
29
+ variable: '--font-geist-sans',
30
+ weight: '100 900'
31
+ })
32
+
33
+ /**
34
+ * Geist Mono - monospace variable font for code blocks, badges, and
35
+ * any UI element that benefits from fixed-width rendering.
36
+ *
37
+ * Exposed as the CSS custom property `--font-geist-mono`.
38
+ *
39
+ * `adjustFontFallback` is disabled because the automatic metric adjustment
40
+ * produces undesirable layout shifts with the custom fallback stack below.
41
+ *
42
+ * @see https://vercel.com/font/mono
43
+ */
44
+ export const geistMono = localFont({
45
+ src: '../fonts/geist-mono/GeistMono-Variable.woff2',
46
+ variable: '--font-geist-mono',
47
+ adjustFontFallback: false,
48
+ fallback: [
49
+ 'ui-monospace',
50
+ 'SFMono-Regular',
51
+ 'Roboto Mono',
52
+ 'Menlo',
53
+ 'Monaco',
54
+ 'Liberation Mono',
55
+ 'DejaVu Sans Mono',
56
+ 'Courier New',
57
+ 'monospace'
58
+ ],
59
+ weight: '100 900'
60
+ })
@@ -0,0 +1,38 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import { RouteError } from '~/components/route-error'
5
+ import { siteConfig } from '~/lib/siteConfig'
6
+ import { geistMono, geistSans } from './fonts'
7
+ import './globals.css'
8
+
9
+ /**
10
+ * Root layout error boundary.
11
+ *
12
+ * Replaces the entire root layout when an error is thrown there.
13
+ * Must define its own document shell (`html` / `body`) and styles.
14
+ */
15
+ export default function GlobalError({
16
+ error,
17
+ unstable_retry
18
+ }: {
19
+ error: Error & { digest?: string }
20
+ unstable_retry: () => void
21
+ }) {
22
+ useEffect(() => {
23
+ console.error('Global error boundary:', error)
24
+ }, [error])
25
+
26
+ return (
27
+ <html
28
+ lang="en"
29
+ suppressHydrationWarning
30
+ className={`${geistSans.variable} ${geistMono.variable}`}
31
+ >
32
+ <body>
33
+ <title>This Page Failed to Load | {siteConfig.name}</title>
34
+ <RouteError error={error} onRetry={unstable_retry} />
35
+ </body>
36
+ </html>
37
+ )
38
+ }
@@ -28,6 +28,14 @@
28
28
  --color-chart-3: var(--chart-3);
29
29
  --color-chart-4: var(--chart-4);
30
30
  --color-chart-5: var(--chart-5);
31
+
32
+ --color-success: var(--success);
33
+ --color-success-foreground: var(--success-foreground);
34
+ --color-warning: var(--warning);
35
+ --color-warning-foreground: var(--warning-foreground);
36
+ --color-info: var(--info);
37
+ --color-info-foreground: var(--info-foreground);
38
+
31
39
  --color-sidebar: var(--sidebar);
32
40
  --color-sidebar-foreground: var(--sidebar-foreground);
33
41
  --color-sidebar-primary: var(--sidebar-primary);
@@ -84,6 +92,13 @@
84
92
  --sidebar-accent-foreground: oklch(0.205 0 0);
85
93
  --sidebar-border: oklch(0.922 0 0);
86
94
  --sidebar-ring: oklch(0.708 0 0);
95
+
96
+ --success: oklch(0.552 0.152 149.579);
97
+ --success-foreground: oklch(1 0 0);
98
+ --warning: oklch(0.574 0.188 70.08);
99
+ --warning-foreground: oklch(1 0 0);
100
+ --info: oklch(0.569 0.165 254.624);
101
+ --info-foreground: oklch(1 0 0);
87
102
  }
88
103
 
89
104
  .dark {
@@ -119,6 +134,13 @@
119
134
  --sidebar-accent-foreground: oklch(0.985 0 0);
120
135
  --sidebar-border: oklch(1 0 0 / 10%);
121
136
  --sidebar-ring: oklch(0.556 0 0);
137
+
138
+ --success: oklch(0.723 0.192 149.579);
139
+ --success-foreground: oklch(0.145 0 0);
140
+ --warning: oklch(0.769 0.188 70.08);
141
+ --warning-foreground: oklch(0.205 0 0);
142
+ --info: oklch(0.707 0.165 254.624);
143
+ --info-foreground: oklch(0.205 0 0);
122
144
  }
123
145
 
124
146
  @layer base {
@@ -128,6 +150,7 @@
128
150
 
129
151
  html {
130
152
  @apply font-sans antialiased;
153
+ scroll-behavior: smooth;
131
154
  }
132
155
 
133
156
  body {
@@ -138,6 +161,12 @@
138
161
  [role='button']:not([disabled]) {
139
162
  cursor: pointer;
140
163
  }
164
+
165
+ @media (prefers-reduced-motion: reduce) {
166
+ html {
167
+ scroll-behavior: auto;
168
+ }
169
+ }
141
170
  }
142
171
 
143
172
  @theme inline {
@@ -1,27 +1,49 @@
1
- import type { Metadata } from 'next'
2
- import { Geist, Geist_Mono } from 'next/font/google'
1
+ /**
2
+ * @fileoverview Root layout: document shell, fonts, metadata, and the
3
+ * client providers every page shares (TanStack Query, theme, tooltips,
4
+ * toasts, and the navigation progress bar).
5
+ *
6
+ * @module app/layout
7
+ */
8
+
9
+ import type { Metadata, Viewport } from 'next'
3
10
  import { Loader } from '~/components/loader'
4
11
  import { ReactQueryProvider } from '~/components/react-query-provider'
5
12
  import { ScreenSize } from '~/components/screen-size'
6
13
  import { ThemeProvider } from '~/components/theme-provider'
7
14
  import { Toaster } from '~/components/ui/sonner'
15
+ import { TooltipProvider } from '~/components/ui/tooltip'
8
16
  import { env } from '~/env'
9
17
  import { siteConfig } from '~/lib/siteConfig'
18
+ import { geistMono, geistSans } from './fonts'
10
19
  import './globals.css'
11
20
 
12
- const geistSans = Geist({
13
- variable: '--font-geist-sans',
14
- subsets: ['latin']
15
- })
16
-
17
- const geistMono = Geist_Mono({
18
- variable: '--font-geist-mono',
19
- subsets: ['latin']
20
- })
21
-
22
21
  export const metadata: Metadata = {
23
- title: siteConfig.name,
24
- description: siteConfig.description
22
+ metadataBase: new URL(env.BETTER_AUTH_URL),
23
+ title: {
24
+ default: siteConfig.name,
25
+ template: `%s | ${siteConfig.name}`
26
+ },
27
+ description: siteConfig.description,
28
+ applicationName: siteConfig.name,
29
+ openGraph: {
30
+ type: 'website',
31
+ locale: 'en_US',
32
+ siteName: siteConfig.name,
33
+ description: siteConfig.description
34
+ },
35
+ twitter: {
36
+ card: 'summary_large_image',
37
+ description: siteConfig.description
38
+ }
39
+ }
40
+
41
+ /** Browser chrome colors, matching `--background` in globals.css. */
42
+ export const viewport: Viewport = {
43
+ themeColor: [
44
+ { media: '(prefers-color-scheme: light)', color: '#ffffff' },
45
+ { media: '(prefers-color-scheme: dark)', color: '#0a0a0a' }
46
+ ]
25
47
  }
26
48
 
27
49
  export default function RootLayout({
@@ -38,10 +60,12 @@ export default function RootLayout({
38
60
  <body>
39
61
  <ReactQueryProvider>
40
62
  <ThemeProvider>
41
- <Loader />
42
- {children}
43
- {env.APP_ENV === 'development' ? <ScreenSize /> : null}
44
- <Toaster />
63
+ <TooltipProvider>
64
+ <Loader />
65
+ {children}
66
+ {env.APP_ENV === 'development' ? <ScreenSize /> : null}
67
+ <Toaster />
68
+ </TooltipProvider>
45
69
  </ThemeProvider>
46
70
  </ReactQueryProvider>
47
71
  </body>
@@ -49,5 +73,5 @@ export default function RootLayout({
49
73
  )
50
74
  }
51
75
 
52
- /** Forces dynamic rendering - page will be re-rendered on each request */
76
+ /** Renders every request dynamically, so env-driven metadata stays current. */
53
77
  export const dynamic = 'force-dynamic'
@@ -1,12 +1,34 @@
1
+ /**
2
+ * @fileoverview 404 Not Found Page
3
+ *
4
+ * Global catch-all page displayed when a route is not found.
5
+ * Redirects users back to the appropriate home page based on the current route context.
6
+ *
7
+ * **Behavior:**
8
+ * - Admin routes (/admin/*) redirect to /admin
9
+ * - All other routes redirect to /
10
+ *
11
+ * @module app/not-found
12
+ */
13
+
1
14
  'use client'
2
15
 
3
16
  import Link from 'next/link'
4
17
  import { usePathname } from 'next/navigation'
5
18
  import { Button } from '~/components/ui/button'
6
19
 
20
+ /**
21
+ * Not Found Page Component
22
+ *
23
+ * Displays a 404 error message with context-aware home button.
24
+ * Uses the current pathname to determine the appropriate home route.
25
+ *
26
+ * @returns {JSX.Element} 404 error page with navigation button
27
+ */
7
28
  export default function NotFound() {
8
29
  const pathname = usePathname()
9
- const homeRoute = pathname?.startsWith('/admin') ? '/admin' : '/'
30
+ const homeRoute =
31
+ pathname?.startsWith('/admin') && pathname !== '/admin' ? '/admin' : '/'
10
32
 
11
33
  return (
12
34
  <main className="bg-background text-foreground flex min-h-screen flex-col">
@@ -15,10 +37,10 @@ export default function NotFound() {
15
37
  <h1 className="text-foreground mb-2 text-6xl font-bold">404</h1>
16
38
  <p className="text-muted-foreground mb-8 text-xl">Page not found</p>
17
39
  <p className="text-muted-foreground mb-8 max-w-md text-base">
18
- The page you're looking for doesn't exist.
40
+ The page you&apos;re looking for doesn&apos;t exist.
19
41
  </p>
20
42
  <Button asChild>
21
- <Link href={homeRoute}>Return to Home</Link>
43
+ <Link href={homeRoute}>Return Home</Link>
22
44
  </Button>
23
45
  </div>
24
46
  </section>
@@ -1,19 +1,16 @@
1
1
  import type { MetadataRoute } from 'next'
2
2
  import { env } from '~/env'
3
3
 
4
+ /** Renders every request dynamically, so the environment check is live. */
4
5
  export const dynamic = 'force-dynamic'
5
6
 
7
+ /**
8
+ * robots.txt. Staging blocks every crawler; production keeps signed-in
9
+ * surfaces and the API out of search results.
10
+ */
6
11
  export default function robots(): MetadataRoute.Robots {
7
- // Block all crawlers in staging to prevent accidental indexing
8
12
  if (env.APP_ENV === 'staging') {
9
- return {
10
- rules: [
11
- {
12
- userAgent: '*',
13
- disallow: '/'
14
- }
15
- ]
16
- }
13
+ return { rules: [{ userAgent: '*', disallow: '/' }] }
17
14
  }
18
15
 
19
16
  const baseUrl = env.BETTER_AUTH_URL
@@ -23,7 +20,14 @@ export default function robots(): MetadataRoute.Robots {
23
20
  {
24
21
  userAgent: '*',
25
22
  allow: '/',
26
- disallow: ['/api/', '/admin/', '/app/', '/accept-invitation/']
23
+ disallow: [
24
+ '/api/',
25
+ '/admin',
26
+ '/app',
27
+ '/accept-invitation',
28
+ '/profile',
29
+ '/support'
30
+ ]
27
31
  }
28
32
  ],
29
33
  sitemap: `${baseUrl}/sitemap.xml`,
@@ -3,13 +3,14 @@ import { env } from '~/env'
3
3
 
4
4
  export const dynamic = 'force-dynamic'
5
5
 
6
+ /** Public pages only. Staging returns an empty sitemap. */
6
7
  export default function sitemap(): MetadataRoute.Sitemap {
7
8
  if (env.APP_ENV === 'staging') return []
8
9
 
9
10
  const baseUrl = env.BETTER_AUTH_URL
10
11
  const lastModified = new Date()
11
12
 
12
- const routes = ['/', '/privacy', '/terms', '/login']
13
+ const routes = ['/', '/contact-us', '/privacy', '/terms', '/login']
13
14
 
14
15
  return routes.map(path => ({
15
16
  url: `${baseUrl}${path === '/' ? '' : path}`,
@@ -0,0 +1,79 @@
1
+ 'use client'
2
+
3
+ import { Loader2 } from 'lucide-react'
4
+ import type { ReactNode } from 'react'
5
+ import {
6
+ AlertDialog,
7
+ AlertDialogCancel,
8
+ AlertDialogContent,
9
+ AlertDialogDescription,
10
+ AlertDialogFooter,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle
13
+ } from '~/components/ui/alert-dialog'
14
+ import { Button } from '~/components/ui/button'
15
+
16
+ /**
17
+ * Controlled confirmation for destructive or hard-to-undo actions. Use it
18
+ * instead of `window.confirm`.
19
+ *
20
+ * The dialog stays open while `pending` is true; close it from the
21
+ * caller's success handler by clearing whatever drives `open`.
22
+ *
23
+ * @example
24
+ * <ConfirmDialog
25
+ * open={target !== null}
26
+ * onOpenChange={open => !open && setTarget(null)}
27
+ * title="Delete User"
28
+ * description="This permanently deletes the account."
29
+ * confirmLabel="Delete User"
30
+ * pending={isDeleting}
31
+ * onConfirm={() => deleteUser(target)}
32
+ * />
33
+ */
34
+ export function ConfirmDialog({
35
+ open,
36
+ onOpenChange,
37
+ title,
38
+ description,
39
+ confirmLabel,
40
+ pending = false,
41
+ destructive = true,
42
+ onConfirm
43
+ }: {
44
+ open: boolean
45
+ onOpenChange: (open: boolean) => void
46
+ title: string
47
+ description: ReactNode
48
+ confirmLabel: string
49
+ pending?: boolean
50
+ destructive?: boolean
51
+ onConfirm: () => void
52
+ }) {
53
+ return (
54
+ <AlertDialog
55
+ open={open}
56
+ onOpenChange={next => {
57
+ if (!pending) onOpenChange(next)
58
+ }}
59
+ >
60
+ <AlertDialogContent>
61
+ <AlertDialogHeader>
62
+ <AlertDialogTitle>{title}</AlertDialogTitle>
63
+ <AlertDialogDescription>{description}</AlertDialogDescription>
64
+ </AlertDialogHeader>
65
+ <AlertDialogFooter>
66
+ <AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
67
+ <Button
68
+ variant={destructive ? 'destructive' : 'default'}
69
+ disabled={pending}
70
+ onClick={onConfirm}
71
+ >
72
+ {pending ? <Loader2 className="animate-spin" /> : null}
73
+ {confirmLabel}
74
+ </Button>
75
+ </AlertDialogFooter>
76
+ </AlertDialogContent>
77
+ </AlertDialog>
78
+ )
79
+ }
@@ -20,6 +20,8 @@ type DashboardHeaderProps = {
20
20
  actions?: ReactNode
21
21
  fallbackName?: string
22
22
  fallbackInitials?: string
23
+ isSuperAdmin?: boolean
24
+ isImpersonating?: boolean
23
25
  }
24
26
 
25
27
  export function DashboardHeader({
@@ -27,7 +29,9 @@ export function DashboardHeader({
27
29
  search,
28
30
  actions,
29
31
  fallbackName,
30
- fallbackInitials
32
+ fallbackInitials,
33
+ isSuperAdmin,
34
+ isImpersonating
31
35
  }: DashboardHeaderProps) {
32
36
  return (
33
37
  <header className="bg-background sticky top-0 z-50 flex h-14 shrink-0 items-center gap-2 border-b px-4">
@@ -57,6 +61,8 @@ export function DashboardHeader({
57
61
  user={user}
58
62
  fallbackName={fallbackName}
59
63
  fallbackInitials={fallbackInitials}
64
+ isSuperAdmin={isSuperAdmin}
65
+ isImpersonating={isImpersonating}
60
66
  />
61
67
  </div>
62
68
  </header>
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Shared TanStack Table v9 feature set for the app DataTable.
3
+ *
4
+ * Defined once outside components so feature types stay stable and tree-shaken.
5
+ */
6
+ import {
7
+ type ColumnDef,
8
+ type RowData,
9
+ columnVisibilityFeature,
10
+ createPaginatedRowModel,
11
+ createSortedRowModel,
12
+ rowPaginationFeature,
13
+ rowSortingFeature,
14
+ sortFn_alphanumeric,
15
+ sortFn_alphanumericCaseSensitive,
16
+ sortFn_basic,
17
+ sortFn_datetime,
18
+ sortFn_text,
19
+ sortFn_textCaseSensitive,
20
+ tableFeatures
21
+ } from '@tanstack/react-table'
22
+
23
+ /** Features registered by every DataTable instance. */
24
+ export const dataTableFeatures = tableFeatures({
25
+ rowSortingFeature,
26
+ rowPaginationFeature,
27
+ columnVisibilityFeature,
28
+ sortedRowModel: createSortedRowModel(),
29
+ paginatedRowModel: createPaginatedRowModel(),
30
+ sortFns: {
31
+ alphanumeric: sortFn_alphanumeric,
32
+ alphanumericCaseSensitive: sortFn_alphanumericCaseSensitive,
33
+ text: sortFn_text,
34
+ textCaseSensitive: sortFn_textCaseSensitive,
35
+ datetime: sortFn_datetime,
36
+ basic: sortFn_basic
37
+ }
38
+ })
39
+
40
+ type DataTableFeatures = typeof dataTableFeatures
41
+
42
+ /**
43
+ * Column definition typed to the DataTable feature set.
44
+ * Prefer this over raw `ColumnDef` from `@tanstack/react-table`.
45
+ */
46
+ export type DataTableColumnDef<
47
+ TData extends RowData,
48
+ TValue = unknown
49
+ > = ColumnDef<DataTableFeatures, TData, TValue>