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,142 @@
1
+ # Simplified Multi-stage Dockerfile for Node.js/Fastify Backend
2
+ # Clean architecture with explicit service stages
3
+ # Supports npm, yarn, and bun package managers
4
+
5
+ # =============================================================================
6
+ # Base Stage - Common setup for all services
7
+ # =============================================================================
8
+ <% if (packageManager === 'bun') { %>FROM oven/bun:alpine AS base<% } else { %>FROM node:alpine AS base<% } %>
9
+
10
+ # Install essential packages
11
+ RUN apk --no-cache add dumb-init ca-certificates
12
+
13
+ # Set working directory
14
+ WORKDIR /app
15
+
16
+ # Copy package files and lock file for dependency installation
17
+ COPY package*.json ./
18
+ <% if (packageManager === 'bun') { %>COPY bun.lock* ./<% } else if (packageManager === 'npm') { %>COPY package-lock.json ./<% } else { %>COPY yarn.lock ./<% } %>
19
+ <% if (backend.orm === 'prisma') { %>
20
+ COPY prisma/schema.prisma ./prisma/
21
+ COPY prisma.config.ts ./
22
+ <% } else if (backend.orm === 'drizzle') { %>
23
+ COPY drizzle/schema.ts ./drizzle/
24
+ COPY drizzle.config.ts ./
25
+ <% } %>
26
+ COPY .env ./
27
+
28
+ # Install dependencies and generate ORM client
29
+ <% if (packageManager === 'bun') { %>RUN bun install && bun run db:generate<% } else if (packageManager === 'npm') { %>RUN npm ci && npm run db:generate<% } else { %>RUN yarn install --frozen-lockfile && yarn db:generate<% } %>
30
+
31
+ # Copy source code
32
+ COPY . .
33
+
34
+ # =============================================================================
35
+ # Development Stages - For local development with hot reload
36
+ # =============================================================================
37
+ FROM base AS rest-api-dev
38
+ EXPOSE 8080
39
+ <% if (packageManager === 'bun') { %>CMD ["dumb-init", "bun", "run", "dev:rest-api"]<% } else if (packageManager === 'npm') { %>CMD ["dumb-init", "npm", "run", "dev:rest-api"]<% } else { %>CMD ["dumb-init", "yarn", "dev:rest-api"]<% } %>
40
+
41
+ FROM base AS event-queue-dev
42
+ <% if (packageManager === 'bun') { %>CMD ["dumb-init", "bun", "run", "dev:event-queue"]<% } else if (packageManager === 'npm') { %>CMD ["dumb-init", "npm", "run", "dev:event-queue"]<% } else { %>CMD ["dumb-init", "yarn", "dev:event-queue"]<% } %>
43
+
44
+ # =============================================================================
45
+ # Production Stages - Optimized for production deployment
46
+ # =============================================================================
47
+ <% if (packageManager === 'bun') { %>FROM oven/bun:alpine AS rest-api-prod<% } else { %>FROM node:alpine AS rest-api-prod<% } %>
48
+
49
+ # Install essential packages and create non-root user
50
+ RUN apk --no-cache add dumb-init ca-certificates \
51
+ <% if (packageManager === 'bun') { %> && addgroup -g 1001 -S bunjs \
52
+ && adduser -S backend -u 1001 -G bunjs<% } else { %> && addgroup -g 1001 -S nodejs \
53
+ && adduser -S backend -u 1001 -G nodejs<% } %>
54
+
55
+ # Set production environment
56
+ ENV NODE_ENV=production
57
+ WORKDIR /app
58
+
59
+ # Copy package files and lock file first (for better layer caching)
60
+ COPY package*.json ./
61
+ <% if (packageManager === 'bun') { %>COPY bun.lock* ./<% } else if (packageManager === 'npm') { %>COPY package-lock.json ./<% } else { %>COPY yarn.lock ./<% } %>
62
+
63
+ # Install production dependencies only (clean install)
64
+ <% if (packageManager === 'bun') { %>RUN bun install --production<% } else if (packageManager === 'npm') { %>RUN npm ci --production<% } else { %>RUN yarn install --production --frozen-lockfile<% } %>
65
+
66
+ # Copy ORM schema, config, and generate client
67
+ <% if (backend.orm === 'prisma') { %>
68
+ COPY prisma/schema.prisma ./prisma/
69
+ COPY prisma.config.ts ./
70
+ <% } else if (backend.orm === 'drizzle') { %>
71
+ COPY drizzle/schema.ts ./drizzle/
72
+ COPY drizzle.config.ts ./
73
+ <% } %>
74
+ COPY .env ./
75
+ <% if (packageManager === 'bun') { %>RUN bun run db:generate<% } else if (packageManager === 'npm') { %>RUN npm run db:generate<% } else { %>RUN yarn db:generate<% } %>
76
+
77
+ # Copy source code
78
+ COPY . .
79
+
80
+ # Build the TypeScript application
81
+ <% if (packageManager === 'bun') { %>RUN bun run build<% } else if (packageManager === 'npm') { %>RUN npm run build<% } else { %>RUN yarn build<% } %>
82
+
83
+ # Change ownership to non-root user
84
+ <% if (packageManager === 'bun') { %>RUN chown -R backend:bunjs /app<% } else { %>RUN chown -R backend:nodejs /app<% } %>
85
+
86
+ # Switch to non-root user
87
+ USER backend
88
+
89
+ # Expose port and add health check
90
+ EXPOSE 8080
91
+ <% if (packageManager === 'bun') { %>HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
92
+ CMD bun -e "const res = await fetch('http://localhost:8080/'); process.exit(res.ok ? 0 : 1)"<% } else { %>HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
93
+ CMD node -e "fetch('http://localhost:8080/').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"<% } %>
94
+
95
+ # Start the REST API service
96
+ <% if (packageManager === 'bun') { %>CMD ["dumb-init", "bun", "run", "start:rest-api"]<% } else if (packageManager === 'npm') { %>CMD ["dumb-init", "npm", "run", "start:rest-api"]<% } else { %>CMD ["dumb-init", "yarn", "start:rest-api"]<% } %>
97
+
98
+ # =============================================================================
99
+ <% if (packageManager === 'bun') { %>FROM oven/bun:alpine AS event-queue-prod<% } else { %>FROM node:alpine AS event-queue-prod<% } %>
100
+
101
+ # Install essential packages and create non-root user
102
+ RUN apk --no-cache add dumb-init ca-certificates \
103
+ <% if (packageManager === 'bun') { %> && addgroup -g 1001 -S bunjs \
104
+ && adduser -S backend -u 1001 -G bunjs<% } else { %> && addgroup -g 1001 -S nodejs \
105
+ && adduser -S backend -u 1001 -G nodejs<% } %>
106
+
107
+ # Set production environment
108
+ ENV NODE_ENV=production
109
+ WORKDIR /app
110
+
111
+ # Copy package files and lock file first (for better layer caching)
112
+ COPY package*.json ./
113
+ <% if (packageManager === 'bun') { %>COPY bun.lock* ./<% } else if (packageManager === 'npm') { %>COPY package-lock.json ./<% } else { %>COPY yarn.lock ./<% } %>
114
+
115
+ # Install production dependencies only (clean install)
116
+ <% if (packageManager === 'bun') { %>RUN bun install --production<% } else if (packageManager === 'npm') { %>RUN npm ci --production<% } else { %>RUN yarn install --production --frozen-lockfile<% } %>
117
+
118
+ # Copy ORM schema, config, and generate client
119
+ <% if (backend.orm === 'prisma') { %>
120
+ COPY prisma/schema.prisma ./prisma/
121
+ COPY prisma.config.ts ./
122
+ <% } else if (backend.orm === 'drizzle') { %>
123
+ COPY drizzle/schema.ts ./drizzle/
124
+ COPY drizzle.config.ts ./
125
+ <% } %>
126
+ COPY .env ./
127
+ <% if (packageManager === 'bun') { %>RUN bun run db:generate<% } else if (packageManager === 'npm') { %>RUN npm run db:generate<% } else { %>RUN yarn db:generate<% } %>
128
+
129
+ # Copy source code
130
+ COPY . .
131
+
132
+ # Build the TypeScript application
133
+ <% if (packageManager === 'bun') { %>RUN bun run build<% } else if (packageManager === 'npm') { %>RUN npm run build<% } else { %>RUN yarn build<% } %>
134
+
135
+ # Change ownership to non-root user
136
+ <% if (packageManager === 'bun') { %>RUN chown -R backend:bunjs /app<% } else { %>RUN chown -R backend:nodejs /app<% } %>
137
+
138
+ # Switch to non-root user
139
+ USER backend
140
+
141
+ # Start the event queue service
142
+ <% if (packageManager === 'bun') { %>CMD ["dumb-init", "bun", "run", "start:event-queue"]<% } else if (packageManager === 'npm') { %>CMD ["dumb-init", "npm", "run", "start:event-queue"]<% } else { %>CMD ["dumb-init", "yarn", "start:event-queue"]<% } %>
@@ -0,0 +1,20 @@
1
+ import dotenv from "dotenv";
2
+ dotenv.config({ path: ".env" });
3
+
4
+ import { Redis } from "ioredis";
5
+ import { createUserWorker } from "./workers/user";
6
+
7
+ const redisConnection = new Redis({
8
+ host: process.env.REDIS_HOST || "localhost",
9
+ port: parseInt(process.env.REDIS_PORT || "6379"),
10
+ password: process.env.REDIS_PASSWORD || "",
11
+ maxRetriesPerRequest: null,
12
+ });
13
+
14
+ // Create a account worker
15
+ const userWorker = createUserWorker(redisConnection, {
16
+ concurrency: 20,
17
+ autorun: true,
18
+ });
19
+
20
+ console.log("User worker started");
@@ -0,0 +1,39 @@
1
+ import { Worker, Job } from "bullmq";
2
+ import { Redis, Cluster as RedisCluster } from "ioredis";
3
+
4
+ /**
5
+ * User Worker
6
+ *
7
+ * Note: User creation/authentication is handled by Better Auth.
8
+ * This worker handles async user-related tasks like sending welcome emails,
9
+ * syncing with external services, etc.
10
+ */
11
+ export const createUserWorker = (
12
+ connection: Redis | RedisCluster,
13
+ options: {
14
+ concurrency?: number;
15
+ autorun?: boolean;
16
+ }
17
+ ) => {
18
+ return new Worker(
19
+ "user",
20
+ async (job: Job) => {
21
+ switch (job.name) {
22
+ case "send_welcome_email":
23
+ // TODO: Implement welcome email sending
24
+ console.log(`Sending welcome email to user: ${job.data.userId}`);
25
+ break;
26
+ case "sync_user_profile":
27
+ // TODO: Sync user profile with external services
28
+ console.log(`Syncing profile for user: ${job.data.userId}`);
29
+ break;
30
+ default:
31
+ console.log(`Unknown job: ${job.name}`);
32
+ }
33
+ },
34
+ {
35
+ connection,
36
+ ...options,
37
+ }
38
+ );
39
+ };
@@ -0,0 +1,48 @@
1
+ import server from "./server";
2
+
3
+ const start = async () => {
4
+ try {
5
+ const host = server.config.API_HOST || '0.0.0.0';
6
+ const port = parseInt(server.config.API_PORT || '8080');
7
+
8
+ await server.listen({
9
+ host,
10
+ port,
11
+ });
12
+
13
+ console.log(`🚀 Server is running at http://${host}:${port}`);
14
+ console.log(`📚 API Documentation available at http://${host}:${port}/documentation (if enabled)`);
15
+
16
+ } catch (error) {
17
+ server.log.error(error);
18
+ console.error('❌ Failed to start server:', error);
19
+ process.exit(1);
20
+ }
21
+ };
22
+
23
+ // Handle graceful shutdown
24
+ process.on('SIGINT', async () => {
25
+ console.log('\n🛑 Received SIGINT, gracefully shutting down...');
26
+ try {
27
+ await server.close();
28
+ console.log('✅ Server closed successfully');
29
+ process.exit(0);
30
+ } catch (error) {
31
+ console.error('❌ Error during shutdown:', error);
32
+ process.exit(1);
33
+ }
34
+ });
35
+
36
+ process.on('SIGTERM', async () => {
37
+ console.log('\n🛑 Received SIGTERM, gracefully shutting down...');
38
+ try {
39
+ await server.close();
40
+ console.log('✅ Server closed successfully');
41
+ process.exit(0);
42
+ } catch (error) {
43
+ console.error('❌ Error during shutdown:', error);
44
+ process.exit(1);
45
+ }
46
+ });
47
+
48
+ start();
@@ -0,0 +1,152 @@
1
+ import type {
2
+ AuthFastifyRequest,
3
+ DeviceSessionFastifyRequest,
4
+ AuthOrDeviceSessionFastifyRequest,
5
+ } from "fastify";
6
+ import { FastifyPluginAsync, FastifyRequest, FastifyReply } from "fastify";
7
+ import fp from "fastify-plugin";
8
+ import { auth, Session } from "../../../lib/auth";
9
+ import { validateDeviceSession, updateDeviceSessionActivity } from "../../../domain/device-session/repository";
10
+ import { DeviceSession } from "../../../domain/device-session/schema";
11
+ import { ErrorFactory, normalizeError } from "../../../utils/errors";
12
+
13
+ /**
14
+ * Convert Node.js/Fastify headers to Web API Headers object for BetterAuth
15
+ */
16
+ function toHeaders(requestHeaders: FastifyRequest["headers"]): Headers {
17
+ const headers = new Headers();
18
+ Object.entries(requestHeaders).forEach(([key, value]) => {
19
+ if (value) headers.set(key, Array.isArray(value) ? value[0] : value);
20
+ });
21
+ return headers;
22
+ }
23
+
24
+ const authPlugin: FastifyPluginAsync = async (server) => {
25
+ // Authenticated user middleware (using BetterAuth session)
26
+ // Uses cookie-based authentication for both mobile and web clients
27
+ server.decorate(
28
+ "requireAuth",
29
+ async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
30
+ try {
31
+ // Validate session via BetterAuth (reads cookies from headers)
32
+ const session = await auth.api.getSession({
33
+ headers: toHeaders(request.headers),
34
+ });
35
+
36
+ if (!session || !session.user) {
37
+ throw ErrorFactory.unauthorized();
38
+ }
39
+
40
+ // Attach to request
41
+ (request as AuthFastifyRequest).user = session.user;
42
+ (request as AuthFastifyRequest).session = session.session;
43
+ } catch (error) {
44
+ const appError = normalizeError(error);
45
+ const response = appError.toApiResponse(request.id, request.url);
46
+ return reply.status(appError.statusCode).send(response);
47
+ }
48
+ }
49
+ );
50
+
51
+ // Anonymous device session middleware (for device-based sessions before auth)
52
+ server.decorate(
53
+ "requireDeviceSession",
54
+ async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
55
+ try {
56
+ const sessionTokenHeader = request.headers["x-device-session-token"] as string;
57
+ if (!sessionTokenHeader) {
58
+ throw ErrorFactory.tokenMissing();
59
+ }
60
+
61
+ const deviceSession = await validateDeviceSession(sessionTokenHeader);
62
+ if (!deviceSession) {
63
+ throw ErrorFactory.sessionNotFound();
64
+ }
65
+
66
+ await updateDeviceSessionActivity(sessionTokenHeader);
67
+
68
+ (request as DeviceSessionFastifyRequest).deviceSession = deviceSession;
69
+ (request as DeviceSessionFastifyRequest).sessionToken = sessionTokenHeader;
70
+ } catch (error) {
71
+ const appError = normalizeError(error);
72
+ const response = appError.toApiResponse(request.id, request.url);
73
+ return reply.status(appError.statusCode).send(response);
74
+ }
75
+ }
76
+ );
77
+
78
+ // Flexible auth: accepts either BetterAuth session or device session
79
+ server.decorate(
80
+ "requireAuthOrDeviceSession",
81
+ async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
82
+ try {
83
+ // Try BetterAuth session first (cookie-based)
84
+ try {
85
+ const session = await auth.api.getSession({
86
+ headers: toHeaders(request.headers),
87
+ });
88
+
89
+ if (session && session.user) {
90
+ (request as AuthOrDeviceSessionFastifyRequest).user = session.user;
91
+ (request as AuthOrDeviceSessionFastifyRequest).session = session.session;
92
+ (request as AuthOrDeviceSessionFastifyRequest).authType = "user";
93
+ return;
94
+ }
95
+ } catch {
96
+ // BetterAuth session not found, try device session
97
+ }
98
+
99
+ // Try device session
100
+ const sessionTokenHeader = request.headers["x-device-session-token"] as string;
101
+ if (sessionTokenHeader) {
102
+ const deviceSession = await validateDeviceSession(sessionTokenHeader);
103
+ if (deviceSession) {
104
+ await updateDeviceSessionActivity(sessionTokenHeader);
105
+ (request as AuthOrDeviceSessionFastifyRequest).deviceSession = deviceSession;
106
+ (request as AuthOrDeviceSessionFastifyRequest).sessionToken = sessionTokenHeader;
107
+ (request as AuthOrDeviceSessionFastifyRequest).authType = "device";
108
+ return;
109
+ }
110
+ }
111
+
112
+ throw ErrorFactory.unauthorized();
113
+ } catch (error) {
114
+ const appError = normalizeError(error);
115
+ const response = appError.toApiResponse(request.id, request.url);
116
+ return reply.status(appError.statusCode).send(response);
117
+ }
118
+ }
119
+ );
120
+ };
121
+
122
+ // Extend Fastify module with auth types and middleware decorators
123
+ declare module "fastify" {
124
+ // For authenticated users (BetterAuth session)
125
+ export interface AuthFastifyRequest extends FastifyRequest {
126
+ user: Session["user"];
127
+ session: Session["session"];
128
+ }
129
+
130
+ // For device/anonymous sessions (before user authentication)
131
+ export interface DeviceSessionFastifyRequest extends FastifyRequest {
132
+ deviceSession: DeviceSession;
133
+ sessionToken: string;
134
+ }
135
+
136
+ // For endpoints that accept either auth type
137
+ export interface AuthOrDeviceSessionFastifyRequest extends FastifyRequest {
138
+ user?: Session["user"];
139
+ session?: Session["session"];
140
+ deviceSession?: DeviceSession;
141
+ sessionToken?: string;
142
+ authType: "user" | "device";
143
+ }
144
+
145
+ interface FastifyInstance {
146
+ requireAuth: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
147
+ requireDeviceSession: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
148
+ requireAuthOrDeviceSession: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
149
+ }
150
+ }
151
+
152
+ export default fp(authPlugin);
@@ -0,0 +1,64 @@
1
+ import fp from "fastify-plugin";
2
+ import { FastifyPluginAsync } from "fastify";
3
+ import { Static, Type } from "@sinclair/typebox";
4
+ import Ajv from "ajv";
5
+
6
+ import dotenv from "dotenv";
7
+ dotenv.config();
8
+
9
+ export enum NodeEnv {
10
+ development = "development",
11
+ test = "test",
12
+ production = "production",
13
+ }
14
+
15
+ const ConfigSchema = Type.Object({
16
+ NODE_ENV: Type.Optional(Type.Enum(NodeEnv)),
17
+ LOG_LEVEL: Type.Optional(Type.String()),
18
+ API_HOST: Type.Optional(Type.String()),
19
+ API_PORT: Type.Optional(Type.String()),
20
+ DATABASE_URL: Type.String(),
21
+ JWT_SECRET: Type.Optional(Type.String()),
22
+ });
23
+
24
+ const ajv = new Ajv({
25
+ allErrors: true,
26
+ removeAdditional: true,
27
+ useDefaults: true,
28
+ coerceTypes: true,
29
+ });
30
+
31
+ export type Config = Static<typeof ConfigSchema>;
32
+
33
+ const configPlugin: FastifyPluginAsync = async (server) => {
34
+ // Set defaults for optional fields
35
+ const configWithDefaults = {
36
+ NODE_ENV: process.env.NODE_ENV || 'development',
37
+ LOG_LEVEL: process.env.LOG_LEVEL || 'info',
38
+ API_HOST: process.env.API_HOST || '0.0.0.0',
39
+ API_PORT: process.env.API_PORT || '8080',
40
+ DATABASE_URL: process.env.DATABASE_URL,
41
+ JWT_SECRET: process.env.JWT_SECRET || 'your-jwt-secret-here-change-in-production',
42
+ ...process.env,
43
+ };
44
+
45
+ const validate = ajv.compile(ConfigSchema);
46
+ const valid = validate(configWithDefaults);
47
+
48
+ if (!valid) {
49
+ throw new Error(
50
+ ".env file validation failed - " +
51
+ JSON.stringify(validate.errors, null, 2)
52
+ );
53
+ }
54
+
55
+ server.decorate("config", configWithDefaults as Config);
56
+ };
57
+
58
+ declare module "fastify" {
59
+ interface FastifyInstance {
60
+ config: Config;
61
+ }
62
+ }
63
+
64
+ export default fp(configPlugin);
@@ -0,0 +1,118 @@
1
+ import fp from "fastify-plugin";
2
+ import { FastifyPluginAsync, FastifyError, FastifyRequest, FastifyReply } from "fastify";
3
+ import { AppError, normalizeError, shouldIncludeErrorDetails, ErrorFactory } from "../../../utils/errors";
4
+
5
+ const errorHandlerPlugin: FastifyPluginAsync = async (server) => {
6
+ // Global error handler
7
+ server.setErrorHandler(async (error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {
8
+ const isDevelopment = process.env.NODE_ENV === 'development';
9
+
10
+ // Log the error for debugging
11
+ server.log.error({
12
+ error: {
13
+ message: error.message,
14
+ stack: error.stack,
15
+ statusCode: error.statusCode,
16
+ validation: error.validation,
17
+ },
18
+ request: {
19
+ id: request.id,
20
+ method: request.method,
21
+ url: request.url,
22
+ headers: request.headers,
23
+ body: request.body,
24
+ },
25
+ }, 'Request error occurred');
26
+
27
+ let appError: AppError;
28
+
29
+ // Handle different types of errors
30
+ if (error instanceof AppError) {
31
+ // Already an AppError, use as-is
32
+ appError = error;
33
+ } else if (error.statusCode === 400 && error.validation) {
34
+ // Fastify validation error
35
+ appError = ErrorFactory.validationFailed({
36
+ fields: error.validation,
37
+ originalMessage: error.message
38
+ });
39
+ } else if (error.statusCode === 401) {
40
+ // Unauthorized error
41
+ appError = ErrorFactory.tokenInvalid({ originalMessage: error.message });
42
+ } else if (error.statusCode === 403) {
43
+ // Forbidden error
44
+ appError = ErrorFactory.permissionDenied();
45
+ } else if (error.statusCode === 404) {
46
+ // Not found error
47
+ appError = ErrorFactory.resourceNotFound('resource');
48
+ } else if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
49
+ // Other client errors
50
+ appError = ErrorFactory.clientError(
51
+ error.message || 'Bad request',
52
+ error.statusCode
53
+ );
54
+ } else {
55
+ // Server errors or unknown errors
56
+ appError = normalizeError(error);
57
+ }
58
+
59
+ // Determine if we should include error details
60
+ const includeDetails = shouldIncludeErrorDetails(appError, isDevelopment);
61
+
62
+ // Create the response
63
+ const response = appError.toApiResponse(request.id, request.url);
64
+
65
+ // Remove details if they shouldn't be included
66
+ if (!includeDetails) {
67
+ delete response.error.details;
68
+ }
69
+
70
+ // Add stack trace in development for critical errors
71
+ if (isDevelopment && appError.severity === 'critical') {
72
+ response.error.details = {
73
+ ...response.error.details,
74
+ stack: error.stack
75
+ };
76
+ }
77
+
78
+ // Send the error response
79
+ return reply.status(appError.statusCode).send(response);
80
+ });
81
+
82
+ // Handle 404 errors for routes that don't exist
83
+ server.setNotFoundHandler(async (request: FastifyRequest, reply: FastifyReply) => {
84
+ const appError = ErrorFactory.routeNotFound(request.method, request.url);
85
+ const response = appError.toApiResponse(request.id, request.url);
86
+ return reply.status(404).send(response);
87
+ });
88
+
89
+ // Add request ID generation for better error tracking
90
+ server.addHook('onRequest', async (request) => {
91
+ // Generate a unique request ID if not already present
92
+ if (!request.id) {
93
+ request.id = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
94
+ }
95
+ });
96
+
97
+ // Add response time tracking
98
+ server.addHook('onRequest', async (request) => {
99
+ (request as any).startTime = Date.now();
100
+ });
101
+
102
+ server.addHook('onResponse', async (request, reply) => {
103
+ const responseTime = Date.now() - ((request as any).startTime || Date.now());
104
+ server.log.info({
105
+ request: {
106
+ id: request.id,
107
+ method: request.method,
108
+ url: request.url,
109
+ },
110
+ response: {
111
+ statusCode: reply.statusCode,
112
+ responseTime: `${responseTime}ms`
113
+ }
114
+ }, 'Request completed');
115
+ });
116
+ };
117
+
118
+ export default fp(errorHandlerPlugin);