create-ej-app 25.0.0 → 27.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 (385) hide show
  1. package/README.md +125 -226
  2. package/cli.ts +83 -28
  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 -0
  15. package/template/nextjs/next.config.ts +8 -1
  16. package/template/nextjs/package.json +47 -40
  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 +8 -0
  140. package/template/saas-starter/.env.example +25 -0
  141. package/template/saas-starter/.vscode/extensions.json +11 -0
  142. package/template/saas-starter/.vscode/settings.json +3 -0
  143. package/template/saas-starter/AGENTS.md +302 -0
  144. package/template/saas-starter/CLAUDE.md +1 -0
  145. package/template/saas-starter/Dockerfile +15 -0
  146. package/template/saas-starter/README.md +125 -0
  147. package/template/saas-starter/bunfig.toml +3 -0
  148. package/template/saas-starter/components.json +25 -0
  149. package/template/saas-starter/drizzle.config.ts +10 -0
  150. package/template/saas-starter/eslint.config.mjs +24 -0
  151. package/template/saas-starter/gitignore +14 -0
  152. package/template/saas-starter/next.config.ts +32 -0
  153. package/template/saas-starter/package.json +97 -0
  154. package/template/saas-starter/postcss.config.mjs +7 -0
  155. package/template/saas-starter/prettier.config.mjs +11 -0
  156. package/template/saas-starter/public/logo-dark.svg +4 -0
  157. package/template/saas-starter/public/logo.svg +4 -0
  158. package/template/saas-starter/public/placeholder.svg +1 -0
  159. package/template/saas-starter/src/app/(admin)/actions/contact-leads.ts +51 -0
  160. package/template/saas-starter/src/app/(admin)/actions/feedbacks.ts +91 -0
  161. package/template/saas-starter/src/app/(admin)/actions/users.ts +183 -0
  162. package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
  163. package/template/saas-starter/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
  164. package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
  165. package/template/saas-starter/src/app/(admin)/admin/contact-leads/page.tsx +123 -0
  166. package/template/saas-starter/src/app/(admin)/admin/feedbacks/page.tsx +161 -0
  167. package/template/saas-starter/src/app/(admin)/admin/layout.tsx +53 -0
  168. package/template/saas-starter/src/app/(admin)/admin/page.tsx +60 -0
  169. package/template/saas-starter/src/app/(admin)/admin/settings/layout.tsx +24 -0
  170. package/template/saas-starter/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
  171. package/template/saas-starter/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
  172. package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +377 -0
  173. package/template/saas-starter/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
  174. package/template/saas-starter/src/app/(admin)/admin/settings/users/validation.ts +23 -0
  175. package/template/saas-starter/src/app/(admin)/admin/support-tickets/[ticketId]/page.tsx +17 -0
  176. package/template/saas-starter/src/app/(admin)/admin/support-tickets/page.tsx +5 -0
  177. package/template/saas-starter/src/app/(app)/actions/billing.ts +216 -0
  178. package/template/saas-starter/src/app/(app)/actions/feedbacks.ts +28 -0
  179. package/template/saas-starter/src/app/(app)/actions/feedbacks.validation.ts +12 -0
  180. package/template/saas-starter/src/app/(app)/actions/members.ts +189 -0
  181. package/template/saas-starter/src/app/(app)/actions/organizations.ts +230 -0
  182. package/template/saas-starter/src/app/(app)/actions/organizations.validation.ts +17 -0
  183. package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +17 -0
  184. package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +31 -0
  185. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +92 -0
  186. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +357 -0
  187. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +130 -0
  188. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +222 -0
  189. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +115 -0
  190. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/layout.tsx +24 -0
  191. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +168 -0
  192. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +604 -0
  193. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +33 -0
  194. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +55 -0
  195. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/[ticketId]/page.tsx +25 -0
  196. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/page.tsx +25 -0
  197. package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +31 -0
  198. package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +26 -0
  199. package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +30 -0
  200. package/template/saas-starter/src/app/(app)/app/_components/feedback-dialog.tsx +108 -0
  201. package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +37 -0
  202. package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +200 -0
  203. package/template/saas-starter/src/app/(app)/app/layout.tsx +64 -0
  204. package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +117 -0
  205. package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +40 -0
  206. package/template/saas-starter/src/app/(app)/app/page.tsx +76 -0
  207. package/template/saas-starter/src/app/(auth)/(account)/layout.tsx +64 -0
  208. package/template/saas-starter/src/app/(auth)/(account)/profile/page.tsx +16 -0
  209. package/template/saas-starter/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
  210. package/template/saas-starter/src/app/(auth)/(account)/support/[ticketId]/page.tsx +19 -0
  211. package/template/saas-starter/src/app/(auth)/(account)/support/page.tsx +9 -0
  212. package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +131 -0
  213. package/template/saas-starter/src/app/(auth)/login/page.tsx +105 -0
  214. package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +137 -0
  215. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +94 -0
  216. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +153 -0
  217. package/template/saas-starter/src/app/(invitation)/actions/invitations.ts +32 -0
  218. package/template/saas-starter/src/app/(marketing)/contact-us/actions.ts +23 -0
  219. package/template/saas-starter/src/app/(marketing)/contact-us/contact-form.tsx +118 -0
  220. package/template/saas-starter/src/app/(marketing)/contact-us/page.tsx +20 -0
  221. package/template/saas-starter/src/app/(marketing)/contact-us/validation.ts +22 -0
  222. package/template/saas-starter/src/app/(marketing)/layout.tsx +48 -0
  223. package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
  224. package/template/saas-starter/src/app/(marketing)/page.tsx +24 -0
  225. package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +63 -0
  226. package/template/saas-starter/src/app/(marketing)/terms/page.tsx +41 -0
  227. package/template/saas-starter/src/app/actions/support-tickets.ts +510 -0
  228. package/template/saas-starter/src/app/actions/support-tickets.validation.ts +112 -0
  229. package/template/saas-starter/src/app/actions/uploads.ts +36 -0
  230. package/template/saas-starter/src/app/actions/uploads.validation.ts +24 -0
  231. package/template/saas-starter/src/app/api/auth/[...all]/route.ts +4 -0
  232. package/template/saas-starter/src/app/api/webhooks/stripe/route.ts +94 -0
  233. package/template/saas-starter/src/app/error.tsx +37 -0
  234. package/template/saas-starter/src/app/favicon.ico +0 -0
  235. package/template/saas-starter/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
  236. package/template/saas-starter/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
  237. package/template/saas-starter/src/app/fonts/index.ts +60 -0
  238. package/template/saas-starter/src/app/global-error.tsx +38 -0
  239. package/template/saas-starter/src/app/globals.css +269 -0
  240. package/template/saas-starter/src/app/layout.tsx +77 -0
  241. package/template/saas-starter/src/app/not-found.tsx +49 -0
  242. package/template/saas-starter/src/app/robots.ts +36 -0
  243. package/template/saas-starter/src/app/sitemap.ts +21 -0
  244. package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
  245. package/template/saas-starter/src/components/dashboard-header.tsx +70 -0
  246. package/template/saas-starter/src/components/data-table/enum.ts +5 -0
  247. package/template/saas-starter/src/components/data-table/features.ts +49 -0
  248. package/template/saas-starter/src/components/data-table/index.tsx +961 -0
  249. package/template/saas-starter/src/components/data-table/types.ts +76 -0
  250. package/template/saas-starter/src/components/error.tsx +71 -0
  251. package/template/saas-starter/src/components/file-upload.tsx +254 -0
  252. package/template/saas-starter/src/components/loader.tsx +41 -0
  253. package/template/saas-starter/src/components/logo.tsx +49 -0
  254. package/template/saas-starter/src/components/navigation-search.tsx +140 -0
  255. package/template/saas-starter/src/components/navigation-sidebar.tsx +241 -0
  256. package/template/saas-starter/src/components/page-heading.tsx +38 -0
  257. package/template/saas-starter/src/components/react-query-provider.tsx +51 -0
  258. package/template/saas-starter/src/components/route-error.tsx +55 -0
  259. package/template/saas-starter/src/components/screen-size.tsx +76 -0
  260. package/template/saas-starter/src/components/site-footer.tsx +39 -0
  261. package/template/saas-starter/src/components/support/create-support-ticket-dialog.tsx +122 -0
  262. package/template/saas-starter/src/components/support/support-ticket-conversation.tsx +236 -0
  263. package/template/saas-starter/src/components/support/support-ticket-shared.tsx +58 -0
  264. package/template/saas-starter/src/components/support/support-tickets-table.tsx +281 -0
  265. package/template/saas-starter/src/components/theme-provider.tsx +115 -0
  266. package/template/saas-starter/src/components/theme-toggle.tsx +75 -0
  267. package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
  268. package/template/saas-starter/src/components/ui/avatar.tsx +112 -0
  269. package/template/saas-starter/src/components/ui/badge.tsx +54 -0
  270. package/template/saas-starter/src/components/ui/button.tsx +69 -0
  271. package/template/saas-starter/src/components/ui/card.tsx +103 -0
  272. package/template/saas-starter/src/components/ui/checkbox.tsx +32 -0
  273. package/template/saas-starter/src/components/ui/collapsible.tsx +33 -0
  274. package/template/saas-starter/src/components/ui/command.tsx +192 -0
  275. package/template/saas-starter/src/components/ui/dialog.tsx +164 -0
  276. package/template/saas-starter/src/components/ui/dropdown-menu.tsx +273 -0
  277. package/template/saas-starter/src/components/ui/field.tsx +238 -0
  278. package/template/saas-starter/src/components/ui/input-group.tsx +156 -0
  279. package/template/saas-starter/src/components/ui/input.tsx +19 -0
  280. package/template/saas-starter/src/components/ui/label.tsx +24 -0
  281. package/template/saas-starter/src/components/ui/popover.tsx +89 -0
  282. package/template/saas-starter/src/components/ui/select.tsx +195 -0
  283. package/template/saas-starter/src/components/ui/separator.tsx +28 -0
  284. package/template/saas-starter/src/components/ui/sheet.tsx +143 -0
  285. package/template/saas-starter/src/components/ui/sidebar.tsx +702 -0
  286. package/template/saas-starter/src/components/ui/skeleton.tsx +13 -0
  287. package/template/saas-starter/src/components/ui/sonner.tsx +45 -0
  288. package/template/saas-starter/src/components/ui/switch.tsx +33 -0
  289. package/template/saas-starter/src/components/ui/table.tsx +116 -0
  290. package/template/saas-starter/src/components/ui/tabs.tsx +90 -0
  291. package/template/saas-starter/src/components/ui/textarea.tsx +18 -0
  292. package/template/saas-starter/src/components/ui/tooltip.tsx +57 -0
  293. package/template/saas-starter/src/components/user-menu.tsx +213 -0
  294. package/template/saas-starter/src/db/common.ts +63 -0
  295. package/template/saas-starter/src/db/enums.ts +50 -0
  296. package/template/saas-starter/src/db/index.ts +60 -0
  297. package/template/saas-starter/src/db/relations.ts +112 -0
  298. package/template/saas-starter/src/db/schema.ts +342 -0
  299. package/template/saas-starter/src/emails/components/email-layout.tsx +79 -0
  300. package/template/saas-starter/src/emails/invitation.tsx +69 -0
  301. package/template/saas-starter/src/emails/magic-link.tsx +51 -0
  302. package/template/saas-starter/src/emails/test.tsx +34 -0
  303. package/template/saas-starter/src/emails/welcome.tsx +37 -0
  304. package/template/saas-starter/src/env.ts +66 -0
  305. package/template/saas-starter/src/hooks/use-mobile.ts +21 -0
  306. package/template/saas-starter/src/instrumentation.ts +44 -0
  307. package/template/saas-starter/src/lib/actions.ts +55 -0
  308. package/template/saas-starter/src/lib/admin-navigation.ts +63 -0
  309. package/template/saas-starter/src/lib/app-navigation.ts +63 -0
  310. package/template/saas-starter/src/lib/auth-client.ts +34 -0
  311. package/template/saas-starter/src/lib/auth-permissions.ts +61 -0
  312. package/template/saas-starter/src/lib/auth.ts +312 -0
  313. package/template/saas-starter/src/lib/billing.ts +152 -0
  314. package/template/saas-starter/src/lib/callback-url.ts +28 -0
  315. package/template/saas-starter/src/lib/cn.ts +4 -0
  316. package/template/saas-starter/src/lib/constants.ts +91 -0
  317. package/template/saas-starter/src/lib/delay.ts +12 -0
  318. package/template/saas-starter/src/lib/email-service.ts +118 -0
  319. package/template/saas-starter/src/lib/errors.ts +74 -0
  320. package/template/saas-starter/src/lib/format-currency.ts +25 -0
  321. package/template/saas-starter/src/lib/format-date.ts +75 -0
  322. package/template/saas-starter/src/lib/image-url.ts +54 -0
  323. package/template/saas-starter/src/lib/nodemailer.ts +67 -0
  324. package/template/saas-starter/src/lib/organization-access.ts +73 -0
  325. package/template/saas-starter/src/lib/otel.ts +117 -0
  326. package/template/saas-starter/src/lib/plan-limits.ts +37 -0
  327. package/template/saas-starter/src/lib/plans.ts +90 -0
  328. package/template/saas-starter/src/lib/rbac.ts +44 -0
  329. package/template/saas-starter/src/lib/safe-action-client.ts +126 -0
  330. package/template/saas-starter/src/lib/safe-action.ts +187 -0
  331. package/template/saas-starter/src/lib/search.ts +16 -0
  332. package/template/saas-starter/src/lib/seo.ts +7 -0
  333. package/template/saas-starter/src/lib/siteConfig.ts +20 -0
  334. package/template/saas-starter/src/lib/storage.ts +311 -0
  335. package/template/saas-starter/src/lib/stripe.ts +9 -0
  336. package/template/saas-starter/src/lib/toast-message.ts +101 -0
  337. package/template/saas-starter/src/lib/validations.ts +95 -0
  338. package/template/saas-starter/src/proxy.ts +48 -0
  339. package/template/saas-starter/src/scripts/cleanup-temp-uploads.ts +24 -0
  340. package/template/saas-starter/src/scripts/reset-db.ts +22 -0
  341. package/template/saas-starter/src/scripts/seed.ts +85 -0
  342. package/template/saas-starter/src/scripts/setup-db.ts +72 -0
  343. package/template/saas-starter/src/scripts/setup-stripe-plans.ts +206 -0
  344. package/template/saas-starter/src/scripts/test-mail.ts +43 -0
  345. package/template/saas-starter/tests/admin/users.test.ts +64 -0
  346. package/template/saas-starter/tests/db/fk-indexes.test.ts +62 -0
  347. package/template/saas-starter/tests/db/seed.test.ts +57 -0
  348. package/template/saas-starter/tests/helpers/action.ts +71 -0
  349. package/template/saas-starter/tests/helpers/fixtures.ts +151 -0
  350. package/template/saas-starter/tests/helpers/index.ts +2 -0
  351. package/template/saas-starter/tests/lib/callback-url.test.ts +48 -0
  352. package/template/saas-starter/tests/lib/client-server-boundary.test.ts +57 -0
  353. package/template/saas-starter/tests/lib/errors.test.ts +10 -0
  354. package/template/saas-starter/tests/lib/image-url.test.ts +31 -0
  355. package/template/saas-starter/tests/lib/search.test.ts +23 -0
  356. package/template/saas-starter/tests/marketing/contact-and-feedback.test.ts +86 -0
  357. package/template/saas-starter/tests/organizations/organizations.test.ts +175 -0
  358. package/template/saas-starter/tests/setup/pglite-db.ts +67 -0
  359. package/template/saas-starter/tests/setup/preload.ts +108 -0
  360. package/template/saas-starter/tests/support/support-tickets.test.ts +145 -0
  361. package/template/saas-starter/tsconfig.json +52 -0
  362. package/cli.js +0 -143
  363. package/template/nextjs/src/app/(admin)/layout.tsx +0 -20
  364. package/template/nextjs/src/app/(app)/layout.tsx +0 -20
  365. package/template/nextjs/terraform/artifact-registry.tf +0 -5
  366. package/template/nextjs/terraform/backend-service.tf +0 -13
  367. package/template/nextjs/terraform/cleanup.sh +0 -9
  368. package/template/nextjs/terraform/cloudbuild.tf +0 -70
  369. package/template/nextjs/terraform/cloudrun.tf +0 -68
  370. package/template/nextjs/terraform/database.tf +0 -60
  371. package/template/nextjs/terraform/forwarding-rule.tf +0 -43
  372. package/template/nextjs/terraform/gitignore +0 -4
  373. package/template/nextjs/terraform/ip-address.tf +0 -11
  374. package/template/nextjs/terraform/network-endpoint-group.tf +0 -11
  375. package/template/nextjs/terraform/network.tf +0 -21
  376. package/template/nextjs/terraform/pre.sh +0 -3
  377. package/template/nextjs/terraform/providers.tf +0 -18
  378. package/template/nextjs/terraform/secret.txt +0 -11
  379. package/template/nextjs/terraform/setup.sh +0 -25
  380. package/template/nextjs/terraform/ssl-certs.tf +0 -32
  381. package/template/nextjs/terraform/target-https-proxy copy.tf +0 -14
  382. package/template/nextjs/terraform/target-https-proxy.tf +0 -14
  383. package/template/nextjs/terraform/terraform.tfvars +0 -4
  384. package/template/nextjs/terraform/url-map.tf +0 -23
  385. package/template/nextjs/terraform/variables.tf +0 -21
@@ -0,0 +1,90 @@
1
+ import 'server-only'
2
+
3
+ import { env } from '~/env'
4
+
5
+ export type PlanId = 'free' | 'pro' | 'ultimate'
6
+
7
+ type PlanPrice = {
8
+ id: string
9
+ /** Minor units, as Stripe stores them: 2000 is $20.00. */
10
+ amount: number
11
+ currency: string
12
+ }
13
+
14
+ export type Plan = {
15
+ id: PlanId
16
+ name: string
17
+ maxMembers: number
18
+ stripePrice?: { monthly: PlanPrice; annual: PlanPrice }
19
+ canUseTrial?: boolean
20
+ }
21
+
22
+ /** Every plan, cheapest first. Stripe prices come from `bun run stripe:setup`. */
23
+ export const PLANS: Plan[] = [
24
+ {
25
+ id: 'free',
26
+ name: 'Free',
27
+ maxMembers: 1
28
+ },
29
+ {
30
+ id: 'pro',
31
+ name: 'Pro',
32
+ maxMembers: 25,
33
+ stripePrice: {
34
+ monthly: {
35
+ id: env.STRIPE_PRICE_PRO_MONTHLY,
36
+ amount: 2000,
37
+ currency: 'USD'
38
+ },
39
+ annual: {
40
+ id: env.STRIPE_PRICE_PRO_ANNUAL,
41
+ amount: 20000,
42
+ currency: 'USD'
43
+ }
44
+ },
45
+ canUseTrial: true
46
+ },
47
+ {
48
+ id: 'ultimate',
49
+ name: 'Ultimate',
50
+ maxMembers: 100,
51
+ stripePrice: {
52
+ monthly: {
53
+ id: env.STRIPE_PRICE_ULTIMATE_MONTHLY,
54
+ amount: 10000,
55
+ currency: 'USD'
56
+ },
57
+ annual: {
58
+ id: env.STRIPE_PRICE_ULTIMATE_ANNUAL,
59
+ amount: 100000,
60
+ currency: 'USD'
61
+ }
62
+ }
63
+ }
64
+ ]
65
+
66
+ /** Subscription statuses that keep the paid plan's features switched on. */
67
+ export const ACTIVE_SUBSCRIPTION_STATUSES = [
68
+ 'trialing',
69
+ 'active',
70
+ 'past_due'
71
+ ] as const
72
+
73
+ export function isActiveSubscriptionStatus(status: string): boolean {
74
+ return (ACTIVE_SUBSCRIPTION_STATUSES as readonly string[]).includes(status)
75
+ }
76
+
77
+ /**
78
+ * Statuses with no live Stripe subscription behind them. Any other status
79
+ * (including `unpaid`, `paused`, and `incomplete`) still bills or can resume,
80
+ * so changes go through the billing portal instead of a new checkout.
81
+ */
82
+ export const ENDED_SUBSCRIPTION_STATUSES = [
83
+ 'free',
84
+ 'canceled',
85
+ 'incomplete_expired'
86
+ ] as const
87
+
88
+ export function hasLiveSubscription(status: string): boolean {
89
+ return !(ENDED_SUBSCRIPTION_STATUSES as readonly string[]).includes(status)
90
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Organization role predicates and labels, shared by server and client.
3
+ *
4
+ * The roles are the Better Auth organization plugin defaults:
5
+ * - owner: full control, including ownership transfer and deletion
6
+ * - admin: manages settings, billing, and members, but not ownership
7
+ * - member: standard access to the organization's workspace
8
+ */
9
+
10
+ export type OrganizationRole = 'owner' | 'admin' | 'member'
11
+
12
+ /** Roles an owner or admin can give to someone. Ownership moves only by transfer. */
13
+ export const ASSIGNABLE_ROLES = ['admin', 'member'] as const
14
+
15
+ export type AssignableRole = (typeof ASSIGNABLE_ROLES)[number]
16
+
17
+ /** True for the manager tier: owner and admin. */
18
+ export function isManagerRole(role: string | null | undefined): boolean {
19
+ return role === 'owner' || role === 'admin'
20
+ }
21
+
22
+ /** Descriptions shown when you pick a role for a member or an invitation. */
23
+ export const ROLE_DESCRIPTIONS: Record<AssignableRole, string> = {
24
+ admin: 'Admin: manage settings, billing, and members',
25
+ member: 'Member: use the workspace'
26
+ }
27
+
28
+ /** Shown to an owner before they transfer ownership to another member. */
29
+ export const OWNERSHIP_DEMOTION_NOTICE =
30
+ 'After the transfer, your role becomes admin: you keep every permission except ownership transfer and organization deletion.'
31
+
32
+ /** Role with its article, for sentences such as "join Acme as an admin". */
33
+ export function roleArticle(role: string): string {
34
+ switch (role) {
35
+ case 'owner':
36
+ return 'an owner'
37
+ case 'admin':
38
+ return 'an admin'
39
+ case 'member':
40
+ return 'a member'
41
+ default:
42
+ return `a ${role}`
43
+ }
44
+ }
@@ -0,0 +1,126 @@
1
+ 'use client'
2
+
3
+ import {
4
+ useMutation,
5
+ useQuery,
6
+ type UseMutationOptions,
7
+ type UseQueryOptions
8
+ } from '@tanstack/react-query'
9
+ import type { SafeActionResult } from 'next-safe-action'
10
+
11
+ /** Type helper for any server action function */
12
+ export type AnyAction = (
13
+ ...args: any[]
14
+ ) => Promise<SafeActionResult<any, any, any, any, any>>
15
+
16
+ /** Extracts the input type from a server action */
17
+ type ActionInput<T extends AnyAction> = Parameters<T>[0]
18
+
19
+ /** Extracts the data type from a server action result */
20
+ type ActionData<T extends AnyAction> = Awaited<ReturnType<T>>['data']
21
+
22
+ /**
23
+ * Client-side error for server action failures.
24
+ * Carries the user-facing `serverError` string (including unmasked AppError
25
+ * messages) and optional Zod validation errors.
26
+ */
27
+ class SafeActionError extends Error {
28
+ constructor(
29
+ public message: string,
30
+ public validationErrors?: Record<string, string[] | undefined>
31
+ ) {
32
+ super(message)
33
+ this.name = 'SafeActionError'
34
+ }
35
+ }
36
+
37
+ /**
38
+ * React Query hook for server actions that fetch data
39
+ * Wraps server actions with proper error handling and caching support
40
+ *
41
+ * @param key - Unique query key for caching
42
+ * @param action - Server action function to execute
43
+ * @param input - Input parameters for the server action (optional)
44
+ * @param options - React Query options (staleTime, refetchInterval, etc.)
45
+ *
46
+ * @returns React Query result with data, loading, and error states
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * const { data, isLoading } = useSafeActionQuery('cart-items', getCartItems)
51
+ * ```
52
+ */
53
+ export function useSafeActionQuery<TAction extends AnyAction>(
54
+ key: string,
55
+ action: TAction,
56
+ input?: ActionInput<TAction>,
57
+ options?: Omit<UseQueryOptions<ActionData<TAction>>, 'queryKey' | 'queryFn'>
58
+ ) {
59
+ return useQuery({
60
+ queryKey: [key, input],
61
+ queryFn: async () => {
62
+ const result = await action(input)
63
+
64
+ if (result?.serverError) {
65
+ throw new SafeActionError(result.serverError)
66
+ }
67
+
68
+ if (result?.validationErrors) {
69
+ throw new SafeActionError(
70
+ 'Check the highlighted fields and try again.',
71
+ result.validationErrors
72
+ )
73
+ }
74
+
75
+ return result?.data
76
+ },
77
+ ...options
78
+ })
79
+ }
80
+
81
+ /**
82
+ * React Query mutation hook for server actions that modify data
83
+ * Wraps server actions with proper error handling and optimistic updates support
84
+ *
85
+ * @param action - Server action function to execute
86
+ * @param options - React Query mutation options (onSuccess, onError, etc.)
87
+ *
88
+ * @returns React Query mutation result with mutate function and states
89
+ *
90
+ * @example
91
+ * ```tsx
92
+ * const { mutate, isPending } = useSafeActionMutation(addToCart, {
93
+ * onSuccess: (data) => {
94
+ * toastSuccessMessage('Added to cart!')
95
+ * queryClient.invalidateQueries({ queryKey: ['cart-items'] })
96
+ * },
97
+ * onError: (error) => {
98
+ * toastActionError(error, 'Failed to add to cart')
99
+ * }
100
+ * })
101
+ * ```
102
+ */
103
+ export function useSafeActionMutation<TAction extends AnyAction>(
104
+ action: TAction,
105
+ options?: UseMutationOptions<ActionData<TAction>, Error, ActionInput<TAction>>
106
+ ) {
107
+ return useMutation({
108
+ mutationFn: async (input: ActionInput<TAction>) => {
109
+ const result = await action(input)
110
+
111
+ if (result?.serverError) {
112
+ throw new SafeActionError(result.serverError)
113
+ }
114
+
115
+ if (result?.validationErrors) {
116
+ throw new SafeActionError(
117
+ 'Check the highlighted fields and try again.',
118
+ result.validationErrors
119
+ )
120
+ }
121
+
122
+ return result?.data
123
+ },
124
+ ...options
125
+ })
126
+ }
@@ -0,0 +1,187 @@
1
+ import 'server-only'
2
+
3
+ import { createSafeActionClient } from 'next-safe-action'
4
+ import { getAuthSession } from './auth'
5
+ import { AppError, AppErrorCode, isAppError } from './errors'
6
+ import { flattenForWideEvent, recordError, recordWideEvent } from './otel'
7
+
8
+ /** Shown for every unexpected error; details stay in telemetry. */
9
+ export const UNEXPECTED_ERROR_MESSAGE =
10
+ 'The request did not finish. Try again in a few minutes.'
11
+
12
+ type ActionServerArgs = {
13
+ parsedInput?: unknown
14
+ clientInput?: unknown
15
+ [key: string]: unknown
16
+ }
17
+
18
+ /**
19
+ * Wrap a safe-action server function with wide-event telemetry.
20
+ *
21
+ * Records `action.name`, flattened `parsedInput` (or `clientInput`), and on
22
+ * success `outcome: 'success'` plus `duration_ms`. Failures keep error fields
23
+ * from `handleServerError`; duration is still recorded.
24
+ *
25
+ * @example
26
+ * export const myAction = authActionClient
27
+ * .inputSchema(schema)
28
+ * .action(
29
+ * withWideEvent('myAction', async ({ parsedInput, ctx }) => {
30
+ * // business logic only
31
+ * })
32
+ * )
33
+ */
34
+ export function withWideEvent<TArgs extends ActionServerArgs, TResult>(
35
+ actionName: string,
36
+ serverCode: (args: TArgs) => Promise<TResult>
37
+ ): (args: TArgs) => Promise<TResult> {
38
+ return async args => {
39
+ const startedAt = Date.now()
40
+ const input = args.parsedInput ?? args.clientInput
41
+
42
+ recordWideEvent({
43
+ 'action.name': actionName,
44
+ ...flattenForWideEvent(input, 'input')
45
+ })
46
+
47
+ try {
48
+ const result = await serverCode(args)
49
+ recordWideEvent({
50
+ outcome: 'success',
51
+ duration_ms: Date.now() - startedAt
52
+ })
53
+ return result
54
+ } catch (error) {
55
+ recordWideEvent({
56
+ outcome: 'error',
57
+ duration_ms: Date.now() - startedAt
58
+ })
59
+ throw error
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Configured safe action client with authentication middleware
66
+ * This client wraps server actions with:
67
+ * - Automatic input/output validation using Zod
68
+ * - Type-safe error handling ({@link AppError} messages reach the client)
69
+ * - Server-side authentication injection
70
+ *
71
+ * All server actions using this client automatically receive the current auth session
72
+ * in the context, so protected actions can access user information.
73
+ *
74
+ * @example
75
+ * export const myAction = actionClient
76
+ * .inputSchema(z.object({ name: z.string() }))
77
+ * .action(async ({ parsedInput: { name }, ctx }) => {
78
+ * const userId = ctx.user?.id // Auth injected automatically
79
+ * return { success: true }
80
+ * })
81
+ */
82
+ export const actionClient = createSafeActionClient({
83
+ handleServerError(e) {
84
+ if (isAppError(e)) {
85
+ recordWideEvent({
86
+ 'error.name': e.name,
87
+ 'error.code': e.code,
88
+ 'error.message': e.message,
89
+ 'error.expected': true,
90
+ outcome: 'error'
91
+ })
92
+ return e.message
93
+ }
94
+
95
+ recordError(e)
96
+ recordWideEvent({
97
+ 'error.name': e.name,
98
+ 'error.expected': false,
99
+ 'error.message': e.message,
100
+ outcome: 'error'
101
+ })
102
+ return UNEXPECTED_ERROR_MESSAGE
103
+ }
104
+ }).use(async ({ next, ctx }) => {
105
+ // Retrieve current authentication session
106
+ const authSession = await getAuthSession()
107
+
108
+ // Record user information in telemetry for all actions, if available
109
+ if (authSession) {
110
+ recordWideEvent({
111
+ 'user.id': authSession.user.id,
112
+ 'user.admin': authSession.isAdmin,
113
+ 'user.superAdmin': authSession.isSuperAdmin,
114
+ 'session.id': authSession.session.id
115
+ })
116
+ }
117
+
118
+ // Inject auth session into action context for all downstream actions
119
+ return next({
120
+ ctx: {
121
+ ...ctx,
122
+ ...authSession
123
+ }
124
+ })
125
+ })
126
+
127
+ export const authActionClient = actionClient.use(async ({ ctx, next }) => {
128
+ if (!ctx?.user || !ctx?.session) {
129
+ throw new AppError(AppErrorCode.UNAUTHORIZED, 'Sign in to continue.')
130
+ }
131
+
132
+ return next({
133
+ ctx: {
134
+ ...ctx,
135
+ user: ctx.user,
136
+ session: ctx.session
137
+ }
138
+ })
139
+ })
140
+
141
+ /**
142
+ * Action client for the admin panel.
143
+ *
144
+ * Allows both `admin` and `superadmin` roles. Use for actions that any
145
+ * admin-panel user may run (support tickets, feedback, and contact leads).
146
+ */
147
+ export const adminActionClient = authActionClient.use(async ({ ctx, next }) => {
148
+ if (!ctx.isAdmin) {
149
+ throw new AppError(
150
+ AppErrorCode.FORBIDDEN,
151
+ 'You do not have permission to do this.'
152
+ )
153
+ }
154
+
155
+ return next({
156
+ ctx: {
157
+ ...ctx,
158
+ user: ctx.user,
159
+ session: ctx.session
160
+ }
161
+ })
162
+ })
163
+
164
+ /**
165
+ * Action client restricted to the `superadmin` role.
166
+ *
167
+ * Use for privileged operations, such as user management, role changes, and
168
+ * impersonation, that `admin` users cannot perform.
169
+ */
170
+ export const superAdminActionClient = adminActionClient.use(
171
+ async ({ ctx, next }) => {
172
+ if (!ctx.isSuperAdmin) {
173
+ throw new AppError(
174
+ AppErrorCode.FORBIDDEN,
175
+ 'You do not have permission to do this.'
176
+ )
177
+ }
178
+
179
+ return next({
180
+ ctx: {
181
+ ...ctx,
182
+ user: ctx.user,
183
+ session: ctx.session
184
+ }
185
+ })
186
+ }
187
+ )
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Escape characters that are special in SQL LIKE / ILIKE patterns.
3
+ *
4
+ * Postgres uses `\` as the default escape character. Without escaping, a
5
+ * user-typed `%` or `_` becomes a wildcard and can force full-table scans.
6
+ */
7
+ export function escapeLikePattern(value: string): string {
8
+ return value.replace(/[\\%_]/g, char => `\\${char}`)
9
+ }
10
+
11
+ /**
12
+ * Build a case-insensitive contains pattern: `%value%` with wildcards escaped.
13
+ */
14
+ export function likeContains(value: string): string {
15
+ return `%${escapeLikePattern(value)}%`
16
+ }
@@ -0,0 +1,7 @@
1
+ import type { Metadata } from 'next'
2
+
3
+ /** Meta robots for authenticated or non-public surfaces. */
4
+ export const privatePageRobots = {
5
+ index: false,
6
+ follow: false
7
+ } satisfies NonNullable<Metadata['robots']>
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @fileoverview Brand identity and site-wide copy.
3
+ *
4
+ * Layouts, emails, legal pages, and metadata read from here. Never hardcode
5
+ * the product name anywhere else.
6
+ *
7
+ * @module lib/siteConfig
8
+ */
9
+ export const siteConfig = {
10
+ name: 'Project Name',
11
+ description: 'Project Description',
12
+ logo: '/logo.svg',
13
+ logoDark: '/logo-dark.svg',
14
+ contact: {
15
+ email: 'hello@example.com'
16
+ },
17
+ legal: {
18
+ lastUpdated: 'September 25, 2026'
19
+ }
20
+ }