@alfredmouelle/create-stack 0.1.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 (206) hide show
  1. package/README.md +56 -0
  2. package/_stack/apps/next-base/.dockerignore +10 -0
  3. package/_stack/apps/next-base/Dockerfile +34 -0
  4. package/_stack/apps/next-base/README.md +32 -0
  5. package/_stack/apps/next-base/components.json +25 -0
  6. package/_stack/apps/next-base/drizzle.config.ts +16 -0
  7. package/_stack/apps/next-base/next.config.ts +8 -0
  8. package/_stack/apps/next-base/package.json +70 -0
  9. package/_stack/apps/next-base/postcss.config.mjs +7 -0
  10. package/_stack/apps/next-base/src/app/api/auth/[...all]/route.ts +4 -0
  11. package/_stack/apps/next-base/src/app/api/trpc/[trpc]/route.ts +23 -0
  12. package/_stack/apps/next-base/src/app/auth/_components/forgot-password-form.tsx +92 -0
  13. package/_stack/apps/next-base/src/app/auth/_components/reset-password-form.tsx +105 -0
  14. package/_stack/apps/next-base/src/app/auth/_components/sign-in-form.tsx +126 -0
  15. package/_stack/apps/next-base/src/app/auth/_components/sign-up-form.tsx +139 -0
  16. package/_stack/apps/next-base/src/app/auth/_components/verify-email-actions.tsx +45 -0
  17. package/_stack/apps/next-base/src/app/auth/forgot-password/page.tsx +19 -0
  18. package/_stack/apps/next-base/src/app/auth/layout.tsx +9 -0
  19. package/_stack/apps/next-base/src/app/auth/reset-password/page.tsx +26 -0
  20. package/_stack/apps/next-base/src/app/auth/sign-in/page.tsx +27 -0
  21. package/_stack/apps/next-base/src/app/auth/sign-up/page.tsx +27 -0
  22. package/_stack/apps/next-base/src/app/auth/verify-email/page.tsx +30 -0
  23. package/_stack/apps/next-base/src/app/dashboard/page.tsx +12 -0
  24. package/_stack/apps/next-base/src/app/globals.css +171 -0
  25. package/_stack/apps/next-base/src/app/layout.tsx +23 -0
  26. package/_stack/apps/next-base/src/app/page.tsx +15 -0
  27. package/_stack/apps/next-base/src/components/data-table.tsx +77 -0
  28. package/_stack/apps/next-base/src/components/infinite-data-table.tsx +102 -0
  29. package/_stack/apps/next-base/src/components/sortable-header.tsx +37 -0
  30. package/_stack/apps/next-base/src/components/theme-provider.tsx +8 -0
  31. package/_stack/apps/next-base/src/components/theme-toggle.tsx +37 -0
  32. package/_stack/apps/next-base/src/components/ui/button.tsx +64 -0
  33. package/_stack/apps/next-base/src/components/ui/calendar.tsx +185 -0
  34. package/_stack/apps/next-base/src/components/ui/card.tsx +84 -0
  35. package/_stack/apps/next-base/src/components/ui/date-picker.tsx +85 -0
  36. package/_stack/apps/next-base/src/components/ui/date-range-picker.tsx +138 -0
  37. package/_stack/apps/next-base/src/components/ui/dropdown-menu.tsx +246 -0
  38. package/_stack/apps/next-base/src/components/ui/form.tsx +149 -0
  39. package/_stack/apps/next-base/src/components/ui/input-group.tsx +97 -0
  40. package/_stack/apps/next-base/src/components/ui/input.tsx +18 -0
  41. package/_stack/apps/next-base/src/components/ui/label.tsx +18 -0
  42. package/_stack/apps/next-base/src/components/ui/popover.tsx +76 -0
  43. package/_stack/apps/next-base/src/components/ui/skeleton.tsx +13 -0
  44. package/_stack/apps/next-base/src/components/ui/spinner.tsx +8 -0
  45. package/_stack/apps/next-base/src/components/ui/table.tsx +87 -0
  46. package/_stack/apps/next-base/src/emails/components/components.tsx +199 -0
  47. package/_stack/apps/next-base/src/emails/components/context.tsx +18 -0
  48. package/_stack/apps/next-base/src/emails/components/index.ts +23 -0
  49. package/_stack/apps/next-base/src/emails/components/theme.ts +65 -0
  50. package/_stack/apps/next-base/src/emails/reset-password.tsx +16 -0
  51. package/_stack/apps/next-base/src/emails/verify-email.tsx +15 -0
  52. package/_stack/apps/next-base/src/env.ts +41 -0
  53. package/_stack/apps/next-base/src/features/auth/auth-card.tsx +30 -0
  54. package/_stack/apps/next-base/src/features/auth/form-alert.tsx +27 -0
  55. package/_stack/apps/next-base/src/features/auth/google-button.tsx +66 -0
  56. package/_stack/apps/next-base/src/features/auth/schemas.ts +35 -0
  57. package/_stack/apps/next-base/src/lib/date.ts +4 -0
  58. package/_stack/apps/next-base/src/lib/utils.ts +6 -0
  59. package/_stack/apps/next-base/src/server/api/root.ts +10 -0
  60. package/_stack/apps/next-base/src/server/api/routers/health.router.ts +8 -0
  61. package/_stack/apps/next-base/src/server/api/trpc.ts +56 -0
  62. package/_stack/apps/next-base/src/server/auth/guards.ts +10 -0
  63. package/_stack/apps/next-base/src/server/better-auth/client.ts +9 -0
  64. package/_stack/apps/next-base/src/server/better-auth/config.ts +60 -0
  65. package/_stack/apps/next-base/src/server/better-auth/emails.tsx +25 -0
  66. package/_stack/apps/next-base/src/server/better-auth/index.ts +1 -0
  67. package/_stack/apps/next-base/src/server/better-auth/server.ts +14 -0
  68. package/_stack/apps/next-base/src/server/db/index.ts +6 -0
  69. package/_stack/apps/next-base/src/server/db/keyset.ts +63 -0
  70. package/_stack/apps/next-base/src/server/db/schemas/auth.schema.ts +71 -0
  71. package/_stack/apps/next-base/src/server/db/schemas/index.ts +2 -0
  72. package/_stack/apps/next-base/src/server/db/seed.ts +27 -0
  73. package/_stack/apps/next-base/src/server/email/adapters/resend/config.ts +7 -0
  74. package/_stack/apps/next-base/src/server/email/adapters/resend/index.ts +75 -0
  75. package/_stack/apps/next-base/src/server/email/core/address.ts +21 -0
  76. package/_stack/apps/next-base/src/server/email/core/port.ts +89 -0
  77. package/_stack/apps/next-base/src/server/email/core/render.ts +16 -0
  78. package/_stack/apps/next-base/src/server/email/factory.ts +47 -0
  79. package/_stack/apps/next-base/src/server/email/index.ts +36 -0
  80. package/_stack/apps/next-base/src/trpc/query-client.ts +19 -0
  81. package/_stack/apps/next-base/src/trpc/react.tsx +62 -0
  82. package/_stack/apps/next-base/src/trpc/server.ts +23 -0
  83. package/_stack/apps/next-base/tsconfig.json +37 -0
  84. package/_stack/apps/tanstack-base/.dockerignore +13 -0
  85. package/_stack/apps/tanstack-base/Dockerfile +28 -0
  86. package/_stack/apps/tanstack-base/README.md +31 -0
  87. package/_stack/apps/tanstack-base/components.json +25 -0
  88. package/_stack/apps/tanstack-base/drizzle.config.ts +16 -0
  89. package/_stack/apps/tanstack-base/package.json +85 -0
  90. package/_stack/apps/tanstack-base/public/favicon.ico +0 -0
  91. package/_stack/apps/tanstack-base/public/logo192.png +0 -0
  92. package/_stack/apps/tanstack-base/public/logo512.png +0 -0
  93. package/_stack/apps/tanstack-base/public/manifest.json +25 -0
  94. package/_stack/apps/tanstack-base/public/robots.txt +3 -0
  95. package/_stack/apps/tanstack-base/src/components/data-table.tsx +77 -0
  96. package/_stack/apps/tanstack-base/src/components/form/field-error.tsx +18 -0
  97. package/_stack/apps/tanstack-base/src/components/form/text-field.tsx +47 -0
  98. package/_stack/apps/tanstack-base/src/components/infinite-data-table.tsx +102 -0
  99. package/_stack/apps/tanstack-base/src/components/sortable-header.tsx +37 -0
  100. package/_stack/apps/tanstack-base/src/components/theme-provider.tsx +69 -0
  101. package/_stack/apps/tanstack-base/src/components/theme-toggle.tsx +35 -0
  102. package/_stack/apps/tanstack-base/src/components/ui/button.tsx +64 -0
  103. package/_stack/apps/tanstack-base/src/components/ui/calendar.tsx +185 -0
  104. package/_stack/apps/tanstack-base/src/components/ui/card.tsx +84 -0
  105. package/_stack/apps/tanstack-base/src/components/ui/date-picker.tsx +83 -0
  106. package/_stack/apps/tanstack-base/src/components/ui/date-range-picker.tsx +136 -0
  107. package/_stack/apps/tanstack-base/src/components/ui/dropdown-menu.tsx +246 -0
  108. package/_stack/apps/tanstack-base/src/components/ui/input-group.tsx +97 -0
  109. package/_stack/apps/tanstack-base/src/components/ui/input.tsx +18 -0
  110. package/_stack/apps/tanstack-base/src/components/ui/label.tsx +18 -0
  111. package/_stack/apps/tanstack-base/src/components/ui/popover.tsx +74 -0
  112. package/_stack/apps/tanstack-base/src/components/ui/skeleton.tsx +13 -0
  113. package/_stack/apps/tanstack-base/src/components/ui/spinner.tsx +8 -0
  114. package/_stack/apps/tanstack-base/src/components/ui/table.tsx +87 -0
  115. package/_stack/apps/tanstack-base/src/emails/components/components.tsx +199 -0
  116. package/_stack/apps/tanstack-base/src/emails/components/context.tsx +18 -0
  117. package/_stack/apps/tanstack-base/src/emails/components/index.ts +23 -0
  118. package/_stack/apps/tanstack-base/src/emails/components/theme.ts +65 -0
  119. package/_stack/apps/tanstack-base/src/emails/reset-password.tsx +16 -0
  120. package/_stack/apps/tanstack-base/src/emails/verify-email.tsx +15 -0
  121. package/_stack/apps/tanstack-base/src/env.ts +41 -0
  122. package/_stack/apps/tanstack-base/src/features/auth/auth-card.tsx +30 -0
  123. package/_stack/apps/tanstack-base/src/features/auth/form-alert.tsx +27 -0
  124. package/_stack/apps/tanstack-base/src/features/auth/google-button.tsx +64 -0
  125. package/_stack/apps/tanstack-base/src/features/auth/schemas.ts +35 -0
  126. package/_stack/apps/tanstack-base/src/lib/date.ts +4 -0
  127. package/_stack/apps/tanstack-base/src/lib/utils.ts +6 -0
  128. package/_stack/apps/tanstack-base/src/router.tsx +40 -0
  129. package/_stack/apps/tanstack-base/src/routes/__root.tsx +73 -0
  130. package/_stack/apps/tanstack-base/src/routes/_authed/dashboard.tsx +12 -0
  131. package/_stack/apps/tanstack-base/src/routes/_authed.tsx +21 -0
  132. package/_stack/apps/tanstack-base/src/routes/api/auth/$.ts +14 -0
  133. package/_stack/apps/tanstack-base/src/routes/api.trpc.$.tsx +31 -0
  134. package/_stack/apps/tanstack-base/src/routes/auth/forgot-password.tsx +89 -0
  135. package/_stack/apps/tanstack-base/src/routes/auth/reset-password.tsx +111 -0
  136. package/_stack/apps/tanstack-base/src/routes/auth/sign-in.tsx +117 -0
  137. package/_stack/apps/tanstack-base/src/routes/auth/sign-up.tsx +119 -0
  138. package/_stack/apps/tanstack-base/src/routes/auth/verify-email.tsx +72 -0
  139. package/_stack/apps/tanstack-base/src/routes/auth.tsx +22 -0
  140. package/_stack/apps/tanstack-base/src/routes/index.tsx +18 -0
  141. package/_stack/apps/tanstack-base/src/server/api/root.ts +10 -0
  142. package/_stack/apps/tanstack-base/src/server/api/routers/health.router.ts +8 -0
  143. package/_stack/apps/tanstack-base/src/server/api/trpc.ts +61 -0
  144. package/_stack/apps/tanstack-base/src/server/better-auth/client.ts +9 -0
  145. package/_stack/apps/tanstack-base/src/server/better-auth/config.ts +68 -0
  146. package/_stack/apps/tanstack-base/src/server/better-auth/emails.tsx +25 -0
  147. package/_stack/apps/tanstack-base/src/server/better-auth/index.ts +1 -0
  148. package/_stack/apps/tanstack-base/src/server/better-auth/session.ts +9 -0
  149. package/_stack/apps/tanstack-base/src/server/db/index.ts +6 -0
  150. package/_stack/apps/tanstack-base/src/server/db/keyset.ts +63 -0
  151. package/_stack/apps/tanstack-base/src/server/db/schemas/auth.schema.ts +71 -0
  152. package/_stack/apps/tanstack-base/src/server/db/schemas/index.ts +2 -0
  153. package/_stack/apps/tanstack-base/src/server/db/seed.ts +27 -0
  154. package/_stack/apps/tanstack-base/src/server/email/adapters/resend/config.ts +7 -0
  155. package/_stack/apps/tanstack-base/src/server/email/adapters/resend/index.ts +75 -0
  156. package/_stack/apps/tanstack-base/src/server/email/core/address.ts +21 -0
  157. package/_stack/apps/tanstack-base/src/server/email/core/port.ts +89 -0
  158. package/_stack/apps/tanstack-base/src/server/email/core/render.ts +16 -0
  159. package/_stack/apps/tanstack-base/src/server/email/factory.ts +47 -0
  160. package/_stack/apps/tanstack-base/src/server/email/index.ts +36 -0
  161. package/_stack/apps/tanstack-base/src/styles.css +171 -0
  162. package/_stack/apps/tanstack-base/src/trpc/devtools.tsx +6 -0
  163. package/_stack/apps/tanstack-base/src/trpc/query-client.ts +19 -0
  164. package/_stack/apps/tanstack-base/src/trpc/react.tsx +49 -0
  165. package/_stack/apps/tanstack-base/src/trpc/server.ts +11 -0
  166. package/_stack/apps/tanstack-base/tsconfig.json +27 -0
  167. package/_stack/apps/tanstack-base/tsr.config.json +3 -0
  168. package/_stack/apps/tanstack-base/vite.config.ts +15 -0
  169. package/_stack/packages/analytics/capability.json +26 -0
  170. package/_stack/packages/cache/capability.json +21 -0
  171. package/_stack/packages/error-tracking/capability.json +21 -0
  172. package/_stack/packages/jobs/capability.json +26 -0
  173. package/_stack/packages/logger/capability.json +21 -0
  174. package/_stack/packages/mailer/capability.json +28 -0
  175. package/_stack/packages/mailer/package.json +37 -0
  176. package/_stack/packages/mailer/src/adapters/brevo/config.ts +7 -0
  177. package/_stack/packages/mailer/src/adapters/brevo/index.ts +90 -0
  178. package/_stack/packages/mailer/src/adapters/resend/config.ts +7 -0
  179. package/_stack/packages/mailer/src/adapters/resend/index.ts +75 -0
  180. package/_stack/packages/mailer/src/adapters/ses/config.ts +13 -0
  181. package/_stack/packages/mailer/src/adapters/ses/index.ts +103 -0
  182. package/_stack/packages/storage/capability.json +32 -0
  183. package/_stack/patterns/README.md +58 -0
  184. package/_stack/patterns/_baseline/README-author.md +10 -0
  185. package/_stack/patterns/_baseline/biome.jsonc +119 -0
  186. package/_stack/patterns/_baseline/env.ts +31 -0
  187. package/_stack/patterns/_baseline/tsconfig.json +27 -0
  188. package/_stack/patterns/better-auth/pattern.json +73 -0
  189. package/_stack/patterns/better-auth-next/pattern.json +76 -0
  190. package/_stack/patterns/data-table/pattern.json +43 -0
  191. package/_stack/patterns/drizzle/pattern.json +61 -0
  192. package/_stack/patterns/trpc/pattern.json +61 -0
  193. package/_stack/patterns/trpc-next/pattern.json +64 -0
  194. package/index.mjs +216 -0
  195. package/lib/build.mjs +64 -0
  196. package/lib/env.mjs +56 -0
  197. package/lib/identity.mjs +33 -0
  198. package/lib/mailer.mjs +95 -0
  199. package/lib/manifests.mjs +61 -0
  200. package/lib/scaffold.mjs +49 -0
  201. package/lib/strip.mjs +132 -0
  202. package/lib/util.mjs +82 -0
  203. package/package.json +51 -0
  204. package/templates/next/layout.no-trpc.tsx +22 -0
  205. package/templates/tanstack/__root.no-trpc.tsx +63 -0
  206. package/templates/tanstack/router.no-trpc.tsx +24 -0
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # create-stack
2
+
3
+ Interactive, **deterministic** installer for the reference stack — the non-LLM
4
+ counterpart of the `bootstrap` skill's CREATE mode. It forks a base app
5
+ (`apps/tanstack-base` or `apps/next-base`) and strips it down to your selection.
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ # from anywhere
11
+ node /path/to/stack/cli/index.mjs my-app
12
+
13
+ # from the stack repo
14
+ pnpm create-stack my-app
15
+ ```
16
+
17
+ The wizard asks for:
18
+
19
+ - **Framework** — TanStack Start or Next.js (App Router)
20
+ - **Foundations** — Drizzle, tRPC, better-auth, data-table (hard deps resolved:
21
+ tRPC/better-auth ⇒ Drizzle; better-auth ⇒ mailer)
22
+ - **Mailer provider** — Resend, Brevo, Amazon SES (or none, if no better-auth)
23
+ - **Extra capabilities** — storage, jobs, cache, … (added afterwards via the
24
+ `add-capability` skill; not baked into the base)
25
+
26
+ Then it forks, strips unselected foundations, swaps the mailer adapter, stamps
27
+ identity, generates `.env(.example)`, and optionally installs + verifies
28
+ (typecheck + Biome).
29
+
30
+ ## How it works
31
+
32
+ - **Single source of truth**: the base apps (`apps/*-base`) hold the real code;
33
+ the `patterns/*/pattern.json` manifests drive deps/scripts/env/file lists.
34
+ - **Deterministic strip**: whole-directory deletes + dep/env/script diffs from the
35
+ manifests, plus a few hardcoded code *seams* (tRPC↔auth context, root provider
36
+ wiring) handled via shipped reduced variants in `templates/`.
37
+ - If you edit a seam file in a base app (`server/api/trpc.ts`, the root
38
+ `router.tsx` / `__root.tsx` / `layout.tsx`, the schema barrel), update the
39
+ matching transform in `lib/strip.mjs` or the `templates/` variant.
40
+
41
+ ## Layout
42
+
43
+ ```
44
+ cli/
45
+ index.mjs wizard (prompts) + install/verify + report
46
+ lib/
47
+ build.mjs pure build phase (fork → strip → mailer → env → identity)
48
+ manifests.mjs load patterns + capabilities; logical→manifest mapping
49
+ scaffold.mjs fork base app, make it standalone
50
+ strip.mjs reverse-strip unselected foundations + code seams
51
+ mailer.mjs mailer provider swap
52
+ env.mjs rebuild src/env.ts blocks + generate .env files
53
+ identity.mjs title/meta + README with the # Author footer
54
+ util.mjs fs / exec / package.json helpers
55
+ templates/ reduced "no-trpc" root-wiring variants
56
+ ```
@@ -0,0 +1,10 @@
1
+ node_modules
2
+ .next
3
+ .git
4
+ .env
5
+ .env.*
6
+ !.env.example
7
+ npm-debug.log*
8
+ Dockerfile
9
+ .dockerignore
10
+ README.md
@@ -0,0 +1,34 @@
1
+ # syntax=docker/dockerfile:1
2
+ # Optimized multi-stage build for VPS deployment (Next.js standalone output).
3
+ # Build context = this app (fork it out of the monorepo, or build with this dir
4
+ # as context). Final image runs the standalone server with no dev deps.
5
+
6
+ FROM node:22-alpine AS base
7
+ RUN corepack enable
8
+ WORKDIR /app
9
+
10
+ # ---- deps: install with a cached store ----
11
+ FROM base AS deps
12
+ COPY package.json pnpm-lock.yaml* ./
13
+ RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
14
+ pnpm install --frozen-lockfile
15
+
16
+ # ---- build ----
17
+ FROM base AS build
18
+ COPY --from=deps /app/node_modules ./node_modules
19
+ COPY . .
20
+ # Env is validated at runtime, not build — skip it so the build needs no secrets.
21
+ ENV NEXT_TELEMETRY_DISABLED=1 SKIP_ENV_VALIDATION=1
22
+ RUN pnpm build
23
+
24
+ # ---- runner: minimal standalone image ----
25
+ FROM base AS runner
26
+ ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1
27
+ RUN addgroup -g 1001 nodejs && adduser -u 1001 -G nodejs -S nextjs
28
+ COPY --from=build /app/public ./public
29
+ COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
30
+ COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
31
+ USER nextjs
32
+ EXPOSE 3000
33
+ ENV PORT=3000 HOSTNAME=0.0.0.0
34
+ CMD ["node", "server.js"]
@@ -0,0 +1,32 @@
1
+ # next-base
2
+
3
+ Reference Next.js (App Router) app — fork it to start a new project. Comes wired
4
+ with the full personal foundation:
5
+
6
+ - **baseline** — `~/*` alias, typed `env.ts`, strict Biome, Tailwind v4 + shadcn
7
+ (Geist, dark mode)
8
+ - **data** — Drizzle (Postgres) + drizzle-kit, faker seed harness
9
+ - **auth** — better-auth (email/password + verification, optional Google) + a full
10
+ auth UI (sign-in/up, forgot/reset, verify) with shadcn `Form` (react-hook-form +
11
+ valibot)
12
+ - **email** — mailer (Resend) + email-kit templates (`email:dev` studio)
13
+ - **API** — tRPC with the `api.x.useQuery` hooks + RSC hydration
14
+ - **UI utilities** — theme toggle (light/dark/system), DataTable, DatePicker
15
+ - **deploy** — multi-stage `Dockerfile` (standalone output) for a VPS
16
+
17
+ ```bash
18
+ pnpm --filter @alfredmouelle/next-base dev # http://localhost:3000
19
+ ```
20
+
21
+ Add more swappable tools with **add-capability**.
22
+
23
+ # Author
24
+
25
+ Alfred MOUELLE | FullStack Developer
26
+
27
+ [![ComeUp](https://img.shields.io/static/v1?style=for-the-badge&label=&message=ComeUp&color=yellow)](https://comeup.com/@alfredmouelle)
28
+ [![GitHub](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/alfredmouelle)
29
+ [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/alfredmouelle)
30
+ [![Twitter](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/kali47_)
31
+ [![Gmail](https://img.shields.io/badge/Gmail-D14836?style=for-the-badge&logo=gmail&logoColor=white)](mailto:alfredmouelle@gmail.com)
32
+ [![Portfolio](https://img.shields.io/static/v1?style=for-the-badge&label=&message=Portfolio&color=blue)](https://alfredmouelle.com)
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "radix-luma",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "~/components",
15
+ "utils": "~/lib/utils",
16
+ "ui": "~/components/ui",
17
+ "lib": "~/lib",
18
+ "hooks": "~/hooks"
19
+ },
20
+ "iconLibrary": "lucide",
21
+ "rtl": false,
22
+ "menuColor": "default-translucent",
23
+ "menuAccent": "subtle",
24
+ "registries": {}
25
+ }
@@ -0,0 +1,16 @@
1
+ import { config } from 'dotenv'
2
+ import { defineConfig } from 'drizzle-kit'
3
+
4
+ config({ path: ['.env.local', '.env'] })
5
+
6
+ const url = process.env.DATABASE_URL
7
+ if (!url) {
8
+ throw new Error('DATABASE_URL is not set')
9
+ }
10
+
11
+ export default defineConfig({
12
+ out: './drizzle',
13
+ schema: './src/server/db/schemas/*.schema.ts',
14
+ dialect: 'postgresql',
15
+ dbCredentials: { url },
16
+ })
@@ -0,0 +1,8 @@
1
+ import type { NextConfig } from 'next'
2
+
3
+ const nextConfig: NextConfig = {
4
+ // Self-contained server bundle for small Docker images (VPS deployment).
5
+ output: 'standalone',
6
+ }
7
+
8
+ export default nextConfig
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@alfredmouelle/next-base",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "author": {
6
+ "name": "Alfred MOUELLE",
7
+ "email": "alfredmouelle@gmail.com",
8
+ "url": "https://alfredmouelle.com"
9
+ },
10
+ "scripts": {
11
+ "dev": "next dev",
12
+ "build": "next build",
13
+ "start": "next start",
14
+ "typecheck": "tsc --noEmit",
15
+ "check": "biome check .",
16
+ "check:unsafe": "biome check --write --unsafe .",
17
+ "check:write": "biome check --write .",
18
+ "email:dev": "email dev --dir src/emails --port 3001",
19
+ "db:generate": "drizzle-kit generate",
20
+ "db:migrate": "drizzle-kit migrate",
21
+ "db:push": "drizzle-kit push",
22
+ "db:studio": "drizzle-kit studio",
23
+ "db:seed": "tsx src/server/db/seed.ts"
24
+ },
25
+ "dependencies": {
26
+ "@fontsource-variable/geist": "^5.2.9",
27
+ "@hookform/resolvers": "^5.2.2",
28
+ "@t3-oss/env-core": "^0.13.11",
29
+ "@tanstack/react-query": "^5.101.0",
30
+ "@tanstack/react-table": "^8.21.3",
31
+ "@trpc/client": "^11.17.0",
32
+ "@trpc/react-query": "^11.17.0",
33
+ "@trpc/server": "^11.17.0",
34
+ "better-auth": "^1.6.19",
35
+ "class-variance-authority": "^0.7.1",
36
+ "clsx": "^2.1.1",
37
+ "date-fns": "^4.4.0",
38
+ "drizzle-orm": "^0.45.2",
39
+ "lucide-react": "^0.545.0",
40
+ "next": "16.2.9",
41
+ "next-themes": "^0.4.6",
42
+ "pg": "^8.21.0",
43
+ "radix-ui": "^1.6.0",
44
+ "react": "19.2.4",
45
+ "react-dom": "19.2.4",
46
+ "react-day-picker": "^10.0.1",
47
+ "react-email": "^6.6.3",
48
+ "react-hook-form": "^7.54.2",
49
+ "resend": "^6.14.0",
50
+ "server-only": "^0.0.1",
51
+ "superjson": "^2.2.2",
52
+ "tailwind-merge": "^3.6.0",
53
+ "valibot": "^1.4.1"
54
+ },
55
+ "devDependencies": {
56
+ "@biomejs/biome": "^2.5.0",
57
+ "@faker-js/faker": "^10.5.0",
58
+ "@tailwindcss/postcss": "^4.3.1",
59
+ "@types/node": "^22.10.2",
60
+ "@types/pg": "^8.15.6",
61
+ "@types/react": "^19",
62
+ "@types/react-dom": "^19",
63
+ "drizzle-kit": "^0.31.10",
64
+ "dotenv": "^17.2.4",
65
+ "tailwindcss": "^4.3.1",
66
+ "tsx": "^4.22.4",
67
+ "tw-animate-css": "^1.4.0",
68
+ "typescript": "^5.9.3"
69
+ }
70
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ },
5
+ }
6
+
7
+ export default config
@@ -0,0 +1,4 @@
1
+ import { toNextJsHandler } from 'better-auth/next-js'
2
+ import { auth } from '~/server/better-auth'
3
+
4
+ export const { GET, POST } = toNextJsHandler(auth.handler)
@@ -0,0 +1,23 @@
1
+ import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
2
+ import type { NextRequest } from 'next/server'
3
+ import { env } from '~/env'
4
+ import { appRouter } from '~/server/api/root'
5
+ import { createTRPCContext } from '~/server/api/trpc'
6
+
7
+ const createContext = async (req: NextRequest) => createTRPCContext({ headers: req.headers })
8
+
9
+ const handler = (req: NextRequest) =>
10
+ fetchRequestHandler({
11
+ endpoint: '/api/trpc',
12
+ req,
13
+ router: appRouter,
14
+ createContext: () => createContext(req),
15
+ onError:
16
+ env.NODE_ENV === 'development'
17
+ ? ({ path, error }) => {
18
+ console.error(`❌ tRPC failed on ${path ?? '<no-path>'}: ${error.message}`)
19
+ }
20
+ : undefined,
21
+ })
22
+
23
+ export { handler as GET, handler as POST }
@@ -0,0 +1,92 @@
1
+ 'use client'
2
+
3
+ import { valibotResolver } from '@hookform/resolvers/valibot'
4
+ import { Mail } from 'lucide-react'
5
+ import { useState } from 'react'
6
+ import { useForm } from 'react-hook-form'
7
+ import { Button } from '~/components/ui/button'
8
+ import {
9
+ Form,
10
+ FormControl,
11
+ FormField,
12
+ FormItem,
13
+ FormLabel,
14
+ FormMessage,
15
+ } from '~/components/ui/form'
16
+ import { InputGroup, InputGroupAddon, InputGroupInput } from '~/components/ui/input-group'
17
+ import { Spinner } from '~/components/ui/spinner'
18
+ import { FormAlert } from '~/features/auth/form-alert'
19
+ import { type ForgotPasswordInput, ForgotPasswordSchema } from '~/features/auth/schemas'
20
+ import { authClient } from '~/server/better-auth/client'
21
+
22
+ export function ForgotPasswordForm() {
23
+ const [formError, setFormError] = useState<string | null>(null)
24
+ const [sent, setSent] = useState(false)
25
+ const form = useForm<ForgotPasswordInput>({
26
+ resolver: valibotResolver(ForgotPasswordSchema),
27
+ defaultValues: { email: '' },
28
+ })
29
+
30
+ const onSubmit = form.handleSubmit(async (values) => {
31
+ setFormError(null)
32
+ const { error } = await authClient.requestPasswordReset({
33
+ email: values.email,
34
+ redirectTo: '/auth/reset-password',
35
+ })
36
+ if (error) {
37
+ setFormError(error.message ?? 'Could not send. Try again.')
38
+ return
39
+ }
40
+ setSent(true)
41
+ })
42
+
43
+ if (sent) {
44
+ return (
45
+ <FormAlert variant="success">
46
+ If an account exists for this address, a reset email has just been sent. Check your spam
47
+ folder.
48
+ </FormAlert>
49
+ )
50
+ }
51
+
52
+ return (
53
+ <Form {...form}>
54
+ <form className="grid gap-4" onSubmit={onSubmit}>
55
+ {formError ? <FormAlert>{formError}</FormAlert> : null}
56
+
57
+ <FormField
58
+ control={form.control}
59
+ name="email"
60
+ render={({ field }) => (
61
+ <FormItem>
62
+ <FormLabel>Email</FormLabel>
63
+ <InputGroup className="h-10">
64
+ <InputGroupAddon align="inline-start">
65
+ <Mail />
66
+ </InputGroupAddon>
67
+ <FormControl>
68
+ <InputGroupInput
69
+ autoComplete="email"
70
+ placeholder="you@example.com"
71
+ type="email"
72
+ {...field}
73
+ />
74
+ </FormControl>
75
+ </InputGroup>
76
+ <FormMessage />
77
+ </FormItem>
78
+ )}
79
+ />
80
+
81
+ <Button
82
+ className="w-full cursor-pointer"
83
+ disabled={form.formState.isSubmitting}
84
+ type="submit"
85
+ >
86
+ {form.formState.isSubmitting ? <Spinner /> : null}
87
+ {form.formState.isSubmitting ? 'Sending…' : 'Send link'}
88
+ </Button>
89
+ </form>
90
+ </Form>
91
+ )
92
+ }
@@ -0,0 +1,105 @@
1
+ 'use client'
2
+
3
+ import { valibotResolver } from '@hookform/resolvers/valibot'
4
+ import { Lock } from 'lucide-react'
5
+ import { useRouter } from 'next/navigation'
6
+ import { useState } from 'react'
7
+ import { useForm } from 'react-hook-form'
8
+ import { Button } from '~/components/ui/button'
9
+ import {
10
+ Form,
11
+ FormControl,
12
+ FormField,
13
+ FormItem,
14
+ FormLabel,
15
+ FormMessage,
16
+ } from '~/components/ui/form'
17
+ import { InputGroup, InputGroupAddon, InputGroupInput } from '~/components/ui/input-group'
18
+ import { Spinner } from '~/components/ui/spinner'
19
+ import { FormAlert } from '~/features/auth/form-alert'
20
+ import { type ResetPasswordInput, ResetPasswordSchema } from '~/features/auth/schemas'
21
+ import { authClient } from '~/server/better-auth/client'
22
+
23
+ export function ResetPasswordForm({ token }: { token?: string }) {
24
+ const router = useRouter()
25
+ const [formError, setFormError] = useState<string | null>(null)
26
+ const form = useForm<ResetPasswordInput>({
27
+ resolver: valibotResolver(ResetPasswordSchema),
28
+ defaultValues: { password: '', confirmPassword: '' },
29
+ })
30
+
31
+ const onSubmit = form.handleSubmit(async (values) => {
32
+ if (!token) return
33
+ setFormError(null)
34
+ const { error } = await authClient.resetPassword({ newPassword: values.password, token })
35
+ if (error) {
36
+ setFormError(error.message ?? 'Could not reset password.')
37
+ return
38
+ }
39
+ router.push('/auth/sign-in')
40
+ })
41
+
42
+ if (!token) {
43
+ return <FormAlert>This reset link is invalid or expired. Please request a new one.</FormAlert>
44
+ }
45
+
46
+ return (
47
+ <Form {...form}>
48
+ <form className="grid gap-4" onSubmit={onSubmit}>
49
+ {formError ? <FormAlert>{formError}</FormAlert> : null}
50
+
51
+ <FormField
52
+ control={form.control}
53
+ name="password"
54
+ render={({ field }) => (
55
+ <FormItem>
56
+ <FormLabel>New password</FormLabel>
57
+ <InputGroup className="h-10">
58
+ <InputGroupAddon align="inline-start">
59
+ <Lock />
60
+ </InputGroupAddon>
61
+ <FormControl>
62
+ <InputGroupInput
63
+ autoComplete="new-password"
64
+ placeholder="At least 8 characters"
65
+ type="password"
66
+ {...field}
67
+ />
68
+ </FormControl>
69
+ </InputGroup>
70
+ <FormMessage />
71
+ </FormItem>
72
+ )}
73
+ />
74
+
75
+ <FormField
76
+ control={form.control}
77
+ name="confirmPassword"
78
+ render={({ field }) => (
79
+ <FormItem>
80
+ <FormLabel>Confirm password</FormLabel>
81
+ <InputGroup className="h-10">
82
+ <InputGroupAddon align="inline-start">
83
+ <Lock />
84
+ </InputGroupAddon>
85
+ <FormControl>
86
+ <InputGroupInput autoComplete="new-password" type="password" {...field} />
87
+ </FormControl>
88
+ </InputGroup>
89
+ <FormMessage />
90
+ </FormItem>
91
+ )}
92
+ />
93
+
94
+ <Button
95
+ className="w-full cursor-pointer"
96
+ disabled={form.formState.isSubmitting}
97
+ type="submit"
98
+ >
99
+ {form.formState.isSubmitting ? <Spinner /> : null}
100
+ {form.formState.isSubmitting ? 'Saving…' : 'Reset password'}
101
+ </Button>
102
+ </form>
103
+ </Form>
104
+ )
105
+ }
@@ -0,0 +1,126 @@
1
+ 'use client'
2
+
3
+ import { valibotResolver } from '@hookform/resolvers/valibot'
4
+ import { Lock, Mail } from 'lucide-react'
5
+ import Link from 'next/link'
6
+ import { useRouter } from 'next/navigation'
7
+ import { useState } from 'react'
8
+ import { useForm } from 'react-hook-form'
9
+ import { Button } from '~/components/ui/button'
10
+ import {
11
+ Form,
12
+ FormControl,
13
+ FormField,
14
+ FormItem,
15
+ FormLabel,
16
+ FormMessage,
17
+ } from '~/components/ui/form'
18
+ import { InputGroup, InputGroupAddon, InputGroupInput } from '~/components/ui/input-group'
19
+ import { Spinner } from '~/components/ui/spinner'
20
+ import { FormAlert } from '~/features/auth/form-alert'
21
+ import { AuthDivider, GoogleButton } from '~/features/auth/google-button'
22
+ import { type SignInInput, SignInSchema } from '~/features/auth/schemas'
23
+ import { authClient } from '~/server/better-auth/client'
24
+
25
+ export function SignInForm() {
26
+ const router = useRouter()
27
+ const [formError, setFormError] = useState<string | null>(null)
28
+ const form = useForm<SignInInput>({
29
+ resolver: valibotResolver(SignInSchema),
30
+ defaultValues: { email: '', password: '' },
31
+ })
32
+
33
+ const onSubmit = form.handleSubmit(async (values) => {
34
+ setFormError(null)
35
+ const { error } = await authClient.signIn.email({
36
+ email: values.email,
37
+ password: values.password,
38
+ })
39
+
40
+ if (!error) {
41
+ router.push('/')
42
+ router.refresh()
43
+ return
44
+ }
45
+
46
+ if (error.code === 'EMAIL_NOT_VERIFIED') {
47
+ router.push(`/auth/verify-email?email=${encodeURIComponent(values.email)}`)
48
+ return
49
+ }
50
+
51
+ setFormError(
52
+ error.code === 'INVALID_EMAIL_OR_PASSWORD'
53
+ ? 'Incorrect email or password.'
54
+ : (error.message ?? 'Could not sign in. Try again.'),
55
+ )
56
+ })
57
+
58
+ return (
59
+ <Form {...form}>
60
+ <form className="grid gap-4" onSubmit={onSubmit}>
61
+ <GoogleButton label="Continue with Google" />
62
+ <AuthDivider />
63
+
64
+ {formError ? <FormAlert>{formError}</FormAlert> : null}
65
+
66
+ <FormField
67
+ control={form.control}
68
+ name="email"
69
+ render={({ field }) => (
70
+ <FormItem>
71
+ <FormLabel>Email</FormLabel>
72
+ <InputGroup className="h-10">
73
+ <InputGroupAddon align="inline-start">
74
+ <Mail />
75
+ </InputGroupAddon>
76
+ <FormControl>
77
+ <InputGroupInput
78
+ autoComplete="email"
79
+ placeholder="you@example.com"
80
+ type="email"
81
+ {...field}
82
+ />
83
+ </FormControl>
84
+ </InputGroup>
85
+ <FormMessage />
86
+ </FormItem>
87
+ )}
88
+ />
89
+
90
+ <FormField
91
+ control={form.control}
92
+ name="password"
93
+ render={({ field }) => (
94
+ <FormItem>
95
+ <FormLabel>Password</FormLabel>
96
+ <InputGroup className="h-10">
97
+ <InputGroupAddon align="inline-start">
98
+ <Lock />
99
+ </InputGroupAddon>
100
+ <FormControl>
101
+ <InputGroupInput autoComplete="current-password" type="password" {...field} />
102
+ </FormControl>
103
+ </InputGroup>
104
+ <Link
105
+ className="justify-self-end text-muted-foreground text-xs hover:underline"
106
+ href="/auth/forgot-password"
107
+ >
108
+ Forgot password?
109
+ </Link>
110
+ <FormMessage />
111
+ </FormItem>
112
+ )}
113
+ />
114
+
115
+ <Button
116
+ className="w-full cursor-pointer"
117
+ disabled={form.formState.isSubmitting}
118
+ type="submit"
119
+ >
120
+ {form.formState.isSubmitting ? <Spinner /> : null}
121
+ {form.formState.isSubmitting ? 'Signing in…' : 'Sign in'}
122
+ </Button>
123
+ </form>
124
+ </Form>
125
+ )
126
+ }