create-stackr 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +642 -0
  3. package/bin/cli.js +12 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +113 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/config/dependencies.d.ts +82 -0
  9. package/dist/config/dependencies.d.ts.map +1 -0
  10. package/dist/config/dependencies.js +82 -0
  11. package/dist/config/dependencies.js.map +1 -0
  12. package/dist/config/presets.d.ts +3 -0
  13. package/dist/config/presets.d.ts.map +1 -0
  14. package/dist/config/presets.js +174 -0
  15. package/dist/config/presets.js.map +1 -0
  16. package/dist/generators/index.d.ts +40 -0
  17. package/dist/generators/index.d.ts.map +1 -0
  18. package/dist/generators/index.js +130 -0
  19. package/dist/generators/index.js.map +1 -0
  20. package/dist/generators/onboarding.d.ts +8 -0
  21. package/dist/generators/onboarding.d.ts.map +1 -0
  22. package/dist/generators/onboarding.js +141 -0
  23. package/dist/generators/onboarding.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +65 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/prompts/features.d.ts +14 -0
  29. package/dist/prompts/features.d.ts.map +1 -0
  30. package/dist/prompts/features.js +96 -0
  31. package/dist/prompts/features.js.map +1 -0
  32. package/dist/prompts/index.d.ts +3 -0
  33. package/dist/prompts/index.d.ts.map +1 -0
  34. package/dist/prompts/index.js +93 -0
  35. package/dist/prompts/index.js.map +1 -0
  36. package/dist/prompts/onboarding.d.ts +6 -0
  37. package/dist/prompts/onboarding.d.ts.map +1 -0
  38. package/dist/prompts/onboarding.js +37 -0
  39. package/dist/prompts/onboarding.js.map +1 -0
  40. package/dist/prompts/orm.d.ts +3 -0
  41. package/dist/prompts/orm.d.ts.map +1 -0
  42. package/dist/prompts/orm.js +23 -0
  43. package/dist/prompts/orm.js.map +1 -0
  44. package/dist/prompts/packageManager.d.ts +2 -0
  45. package/dist/prompts/packageManager.d.ts.map +1 -0
  46. package/dist/prompts/packageManager.js +18 -0
  47. package/dist/prompts/packageManager.js.map +1 -0
  48. package/dist/prompts/platform.d.ts +3 -0
  49. package/dist/prompts/platform.d.ts.map +1 -0
  50. package/dist/prompts/platform.js +21 -0
  51. package/dist/prompts/platform.js.map +1 -0
  52. package/dist/prompts/preset.d.ts +4 -0
  53. package/dist/prompts/preset.d.ts.map +1 -0
  54. package/dist/prompts/preset.js +165 -0
  55. package/dist/prompts/preset.js.map +1 -0
  56. package/dist/prompts/project.d.ts +2 -0
  57. package/dist/prompts/project.d.ts.map +1 -0
  58. package/dist/prompts/project.js +27 -0
  59. package/dist/prompts/project.js.map +1 -0
  60. package/dist/prompts/sdks.d.ts +2 -0
  61. package/dist/prompts/sdks.d.ts.map +1 -0
  62. package/dist/prompts/sdks.js +46 -0
  63. package/dist/prompts/sdks.js.map +1 -0
  64. package/dist/types/index.d.ts +77 -0
  65. package/dist/types/index.d.ts.map +1 -0
  66. package/dist/types/index.js +25 -0
  67. package/dist/types/index.js.map +1 -0
  68. package/dist/utils/cleanup.d.ts +5 -0
  69. package/dist/utils/cleanup.d.ts.map +1 -0
  70. package/dist/utils/cleanup.js +38 -0
  71. package/dist/utils/cleanup.js.map +1 -0
  72. package/dist/utils/copy.d.ts +10 -0
  73. package/dist/utils/copy.d.ts.map +1 -0
  74. package/dist/utils/copy.js +53 -0
  75. package/dist/utils/copy.js.map +1 -0
  76. package/dist/utils/errors.d.ts +33 -0
  77. package/dist/utils/errors.d.ts.map +1 -0
  78. package/dist/utils/errors.js +136 -0
  79. package/dist/utils/errors.js.map +1 -0
  80. package/dist/utils/git.d.ts +5 -0
  81. package/dist/utils/git.d.ts.map +1 -0
  82. package/dist/utils/git.js +33 -0
  83. package/dist/utils/git.js.map +1 -0
  84. package/dist/utils/logger.d.ts +9 -0
  85. package/dist/utils/logger.d.ts.map +1 -0
  86. package/dist/utils/logger.js +22 -0
  87. package/dist/utils/logger.js.map +1 -0
  88. package/dist/utils/package.d.ts +16 -0
  89. package/dist/utils/package.d.ts.map +1 -0
  90. package/dist/utils/package.js +86 -0
  91. package/dist/utils/package.js.map +1 -0
  92. package/dist/utils/system-validation.d.ts +9 -0
  93. package/dist/utils/system-validation.d.ts.map +1 -0
  94. package/dist/utils/system-validation.js +31 -0
  95. package/dist/utils/system-validation.js.map +1 -0
  96. package/dist/utils/template.d.ts +20 -0
  97. package/dist/utils/template.d.ts.map +1 -0
  98. package/dist/utils/template.js +234 -0
  99. package/dist/utils/template.js.map +1 -0
  100. package/dist/utils/validation.d.ts +8 -0
  101. package/dist/utils/validation.d.ts.map +1 -0
  102. package/dist/utils/validation.js +94 -0
  103. package/dist/utils/validation.js.map +1 -0
  104. package/package.json +96 -0
  105. package/templates/base/backend/.dockerignore.ejs +62 -0
  106. package/templates/base/backend/.env.example.ejs +116 -0
  107. package/templates/base/backend/Dockerfile.ejs +142 -0
  108. package/templates/base/backend/controllers/event-queue/index.ts +20 -0
  109. package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
  110. package/templates/base/backend/controllers/rest-api/index.ts +48 -0
  111. package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
  112. package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
  113. package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
  114. package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
  115. package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
  116. package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
  117. package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
  118. package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
  119. package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
  120. package/templates/base/backend/domain/device-session/schema.ts +72 -0
  121. package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
  122. package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
  123. package/templates/base/backend/domain/session/schema.ts +29 -0
  124. package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
  125. package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
  126. package/templates/base/backend/domain/user/schema.ts +14 -0
  127. package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
  128. package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
  129. package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
  130. package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
  131. package/templates/base/backend/lib/constants.ts.ejs +29 -0
  132. package/templates/base/backend/package.json.ejs +50 -0
  133. package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
  134. package/templates/base/backend/prisma.config.prisma.ts +12 -0
  135. package/templates/base/backend/tsconfig.json +39 -0
  136. package/templates/base/backend/utils/db.drizzle.ts +41 -0
  137. package/templates/base/backend/utils/db.prisma.ts +51 -0
  138. package/templates/base/backend/utils/email.ts.ejs +35 -0
  139. package/templates/base/backend/utils/errors.ts +348 -0
  140. package/templates/base/backend/utils/redis.ts.ejs +279 -0
  141. package/templates/base/mobile/.env.example.ejs +35 -0
  142. package/templates/base/mobile/.gitignore.ejs +167 -0
  143. package/templates/base/mobile/app/+not-found.tsx +85 -0
  144. package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
  145. package/templates/base/mobile/app.json.ejs +88 -0
  146. package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
  147. package/templates/base/mobile/assets/images/favicon.png +0 -0
  148. package/templates/base/mobile/assets/images/icon.png +0 -0
  149. package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
  150. package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
  151. package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
  152. package/templates/base/mobile/assets/images/paywall_image.png +0 -0
  153. package/templates/base/mobile/assets/images/splash.png +0 -0
  154. package/templates/base/mobile/eas.json.ejs +49 -0
  155. package/templates/base/mobile/metro.config.js +9 -0
  156. package/templates/base/mobile/package.json.ejs +53 -0
  157. package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
  158. package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
  159. package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
  160. package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
  161. package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
  162. package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
  163. package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
  164. package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
  165. package/templates/base/mobile/src/components/ui/index.ts +6 -0
  166. package/templates/base/mobile/src/constants/Theme.ts +163 -0
  167. package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
  168. package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
  169. package/templates/base/mobile/src/services/api.ts.ejs +71 -0
  170. package/templates/base/mobile/src/services/errorService.ts +179 -0
  171. package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
  172. package/templates/base/mobile/src/store/index.ts.ejs +18 -0
  173. package/templates/base/mobile/src/store/ui.store.ts +100 -0
  174. package/templates/base/mobile/src/utils/formatters.ts +105 -0
  175. package/templates/base/mobile/src/utils/logger.ts +73 -0
  176. package/templates/base/mobile/src/utils/responsive.ts +234 -0
  177. package/templates/base/mobile/tsconfig.json +32 -0
  178. package/templates/base/web/.env.example.ejs +26 -0
  179. package/templates/base/web/components.json +22 -0
  180. package/templates/base/web/eslint.config.mjs +18 -0
  181. package/templates/base/web/next.config.ts +7 -0
  182. package/templates/base/web/package.json.ejs +35 -0
  183. package/templates/base/web/postcss.config.mjs +7 -0
  184. package/templates/base/web/public/.gitkeep +0 -0
  185. package/templates/base/web/public/file.svg +1 -0
  186. package/templates/base/web/public/globe.svg +1 -0
  187. package/templates/base/web/public/next.svg +1 -0
  188. package/templates/base/web/public/vercel.svg +1 -0
  189. package/templates/base/web/public/window.svg +1 -0
  190. package/templates/base/web/src/app/favicon.ico +0 -0
  191. package/templates/base/web/src/app/globals.css +152 -0
  192. package/templates/base/web/src/app/layout.tsx.ejs +54 -0
  193. package/templates/base/web/src/app/page.tsx.ejs +92 -0
  194. package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
  195. package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
  196. package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
  197. package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
  198. package/templates/base/web/src/components/theme-toggle.tsx +34 -0
  199. package/templates/base/web/src/components/ui/button.tsx +62 -0
  200. package/templates/base/web/src/components/ui/card.tsx +92 -0
  201. package/templates/base/web/src/components/ui/input.tsx +21 -0
  202. package/templates/base/web/src/components/ui/label.tsx +24 -0
  203. package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
  204. package/templates/base/web/src/components/ui/spinner.tsx +20 -0
  205. package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
  206. package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
  207. package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
  208. package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
  209. package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
  210. package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
  211. package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
  212. package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
  213. package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
  214. package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
  215. package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
  216. package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
  217. package/templates/base/web/src/lib/utils.ts +6 -0
  218. package/templates/base/web/src/proxy.ts.ejs +66 -0
  219. package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
  220. package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
  221. package/templates/base/web/tsconfig.json +34 -0
  222. package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
  223. package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
  224. package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
  225. package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
  226. package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
  227. package/templates/features/mobile/auth/components/auth/index.ts +2 -0
  228. package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
  229. package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
  230. package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
  231. package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
  232. package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
  233. package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
  234. package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
  235. package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
  236. package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
  237. package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
  238. package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
  239. package/templates/features/web/.gitkeep +0 -0
  240. package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
  241. package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
  242. package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
  243. package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
  244. package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
  245. package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
  246. package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
  247. package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
  248. package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
  249. package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
  250. package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
  251. package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
  252. package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
  253. package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
  254. package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
  255. package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
  256. package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
  257. package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
  258. package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
  259. package/templates/integrations/mobile/att/services/attService.ts +84 -0
  260. package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
  261. package/templates/integrations/mobile/att/store/att.store.ts +162 -0
  262. package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
  263. package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
  264. package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
  265. package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
  266. package/templates/integrations/web/.gitkeep +0 -0
  267. package/templates/shared/.env.example.ejs +21 -0
  268. package/templates/shared/.gitignore.ejs +145 -0
  269. package/templates/shared/README.md.ejs +134 -0
  270. package/templates/shared/docker-compose.prod.yml.ejs +120 -0
  271. package/templates/shared/docker-compose.yml.ejs +129 -0
  272. package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
  273. package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
  274. package/templates/shared/scripts/setup.sh.ejs +979 -0
@@ -0,0 +1,115 @@
1
+ import { db } from "../../utils/db";
2
+ import { ErrorFactory } from "../../utils/errors";
3
+
4
+ /**
5
+ * User Repository
6
+ *
7
+ * Note: User creation and authentication is handled by BetterAuth.
8
+ * This repository provides helper functions for user lookups and profile updates.
9
+ */
10
+
11
+ export const getUser = async (id: string) => {
12
+ try {
13
+ const user = await db.user.findUnique({
14
+ where: { id },
15
+ select: {
16
+ id: true,
17
+ email: true,
18
+ emailVerified: true,
19
+ name: true,
20
+ image: true,
21
+ createdAt: true,
22
+ updatedAt: true,
23
+ },
24
+ });
25
+ return user;
26
+ } catch (error) {
27
+ throw ErrorFactory.databaseError({
28
+ operation: 'getUser',
29
+ userId: id,
30
+ originalError: error instanceof Error ? error.message : String(error)
31
+ });
32
+ }
33
+ };
34
+
35
+ export const getUserByEmail = async (email: string) => {
36
+ try {
37
+ const user = await db.user.findUnique({
38
+ where: { email },
39
+ select: {
40
+ id: true,
41
+ email: true,
42
+ emailVerified: true,
43
+ name: true,
44
+ image: true,
45
+ createdAt: true,
46
+ updatedAt: true,
47
+ },
48
+ });
49
+ return user;
50
+ } catch (error) {
51
+ throw ErrorFactory.databaseError({
52
+ operation: 'getUserByEmail',
53
+ email,
54
+ originalError: error instanceof Error ? error.message : String(error)
55
+ });
56
+ }
57
+ };
58
+
59
+ export const isUserExistByEmail = async (email: string): Promise<boolean> => {
60
+ try {
61
+ const userCount = await db.user.count({
62
+ where: { email },
63
+ });
64
+ return userCount > 0;
65
+ } catch (error) {
66
+ throw ErrorFactory.databaseError({
67
+ operation: 'isUserExistByEmail',
68
+ email,
69
+ originalError: error instanceof Error ? error.message : String(error)
70
+ });
71
+ }
72
+ };
73
+
74
+ export const updateUserProfile = async (
75
+ userId: string,
76
+ data: { name?: string }
77
+ ) => {
78
+ try {
79
+ const user = await db.user.update({
80
+ where: { id: userId },
81
+ data,
82
+ select: {
83
+ id: true,
84
+ email: true,
85
+ emailVerified: true,
86
+ name: true,
87
+ image: true,
88
+ createdAt: true,
89
+ updatedAt: true,
90
+ },
91
+ });
92
+ return user;
93
+ } catch (error) {
94
+ throw ErrorFactory.databaseError({
95
+ operation: 'updateUserProfile',
96
+ userId,
97
+ updateData: data,
98
+ originalError: error instanceof Error ? error.message : String(error)
99
+ });
100
+ }
101
+ };
102
+
103
+ export const deleteUser = async (userId: string): Promise<void> => {
104
+ try {
105
+ await db.user.delete({
106
+ where: { id: userId },
107
+ });
108
+ } catch (error) {
109
+ throw ErrorFactory.databaseError({
110
+ operation: 'deleteUser',
111
+ userId,
112
+ originalError: error instanceof Error ? error.message : String(error)
113
+ });
114
+ }
115
+ };
@@ -0,0 +1,14 @@
1
+ import { Static, Type } from "@sinclair/typebox";
2
+
3
+ // User schema matching BetterAuth's user model
4
+ export const UserSchema = Type.Object({
5
+ id: Type.String(),
6
+ email: Type.String(),
7
+ emailVerified: Type.Boolean(),
8
+ name: Type.Union([Type.String(), Type.Null()]),
9
+ image: Type.Union([Type.String(), Type.Null()]),
10
+ createdAt: Type.String({ format: 'date-time' }),
11
+ updatedAt: Type.String({ format: 'date-time' }),
12
+ });
13
+
14
+ export type User = Static<typeof UserSchema>;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Drizzle ORM Schema for BetterAuth + Application
3
+ *
4
+ * IMPORTANT: The user, session, account, and verification tables MUST match
5
+ * BetterAuth's expected schema structure. Do not modify column names or types
6
+ * in these tables without verifying compatibility with BetterAuth.
7
+ *
8
+ * @see https://www.better-auth.com/docs/adapters/drizzle
9
+ * @see https://www.better-auth.com/docs/concepts/database
10
+ */
11
+
12
+ import { pgTable, text, boolean, timestamp, varchar, uniqueIndex } from 'drizzle-orm/pg-core';
13
+ import { relations } from 'drizzle-orm';
14
+
15
+ // ==================== BetterAuth Tables ====================
16
+ // WARNING: These tables are managed by BetterAuth. Modify with caution.
17
+
18
+ // BetterAuth User model
19
+ export const user = pgTable('user', {
20
+ id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
21
+ email: varchar('email', { length: 255 }).notNull().unique(),
22
+ emailVerified: boolean('email_verified').default(false).notNull(),
23
+ name: text('name'),
24
+ image: text('image'),
25
+ createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
26
+ updatedAt: timestamp('updated_at', { mode: 'date' }).defaultNow().notNull().$onUpdate(() => new Date()),
27
+ });
28
+
29
+ // BetterAuth Session model
30
+ export const session = pgTable('session', {
31
+ id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
32
+ userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
33
+ token: text('token').notNull().unique(),
34
+ expiresAt: timestamp('expires_at', { mode: 'date' }).notNull(),
35
+ ipAddress: text('ip_address'),
36
+ userAgent: text('user_agent'),
37
+ createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
38
+ updatedAt: timestamp('updated_at', { mode: 'date' }).defaultNow().notNull().$onUpdate(() => new Date()),
39
+ });
40
+
41
+ // BetterAuth Account model (OAuth connections)
42
+ export const account = pgTable('account', {
43
+ id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
44
+ userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
45
+ accountId: text('account_id').notNull(),
46
+ providerId: text('provider_id').notNull(),
47
+ accessToken: text('access_token'),
48
+ refreshToken: text('refresh_token'),
49
+ accessTokenExpiresAt: timestamp('access_token_expires_at', { mode: 'date' }),
50
+ refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { mode: 'date' }),
51
+ scope: text('scope'),
52
+ idToken: text('id_token'),
53
+ password: text('password'),
54
+ createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
55
+ updatedAt: timestamp('updated_at', { mode: 'date' }).defaultNow().notNull().$onUpdate(() => new Date()),
56
+ }, (table) => ({
57
+ providerAccountIdx: uniqueIndex('provider_account_idx').on(table.providerId, table.accountId),
58
+ }));
59
+
60
+ // BetterAuth Verification model
61
+ export const verification = pgTable('verification', {
62
+ id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
63
+ identifier: text('identifier').notNull(),
64
+ value: text('value').notNull(),
65
+ expiresAt: timestamp('expires_at', { mode: 'date' }).notNull(),
66
+ createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
67
+ updatedAt: timestamp('updated_at', { mode: 'date' }).defaultNow().notNull().$onUpdate(() => new Date()),
68
+ });
69
+
70
+ // ==================== App-Specific Tables ====================
71
+
72
+ // Device Session model (anonymous sessions before authentication)
73
+ export const deviceSession = pgTable('device_session', {
74
+ id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
75
+ deviceId: text('device_id').notNull(),
76
+ sessionToken: text('session_token').notNull().unique().$defaultFn(() => crypto.randomUUID()),
77
+ createdAt: timestamp('created_at', { mode: 'date' }).defaultNow().notNull(),
78
+ lastActiveAt: timestamp('last_active_at', { mode: 'date' }).defaultNow().notNull(),
79
+ migrated: boolean('migrated').default(false).notNull(),
80
+ migratedToUserId: text('migrated_to_user_id').references(() => user.id, { onDelete: 'set null' }),
81
+ preferredCurrency: varchar('preferred_currency', { length: 3 }).default('USD').notNull(),
82
+ });
83
+
84
+ // ==================== Relations ====================
85
+
86
+ export const userRelations = relations(user, ({ many }) => ({
87
+ sessions: many(session),
88
+ accounts: many(account),
89
+ deviceSessions: many(deviceSession),
90
+ }));
91
+
92
+ export const sessionRelations = relations(session, ({ one }) => ({
93
+ user: one(user, {
94
+ fields: [session.userId],
95
+ references: [user.id],
96
+ }),
97
+ }));
98
+
99
+ export const accountRelations = relations(account, ({ one }) => ({
100
+ user: one(user, {
101
+ fields: [account.userId],
102
+ references: [user.id],
103
+ }),
104
+ }));
105
+
106
+ export const deviceSessionRelations = relations(deviceSession, ({ one }) => ({
107
+ migratedToUser: one(user, {
108
+ fields: [deviceSession.migratedToUserId],
109
+ references: [user.id],
110
+ }),
111
+ }));
@@ -0,0 +1,13 @@
1
+ import 'dotenv/config';
2
+ import { defineConfig } from 'drizzle-kit';
3
+
4
+ export default defineConfig({
5
+ out: './drizzle/migrations',
6
+ schema: './drizzle/schema.ts',
7
+ dialect: 'postgresql',
8
+ dbCredentials: {
9
+ url: process.env.DATABASE_URL!,
10
+ },
11
+ verbose: true,
12
+ strict: true,
13
+ });
@@ -0,0 +1,104 @@
1
+ import { betterAuth } from "better-auth";
2
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
3
+ import { expo } from "@better-auth/expo";
4
+ <% if (features.authentication.twoFactor) { %>
5
+ import { twoFactor } from "better-auth/plugins";
6
+ <% } %>
7
+ import { db } from "../utils/db";
8
+ import * as schema from "../drizzle/schema";
9
+ <% if (features.authentication.emailVerification || features.authentication.passwordReset) { %>
10
+ import { sendEmail } from "../utils/email";
11
+ <% } %>
12
+
13
+ export const auth = betterAuth({
14
+ database: drizzleAdapter(db, {
15
+ provider: "pg",
16
+ schema: {
17
+ user: schema.user,
18
+ session: schema.session,
19
+ account: schema.account,
20
+ verification: schema.verification,
21
+ },
22
+ }),
23
+ basePath: "/api/auth", // BetterAuth endpoints will be at /api/auth/*
24
+ emailAndPassword: {
25
+ enabled: true,
26
+ requireEmailVerification: <%= features.authentication.emailVerification %>,
27
+ <% if (features.authentication.passwordReset) { %>
28
+ sendResetPassword: async ({ user, url }) => {
29
+ await sendEmail({
30
+ to: user.email,
31
+ subject: "Reset your password",
32
+ html: `
33
+ <h1>Reset your password</h1>
34
+ <p>Click the link below to reset your password:</p>
35
+ <a href="${url}">Reset Password</a>
36
+ <p>If you didn't request this, you can safely ignore this email.</p>
37
+ `,
38
+ });
39
+ },
40
+ <% } %>
41
+ },
42
+ <% if (features.authentication.emailVerification) { %>
43
+ emailVerification: {
44
+ sendVerificationEmail: async ({ user, url }) => {
45
+ await sendEmail({
46
+ to: user.email,
47
+ subject: "Verify your email address",
48
+ html: `
49
+ <h1>Verify your email</h1>
50
+ <p>Click the link below to verify your email address:</p>
51
+ <a href="${url}">Verify Email</a>
52
+ `,
53
+ });
54
+ },
55
+ },
56
+ <% } %>
57
+ <% if (features.authentication.providers.google || features.authentication.providers.apple || features.authentication.providers.github) { %>
58
+ socialProviders: {
59
+ <% if (features.authentication.providers.google) { %>
60
+ google: {
61
+ clientId: process.env.GOOGLE_WEB_CLIENT_ID!,
62
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
63
+ },
64
+ <% } %>
65
+ <% if (features.authentication.providers.apple) { %>
66
+ apple: {
67
+ clientId: process.env.APPLE_SERVICE_ID!,
68
+ clientSecret: process.env.APPLE_CLIENT_SECRET!,
69
+ appBundleIdentifier: process.env.APPLE_BUNDLE_ID!,
70
+ },
71
+ <% } %>
72
+ <% if (features.authentication.providers.github) { %>
73
+ github: {
74
+ clientId: process.env.GITHUB_CLIENT_ID!,
75
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
76
+ },
77
+ <% } %>
78
+ },
79
+ <% } %>
80
+ plugins: [
81
+ expo(), // Required for React Native/Expo OAuth deep link handling
82
+ <% if (features.authentication.twoFactor) { %>
83
+ twoFactor(),
84
+ <% } %>
85
+ ],
86
+ session: {
87
+ expiresIn: 60 * 60 * 24 * 7, // 7 days
88
+ updateAge: 60 * 60 * 24, // 1 day
89
+ },
90
+ trustedOrigins: [
91
+ // Web origins (includes development and production URLs)
92
+ ...(process.env.TRUSTED_ORIGINS?.split(",").map(o => o.trim()) || ["http://localhost:3000"]),
93
+ // Mobile deep link scheme (required for OAuth callbacks)
94
+ "<%= appScheme %>://",
95
+ // Expo development URLs (development only)
96
+ ...(process.env.NODE_ENV === "development" ? ["exp://*/*", "exp://192.168.*.*:*/*"] : []),
97
+ <% if (features.authentication.providers.apple) { %>
98
+ // Apple Sign In origin (required for native iOS sign-in)
99
+ "https://appleid.apple.com",
100
+ <% } %>
101
+ ],
102
+ });
103
+
104
+ export type Session = typeof auth.$Infer.Session;
@@ -0,0 +1,97 @@
1
+ import { betterAuth } from "better-auth";
2
+ import { prismaAdapter } from "better-auth/adapters/prisma";
3
+ import { expo } from "@better-auth/expo";
4
+ <% if (features.authentication.twoFactor) { %>
5
+ import { twoFactor } from "better-auth/plugins";
6
+ <% } %>
7
+ import { db } from "../utils/db";
8
+ <% if (features.authentication.emailVerification || features.authentication.passwordReset) { %>
9
+ import { sendEmail } from "../utils/email";
10
+ <% } %>
11
+
12
+ export const auth = betterAuth({
13
+ database: prismaAdapter(db, {
14
+ provider: "postgresql",
15
+ }),
16
+ basePath: "/api/auth", // BetterAuth endpoints will be at /api/auth/*
17
+ emailAndPassword: {
18
+ enabled: true,
19
+ requireEmailVerification: <%= features.authentication.emailVerification %>,
20
+ <% if (features.authentication.passwordReset) { %>
21
+ sendResetPassword: async ({ user, url }) => {
22
+ await sendEmail({
23
+ to: user.email,
24
+ subject: "Reset your password",
25
+ html: `
26
+ <h1>Reset your password</h1>
27
+ <p>Click the link below to reset your password:</p>
28
+ <a href="${url}">Reset Password</a>
29
+ <p>If you didn't request this, you can safely ignore this email.</p>
30
+ `,
31
+ });
32
+ },
33
+ <% } %>
34
+ },
35
+ <% if (features.authentication.emailVerification) { %>
36
+ emailVerification: {
37
+ sendVerificationEmail: async ({ user, url }) => {
38
+ await sendEmail({
39
+ to: user.email,
40
+ subject: "Verify your email address",
41
+ html: `
42
+ <h1>Verify your email</h1>
43
+ <p>Click the link below to verify your email address:</p>
44
+ <a href="${url}">Verify Email</a>
45
+ `,
46
+ });
47
+ },
48
+ },
49
+ <% } %>
50
+ <% if (features.authentication.providers.google || features.authentication.providers.apple || features.authentication.providers.github) { %>
51
+ socialProviders: {
52
+ <% if (features.authentication.providers.google) { %>
53
+ google: {
54
+ clientId: process.env.GOOGLE_WEB_CLIENT_ID!,
55
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
56
+ },
57
+ <% } %>
58
+ <% if (features.authentication.providers.apple) { %>
59
+ apple: {
60
+ clientId: process.env.APPLE_SERVICE_ID!,
61
+ clientSecret: process.env.APPLE_CLIENT_SECRET!,
62
+ appBundleIdentifier: process.env.APPLE_BUNDLE_ID!,
63
+ },
64
+ <% } %>
65
+ <% if (features.authentication.providers.github) { %>
66
+ github: {
67
+ clientId: process.env.GITHUB_CLIENT_ID!,
68
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
69
+ },
70
+ <% } %>
71
+ },
72
+ <% } %>
73
+ plugins: [
74
+ expo(), // Required for React Native/Expo OAuth deep link handling
75
+ <% if (features.authentication.twoFactor) { %>
76
+ twoFactor(),
77
+ <% } %>
78
+ ],
79
+ session: {
80
+ expiresIn: 60 * 60 * 24 * 7, // 7 days
81
+ updateAge: 60 * 60 * 24, // 1 day
82
+ },
83
+ trustedOrigins: [
84
+ // Web origins (includes development and production URLs)
85
+ ...(process.env.TRUSTED_ORIGINS?.split(",").map(o => o.trim()) || ["http://localhost:3000"]),
86
+ // Mobile deep link scheme (required for OAuth callbacks)
87
+ "<%= appScheme %>://",
88
+ // Expo development URLs (development only)
89
+ ...(process.env.NODE_ENV === "development" ? ["exp://*/*", "exp://192.168.*.*:*/*"] : []),
90
+ <% if (features.authentication.providers.apple) { %>
91
+ // Apple Sign In origin (required for native iOS sign-in)
92
+ "https://appleid.apple.com",
93
+ <% } %>
94
+ ],
95
+ });
96
+
97
+ export type Session = typeof auth.$Infer.Session;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared constants for authentication
3
+ * Centralized to avoid magic strings scattered across the codebase
4
+ */
5
+
6
+ // Better Auth cookie name pattern
7
+ export const BETTER_AUTH_COOKIE_NAME = 'better-auth.session_token';
8
+
9
+ // Web-specific cookie names
10
+ export const SESSION_COOKIE_NAME = 'session_token';
11
+ export const DEVICE_SESSION_COOKIE_NAME = 'device_session_token';
12
+
13
+ // OAuth-related cookie names (used in PKCE flow)
14
+ export const OAUTH_PKCE_VERIFIER_COOKIE = 'oauth_pkce_verifier';
15
+ export const OAUTH_STATE_COOKIE = 'oauth_state';
16
+
17
+ // Redis key prefixes
18
+ export const REDIS_KEYS = {
19
+ OAUTH_WEB_STATE: 'oauth:web:', // oauth:web:{state}
20
+ OAUTH_EXCHANGE: 'oauth:exchange:', // oauth:exchange:{token}
21
+ } as const;
22
+
23
+ // TTLs in seconds
24
+ export const TTL = {
25
+ OAUTH_STATE: 600, // 10 minutes
26
+ EXCHANGE_TOKEN: 60, // 1 minute (very short!)
27
+ SESSION: 60 * 60 * 24 * 7, // 7 days
28
+ DEVICE_SESSION: 60 * 60 * 24 * 30, // 30 days
29
+ } as const;
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "<%= projectName %>-backend",
3
+ "version": "1.0.0",
4
+ "description": "Backend API for <%= projectName %>",
5
+ "type": "module",
6
+ "main": "controllers/rest-api/index.ts",
7
+ "scripts": {
8
+ "dev:rest-api": "bun --watch ./controllers/rest-api/index.ts | pino-pretty --colorize",<% if (backend.eventQueue) { %>
9
+ "dev:event-queue": "bun --watch ./controllers/event-queue/index.ts | pino-pretty --colorize",<% } %>
10
+ "build": "bun run tsc",
11
+ "start:rest-api": "bun run dist/controllers/rest-api/index.js",<% if (backend.eventQueue) { %>
12
+ "start:event-queue": "bun run dist/controllers/event-queue/index.js",<% } %><% if (backend.orm === 'prisma') { %>
13
+ "db:generate": "prisma generate",
14
+ "db:push": "prisma db push",
15
+ "db:migrate": "prisma migrate dev",
16
+ "db:studio": "prisma studio",
17
+ "postinstall": "prisma generate"<% } %><% if (backend.orm === 'drizzle') { %>
18
+ "db:generate": "drizzle-kit generate",
19
+ "db:push": "drizzle-kit push",
20
+ "db:migrate": "drizzle-kit migrate",
21
+ "db:studio": "drizzle-kit studio"<% } %>
22
+ },
23
+ "dependencies": {
24
+ "@fastify/cors": "^11.0.1",<% if (backend.orm === 'prisma') { %>
25
+ "@prisma/adapter-pg": "^7.0.0",
26
+ "@prisma/client": "^7.0.0",<% } %><% if (backend.orm === 'drizzle') { %>
27
+ "drizzle-orm": "^0.44.0",
28
+ "pg": "^8.13.0",<% } %>
29
+ "@sinclair/typebox": "^0.34.33",
30
+ "ajv": "^8.17.1",
31
+ "better-auth": "^1.4.5",
32
+ "@better-auth/expo": "^1.4.5",
33
+ "dotenv": "^16.5.0",
34
+ "fastify": "^5.3.3",
35
+ "fastify-plugin": "^5.0.1",
36
+ "ioredis": "^5.4.1",
37
+ "pino-pretty": "^13.0.0"<% if (backend.eventQueue) { %>,
38
+ "bullmq": "^5.40.3"<% } %><% if (features.authentication.emailVerification || features.authentication.passwordReset) { %>,
39
+ "nodemailer": "^6.9.0"<% } %>
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^24.0.0",<% if (backend.orm === 'prisma') { %>
43
+ "prisma": "^7.0.0",<% } %><% if (backend.orm === 'drizzle') { %>
44
+ "drizzle-kit": "^0.30.0",
45
+ "@types/pg": "^8.11.0",<% } %>
46
+ "tsx": "^4.20.1",
47
+ "typescript": "^5.8.3"<% if (features.authentication.emailVerification || features.authentication.passwordReset) { %>,
48
+ "@types/nodemailer": "^6.4.0"<% } %>
49
+ }
50
+ }
@@ -0,0 +1,102 @@
1
+ // This is your Prisma schema file,
2
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+ generator client {
5
+ provider = "prisma-client"
6
+ output = "./generated/prisma"
7
+ }
8
+
9
+ datasource db {
10
+ provider = "postgresql"
11
+ }
12
+
13
+ // BetterAuth User model
14
+ // Uses BetterAuth default table name: "user"
15
+ model User {
16
+ id String @id @default(cuid())
17
+ email String @unique
18
+ emailVerified Boolean @default(false)
19
+ name String?
20
+ image String?
21
+ createdAt DateTime @default(now())
22
+ updatedAt DateTime @updatedAt
23
+
24
+ // BetterAuth relations
25
+ sessions Session[]
26
+ accounts Account[]
27
+
28
+ // Device session migration relation
29
+ deviceSessions DeviceSession[]
30
+
31
+ @@map("user")
32
+ }
33
+
34
+ // BetterAuth Session model (authenticated sessions)
35
+ // Uses BetterAuth default table name: "session"
36
+ model Session {
37
+ id String @id @default(cuid())
38
+ userId String
39
+ token String @unique
40
+ expiresAt DateTime
41
+ ipAddress String?
42
+ userAgent String?
43
+ createdAt DateTime @default(now())
44
+ updatedAt DateTime @updatedAt
45
+
46
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
47
+
48
+ @@map("session")
49
+ }
50
+
51
+ // BetterAuth Account model (OAuth connections)
52
+ // Uses BetterAuth default table name: "account"
53
+ model Account {
54
+ id String @id @default(cuid())
55
+ userId String
56
+ accountId String
57
+ providerId String
58
+ accessToken String?
59
+ refreshToken String?
60
+ accessTokenExpiresAt DateTime?
61
+ refreshTokenExpiresAt DateTime?
62
+ scope String?
63
+ idToken String?
64
+ password String? // For email/password auth
65
+ createdAt DateTime @default(now())
66
+ updatedAt DateTime @updatedAt
67
+
68
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
69
+
70
+ @@unique([providerId, accountId])
71
+ @@map("account")
72
+ }
73
+
74
+ // BetterAuth Verification model (email verification, password reset)
75
+ // Uses BetterAuth default table name: "verification"
76
+ model Verification {
77
+ id String @id @default(cuid())
78
+ identifier String
79
+ value String
80
+ expiresAt DateTime
81
+ createdAt DateTime @default(now())
82
+ updatedAt DateTime @updatedAt
83
+
84
+ @@map("verification")
85
+ }
86
+
87
+ // Device Session model (anonymous device sessions before authentication)
88
+ // Renamed from Session to avoid conflict with BetterAuth's Session model
89
+ model DeviceSession {
90
+ id String @id @default(cuid())
91
+ deviceId String
92
+ sessionToken String @unique @default(uuid())
93
+ createdAt DateTime @default(now())
94
+ lastActiveAt DateTime @default(now())
95
+ migrated Boolean @default(false)
96
+ migratedToUserId String?
97
+ preferredCurrency String @default("USD")
98
+
99
+ migratedToUser User? @relation(fields: [migratedToUserId], references: [id], onDelete: SetNull)
100
+
101
+ @@map("device_session")
102
+ }
@@ -0,0 +1,12 @@
1
+ import "dotenv/config";
2
+ import { defineConfig, env } from "prisma/config";
3
+
4
+ export default defineConfig({
5
+ schema: "prisma/schema.prisma",
6
+ migrations: {
7
+ path: "prisma/migrations",
8
+ },
9
+ datasource: {
10
+ url: env("DATABASE_URL"),
11
+ },
12
+ });