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,51 @@
1
+ import { Button, Heading, Text } from 'react-email'
2
+ import EmailLayout from '~/emails/components/email-layout'
3
+ import { siteConfig } from '~/lib/siteConfig'
4
+
5
+ export interface MagicLinkEmailProps {
6
+ magicLink: string
7
+ companyName: string
8
+ }
9
+
10
+ /** Passwordless sign-in link. The link works once and expires in 5 minutes. */
11
+ export default function MagicLinkEmail({
12
+ magicLink,
13
+ companyName
14
+ }: MagicLinkEmailProps) {
15
+ return (
16
+ <EmailLayout
17
+ previewText={`Your link to sign in to ${companyName}`}
18
+ companyName={companyName}
19
+ >
20
+ <Heading className="mb-4 text-2xl font-bold text-gray-900">
21
+ Sign In to {companyName}
22
+ </Heading>
23
+
24
+ <Text className="mb-4 text-base text-gray-700">
25
+ Select the button below to sign in. The link expires in 5 minutes.
26
+ </Text>
27
+
28
+ <Button
29
+ href={magicLink}
30
+ className="bg-brand mb-4 rounded-lg px-6 py-3 text-center text-base font-semibold text-white no-underline"
31
+ >
32
+ Sign In
33
+ </Button>
34
+
35
+ <Text className="mb-4 text-sm text-gray-600">
36
+ Or copy this link into your browser:
37
+ </Text>
38
+
39
+ <Text className="text-brand mb-4 text-sm break-all">{magicLink}</Text>
40
+
41
+ <Text className="text-sm text-gray-500">
42
+ If you didn&apos;t ask to sign in, you can ignore this email.
43
+ </Text>
44
+ </EmailLayout>
45
+ )
46
+ }
47
+
48
+ MagicLinkEmail.PreviewProps = {
49
+ magicLink: 'https://example.com/magic-link?token=your_magic_link_token_here',
50
+ companyName: siteConfig.name
51
+ } satisfies MagicLinkEmailProps
@@ -0,0 +1,34 @@
1
+ import { Button, Heading, Text } from 'react-email'
2
+ import EmailLayout from '~/emails/components/email-layout'
3
+ import { siteConfig } from '~/lib/siteConfig'
4
+
5
+ interface TestEmailProps {
6
+ name: string
7
+ }
8
+
9
+ /** Sent by `bun run email:test` to check SMTP settings. */
10
+ export default function TestEmail({ name }: TestEmailProps) {
11
+ return (
12
+ <EmailLayout
13
+ previewText="Test email from your app"
14
+ companyName={siteConfig.name}
15
+ >
16
+ <Heading className="mb-4 text-2xl font-bold text-gray-900">
17
+ Test Email for {name}
18
+ </Heading>
19
+ <Text className="mb-6 text-base text-gray-700">
20
+ Your SMTP settings work: this email reached your inbox.
21
+ </Text>
22
+ <Button
23
+ href="https://example.com"
24
+ className="bg-brand rounded-md px-5 py-3 text-center text-sm font-medium text-white"
25
+ >
26
+ Test Button
27
+ </Button>
28
+ </EmailLayout>
29
+ )
30
+ }
31
+
32
+ TestEmail.PreviewProps = {
33
+ name: 'Alex Morgan'
34
+ } satisfies TestEmailProps
@@ -0,0 +1,37 @@
1
+ import { Heading, Text } from 'react-email'
2
+ import EmailLayout from '~/emails/components/email-layout'
3
+ import { siteConfig } from '~/lib/siteConfig'
4
+
5
+ export interface WelcomeEmailProps {
6
+ userName: string
7
+ companyName: string
8
+ }
9
+
10
+ /** Sent once, right after an account is created. */
11
+ export default function WelcomeEmail({
12
+ userName,
13
+ companyName
14
+ }: WelcomeEmailProps) {
15
+ return (
16
+ <EmailLayout
17
+ previewText={`Welcome to ${companyName}. Your account is ready.`}
18
+ companyName={companyName}
19
+ >
20
+ <Heading className="mb-4 text-2xl font-bold text-gray-900">
21
+ Welcome to {companyName}
22
+ </Heading>
23
+
24
+ <Text className="mb-4 text-base text-gray-700">Hi {userName},</Text>
25
+
26
+ <Text className="mb-4 text-base text-gray-700">
27
+ Your account is ready. Sign in any time with the email address this
28
+ message was sent to.
29
+ </Text>
30
+ </EmailLayout>
31
+ )
32
+ }
33
+
34
+ WelcomeEmail.PreviewProps = {
35
+ userName: 'Alex Morgan',
36
+ companyName: siteConfig.name
37
+ } satisfies WelcomeEmailProps
@@ -0,0 +1,66 @@
1
+ import { createEnv } from '@t3-oss/env-nextjs'
2
+ import { z } from 'zod'
3
+
4
+ export const env = createEnv({
5
+ server: {
6
+ APP_ENV: z.enum(['development', 'staging', 'production']),
7
+ ANALYZE: z.string().optional(),
8
+ DATABASE_URL: z.url(),
9
+ BETTER_AUTH_URL: z.url(),
10
+ BETTER_AUTH_SECRET: z.string(),
11
+ BETTER_AUTH_GITHUB_ID: z.string(),
12
+ BETTER_AUTH_GITHUB_SECRET: z.string(),
13
+ BETTER_AUTH_GOOGLE_ID: z.string(),
14
+ BETTER_AUTH_GOOGLE_SECRET: z.string(),
15
+ EMAIL_SERVER_USER: z.string(),
16
+ EMAIL_SERVER_PASSWORD: z.string(),
17
+ EMAIL_SERVER_HOST: z.string(),
18
+ EMAIL_SERVER_PORT: z.coerce.number().int().min(1).max(65535),
19
+ EMAIL_FROM: z.email(),
20
+ STRIPE_SECRET_KEY: z.string(),
21
+ STRIPE_WEBHOOK_SECRET: z.string(),
22
+ STRIPE_PRICE_PRO_MONTHLY: z.string(),
23
+ STRIPE_PRICE_PRO_ANNUAL: z.string(),
24
+ STRIPE_PRICE_ULTIMATE_MONTHLY: z.string(),
25
+ STRIPE_PRICE_ULTIMATE_ANNUAL: z.string(),
26
+ STRIPE_PORTAL_CONFIGURATION_ID: z.string(),
27
+ R2_ACCOUNT_ID: z.string().optional(),
28
+ R2_ACCESS_KEY_ID: z.string().optional(),
29
+ R2_SECRET_ACCESS_KEY: z.string().optional(),
30
+ R2_BUCKET_NAME: z.string().optional(),
31
+ R2_PUBLIC_URL: z.url().optional()
32
+ },
33
+
34
+ client: {},
35
+
36
+ runtimeEnv: {
37
+ APP_ENV: process.env.APP_ENV,
38
+ ANALYZE: process.env.ANALYZE,
39
+ DATABASE_URL: process.env.DATABASE_URL,
40
+ BETTER_AUTH_URL: process.env.BETTER_AUTH_URL,
41
+ BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
42
+ BETTER_AUTH_GITHUB_ID: process.env.BETTER_AUTH_GITHUB_ID,
43
+ BETTER_AUTH_GITHUB_SECRET: process.env.BETTER_AUTH_GITHUB_SECRET,
44
+ BETTER_AUTH_GOOGLE_ID: process.env.BETTER_AUTH_GOOGLE_ID,
45
+ BETTER_AUTH_GOOGLE_SECRET: process.env.BETTER_AUTH_GOOGLE_SECRET,
46
+ EMAIL_SERVER_USER: process.env.EMAIL_SERVER_USER,
47
+ EMAIL_SERVER_PASSWORD: process.env.EMAIL_SERVER_PASSWORD,
48
+ EMAIL_SERVER_HOST: process.env.EMAIL_SERVER_HOST,
49
+ EMAIL_SERVER_PORT: process.env.EMAIL_SERVER_PORT,
50
+ EMAIL_FROM: process.env.EMAIL_FROM,
51
+ STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
52
+ STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
53
+ STRIPE_PRICE_PRO_MONTHLY: process.env.STRIPE_PRICE_PRO_MONTHLY,
54
+ STRIPE_PRICE_PRO_ANNUAL: process.env.STRIPE_PRICE_PRO_ANNUAL,
55
+ STRIPE_PRICE_ULTIMATE_MONTHLY: process.env.STRIPE_PRICE_ULTIMATE_MONTHLY,
56
+ STRIPE_PRICE_ULTIMATE_ANNUAL: process.env.STRIPE_PRICE_ULTIMATE_ANNUAL,
57
+ STRIPE_PORTAL_CONFIGURATION_ID: process.env.STRIPE_PORTAL_CONFIGURATION_ID,
58
+ R2_ACCOUNT_ID: process.env.R2_ACCOUNT_ID,
59
+ R2_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID,
60
+ R2_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY,
61
+ R2_BUCKET_NAME: process.env.R2_BUCKET_NAME,
62
+ R2_PUBLIC_URL: process.env.R2_PUBLIC_URL
63
+ },
64
+
65
+ emptyStringAsUndefined: true
66
+ })
@@ -0,0 +1,21 @@
1
+ import * as React from 'react'
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ function subscribe(onStoreChange: () => void) {
6
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
7
+ mql.addEventListener('change', onStoreChange)
8
+ return () => mql.removeEventListener('change', onStoreChange)
9
+ }
10
+
11
+ function getSnapshot() {
12
+ return window.innerWidth < MOBILE_BREAKPOINT
13
+ }
14
+
15
+ function getServerSnapshot() {
16
+ return false
17
+ }
18
+
19
+ export function useIsMobile() {
20
+ return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
21
+ }
@@ -0,0 +1,44 @@
1
+ import { SpanStatusCode } from '@opentelemetry/api'
2
+ import {
3
+ ConsoleSpanExporter,
4
+ ReadableSpan,
5
+ SimpleSpanProcessor
6
+ } from '@opentelemetry/sdk-trace-base'
7
+ import { registerOTel } from '@vercel/otel'
8
+ import { siteConfig } from '~/lib/siteConfig'
9
+
10
+ /**
11
+ * Custom SpanProcessor that samples spans to reduce telemetry volume.
12
+ * Exports every error span, exports every span slower than 5s.
13
+ */
14
+ class SmartSamplingSpanProcessor extends SimpleSpanProcessor {
15
+ /**
16
+ * Called when a span ends. Evaluates whether the span should be exported based on its properties.
17
+ *
18
+ * @param span - The readable span that has just ended
19
+ */
20
+ onEnd(span: ReadableSpan): void {
21
+ const isError = span.status.code === SpanStatusCode.ERROR
22
+ // span.duration is [seconds, nanoseconds]
23
+ // Spans slower than 5s
24
+ const isSlow = span.duration[0] * 1000 + span.duration[1] / 1e6 > 5000
25
+ // Random sampling: Adjust probability as needed (e.g. 0.1 for 10%)
26
+ const isLucky = Math.random() < 0 // Currently disabled for non-essential traces
27
+
28
+ if (isError || isSlow || isLucky) {
29
+ super.onEnd(span)
30
+ }
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Registers OpenTelemetry instrumentation for the application.
36
+ * This function should be exported as `register` from `src/instrumentation.ts`
37
+ * to be automatically picked up by Next.js.
38
+ */
39
+ export function register() {
40
+ registerOTel({
41
+ serviceName: siteConfig.name,
42
+ spanProcessors: [new SmartSamplingSpanProcessor(new ConsoleSpanExporter())]
43
+ })
44
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Reusable Zod fragments for paginated, sortable list actions.
3
+ */
4
+ import { z } from 'zod'
5
+ import { SortOrderEnum } from '~/components/data-table/enum'
6
+
7
+ /**
8
+ * Returns a reusable Zod schema fragment for cursor-less offset pagination.
9
+ *
10
+ * - `page`: 1-based page number (defaults to 1)
11
+ * - `limit`: Number of items per page; clamped between 10 and 100 (defaults to `defaultPageSize`)
12
+ *
13
+ * Spread the returned object into a `z.object({})` call to add pagination
14
+ * to any query schema.
15
+ *
16
+ * @param defaultPageSize - Default number of items per page (defaults to 10)
17
+ *
18
+ * @example
19
+ * z.object({
20
+ * ...getPaginationSchema(20),
21
+ * ...getSortSchema(['title', 'createdAt'])
22
+ * })
23
+ */
24
+ export const getPaginationSchema = (defaultPageSize = 10) => ({
25
+ page: z.coerce.number().int().positive().optional().default(1),
26
+ limit: z.coerce
27
+ .number()
28
+ .int()
29
+ .positive()
30
+ .min(10)
31
+ .max(100)
32
+ .optional()
33
+ .default(defaultPageSize)
34
+ })
35
+
36
+ /**
37
+ * Generates a reusable Zod schema fragment for sort parameters.
38
+ *
39
+ * @param keys - A non-empty tuple of the allowed `sortBy` field names for
40
+ * the resource (e.g. `['title', 'createdAt', 'updatedAt']`).
41
+ * @returns An object with `sortOrder` and `sortBy` Zod validators that can
42
+ * be spread into a `z.object({})` query schema.
43
+ *
44
+ * @example
45
+ * z.object({
46
+ * ...paginationSchema,
47
+ * ...getSortSchema(['title', 'createdAt', 'updatedAt'])
48
+ * })
49
+ */
50
+ export const getSortSchema = <T extends string>(
51
+ keys: readonly [T, ...T[]]
52
+ ) => ({
53
+ sortOrder: z.enum(SortOrderEnum).optional(),
54
+ sortBy: z.enum(keys).optional()
55
+ })
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @fileoverview Admin panel navigation, shared by the sidebar and the
3
+ * command search.
4
+ *
5
+ * @module lib/admin-navigation
6
+ */
7
+
8
+ import { Home, MessageCircle, MessageSquare, Ticket, Users } from 'lucide-react'
9
+ import { SidebarNavGroup } from '~/components/navigation-sidebar'
10
+
11
+ const adminNavigation: SidebarNavGroup[] = [
12
+ {
13
+ label: 'Overview',
14
+ items: [
15
+ {
16
+ title: 'Dashboard',
17
+ url: '/admin',
18
+ exact: true,
19
+ icon: Home
20
+ }
21
+ ]
22
+ },
23
+ {
24
+ label: 'Customers',
25
+ items: [
26
+ {
27
+ title: 'Support Tickets',
28
+ url: '/admin/support-tickets',
29
+ icon: Ticket
30
+ },
31
+ {
32
+ title: 'Feedback',
33
+ url: '/admin/feedbacks',
34
+ icon: MessageCircle
35
+ },
36
+ {
37
+ title: 'Contact Leads',
38
+ url: '/admin/contact-leads',
39
+ icon: MessageSquare
40
+ }
41
+ ]
42
+ },
43
+ {
44
+ label: 'Settings',
45
+ superadminOnly: true,
46
+ items: [
47
+ {
48
+ title: 'Users & Roles',
49
+ url: '/admin/settings/users',
50
+ icon: Users
51
+ }
52
+ ]
53
+ }
54
+ ]
55
+
56
+ /**
57
+ * The admin navigation visible to the current user. Superadmin-only groups
58
+ * are hidden from `admin` users.
59
+ */
60
+ export const getAdminNavigation = (isSuperAdmin: boolean): SidebarNavGroup[] =>
61
+ isSuperAdmin
62
+ ? adminNavigation
63
+ : adminNavigation.filter(group => !group.superadminOnly)
@@ -0,0 +1,63 @@
1
+ import { CreditCard, Home, Settings, Ticket, Users } from 'lucide-react'
2
+ import { SidebarNavGroup } from '~/components/navigation-sidebar'
3
+ import { isManagerRole } from './rbac'
4
+
5
+ /**
6
+ * Sidebar and command-search entries for one organization. Managers (owner
7
+ * and admin) also get support and settings pages.
8
+ */
9
+ export function getAppNavigation(
10
+ organizationId: string,
11
+ role: string
12
+ ): SidebarNavGroup[] {
13
+ const groups: SidebarNavGroup[] = [
14
+ {
15
+ label: 'Workspace',
16
+ items: [
17
+ {
18
+ title: 'Dashboard',
19
+ url: `/app/${organizationId}/dashboard`,
20
+ icon: Home
21
+ }
22
+ ]
23
+ }
24
+ ]
25
+
26
+ if (isManagerRole(role)) {
27
+ groups.push(
28
+ {
29
+ label: 'Help',
30
+ items: [
31
+ {
32
+ title: 'Support',
33
+ url: `/app/${organizationId}/support-tickets`,
34
+ icon: Ticket
35
+ }
36
+ ]
37
+ },
38
+ {
39
+ label: 'Settings',
40
+ items: [
41
+ {
42
+ title: 'General',
43
+ url: `/app/${organizationId}/settings`,
44
+ icon: Settings,
45
+ exact: true
46
+ },
47
+ {
48
+ title: 'Members',
49
+ url: `/app/${organizationId}/settings/members`,
50
+ icon: Users
51
+ },
52
+ {
53
+ title: 'Billing',
54
+ url: `/app/${organizationId}/settings/billing`,
55
+ icon: CreditCard
56
+ }
57
+ ]
58
+ }
59
+ )
60
+ }
61
+
62
+ return groups
63
+ }
@@ -0,0 +1,34 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Browser auth client. The admin plugin gets the same access control as the
5
+ * server so `admin.hasPermission` checks agree on both sides.
6
+ */
7
+ import {
8
+ adminClient,
9
+ inferAdditionalFields,
10
+ lastLoginMethodClient,
11
+ magicLinkClient,
12
+ organizationClient
13
+ } from 'better-auth/client/plugins'
14
+ import { createAuthClient } from 'better-auth/react'
15
+ import type { auth } from './auth'
16
+ import { ac, roles } from './auth-permissions'
17
+
18
+ export const {
19
+ signIn,
20
+ signOut,
21
+ useSession,
22
+ updateUser,
23
+ getLastUsedLoginMethod,
24
+ admin,
25
+ organization
26
+ } = createAuthClient({
27
+ plugins: [
28
+ inferAdditionalFields<typeof auth>(),
29
+ magicLinkClient(),
30
+ adminClient({ ac, roles }),
31
+ organizationClient(),
32
+ lastLoginMethodClient()
33
+ ]
34
+ })
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @fileoverview Access-control definitions for the Better Auth admin plugin.
3
+ *
4
+ * The platform has three roles:
5
+ *
6
+ * - `superadmin`: full control of the admin panel. Can list, view, create,
7
+ * update, change the role of, ban and impersonate any user (including other
8
+ * admins and superadmins).
9
+ * - `admin`: can open the admin panel and work with customer-facing data
10
+ * (support tickets, feedback, and contact leads), but is intentionally
11
+ * granted no user-management or impersonation permissions.
12
+ * - `user`: a regular customer with no admin-panel access
13
+ *
14
+ * These statements/roles are shared by both the server (`auth.ts`) and the
15
+ * client (`auth-client.ts`) so permission checks stay consistent everywhere.
16
+ *
17
+ * @module lib/auth-permissions
18
+ */
19
+
20
+ import { createAccessControl } from 'better-auth/plugins/access'
21
+ import { defaultStatements } from 'better-auth/plugins/admin/access'
22
+
23
+ /**
24
+ * The full set of permissions available across the admin panel.
25
+ *
26
+ * `defaultStatements` contributes the admin plugin's built-in `user` and
27
+ * `session` resources.
28
+ */
29
+ export const statement = { ...defaultStatements } as const
30
+
31
+ export const ac = createAccessControl(statement)
32
+
33
+ /** Regular customer with no admin-panel privileges. */
34
+ export const user = ac.newRole({})
35
+
36
+ /**
37
+ * Operational admin who can open the admin panel but has no
38
+ * `user` or `session` permissions. User management and impersonation are
39
+ * therefore denied.
40
+ */
41
+ export const admin = ac.newRole({})
42
+
43
+ /**
44
+ * Full-control admin who receives every built-in user/session permission,
45
+ * including `impersonate-admins`.
46
+ *
47
+ * Each resource is granted explicitly rather than spreading
48
+ * `defaultStatements` (which is the statement *definition*, not a grant).
49
+ * This way a newly added resource is denied by default until it is
50
+ * deliberately listed here.
51
+ */
52
+ export const superadmin = ac.newRole({
53
+ user: [...defaultStatements.user],
54
+ session: [...defaultStatements.session]
55
+ })
56
+
57
+ /** Role map passed to the admin plugin on both server and client. */
58
+ export const roles = { user, admin, superadmin }
59
+
60
+ /** Roles that are allowed to reach the admin panel at all. */
61
+ export const adminRoles = ['admin', 'superadmin'] as const