@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
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@alfredmouelle/mailer",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts"
8
+ },
9
+ "scripts": {
10
+ "build": "tsdown",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "typecheck": "tsc --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "@aws-sdk/client-sesv2": "^3.1073.0",
17
+ "@getbrevo/brevo": "^5.0.4",
18
+ "resend": "^6.14.0",
19
+ "valibot": "^1.4.1"
20
+ },
21
+ "peerDependencies": {
22
+ "react": ">=18",
23
+ "react-dom": ">=18",
24
+ "react-email": "^6.6.3"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.10.2",
28
+ "@types/react": "^19.0.1",
29
+ "@types/react-dom": "^19.0.2",
30
+ "react": "^19.0.0",
31
+ "react-dom": "^19.0.0",
32
+ "react-email": "^6.6.3",
33
+ "tsdown": "^0.22.3",
34
+ "typescript": "^5.9.3",
35
+ "vitest": "^4.1.9"
36
+ }
37
+ }
@@ -0,0 +1,7 @@
1
+ import * as v from 'valibot'
2
+
3
+ export const BrevoConfigSchema = v.object({
4
+ apiKey: v.pipe(v.string(), v.minLength(1, 'BREVO_API_KEY is required')),
5
+ })
6
+
7
+ export type BrevoConfig = v.InferOutput<typeof BrevoConfigSchema>
@@ -0,0 +1,90 @@
1
+ import { BrevoClient } from '@getbrevo/brevo'
2
+ import * as v from 'valibot'
3
+ import type { MailAddress } from '../../core/port.js'
4
+ import { type MailerAdapter, MailerError, type RenderedMessage } from '../../core/port.js'
5
+ import { BrevoConfigSchema } from './config.js'
6
+
7
+ interface BrevoContact {
8
+ email: string
9
+ name?: string
10
+ }
11
+
12
+ interface BrevoSendRequest {
13
+ sender: BrevoContact
14
+ to: BrevoContact[]
15
+ subject: string
16
+ htmlContent: string
17
+ textContent: string
18
+ replyTo?: BrevoContact
19
+ cc?: BrevoContact[]
20
+ bcc?: BrevoContact[]
21
+ headers?: Record<string, unknown>
22
+ tags?: string[]
23
+ attachment?: { name: string; content: string }[]
24
+ }
25
+
26
+ interface BrevoSendResponse {
27
+ messageId?: string
28
+ messageIds?: string[]
29
+ }
30
+
31
+ /** Minimal structural view of the Brevo client we depend on (eases testing). */
32
+ export interface BrevoClientLike {
33
+ transactionalEmails: {
34
+ sendTransacEmail(request: BrevoSendRequest): Promise<BrevoSendResponse>
35
+ }
36
+ }
37
+
38
+ export interface BrevoAdapterOptions {
39
+ apiKey: string
40
+ /** Inject a custom/mock client. Defaults to a real `BrevoClient`. */
41
+ client?: BrevoClientLike
42
+ }
43
+
44
+ function toContact(address: MailAddress): BrevoContact {
45
+ return { email: address.email, name: address.name }
46
+ }
47
+
48
+ function toBase64(content: Uint8Array | string): string {
49
+ return typeof content === 'string' ? content : Buffer.from(content).toString('base64')
50
+ }
51
+
52
+ export function brevoAdapter(options: BrevoAdapterOptions): MailerAdapter {
53
+ // Validate config early so a missing key fails at construction, not at send().
54
+ const config = v.parse(BrevoConfigSchema, { apiKey: options.apiKey })
55
+ const client: BrevoClientLike =
56
+ options.client ?? (new BrevoClient({ apiKey: config.apiKey }) as unknown as BrevoClientLike)
57
+
58
+ return {
59
+ name: 'brevo',
60
+ async send(message: RenderedMessage) {
61
+ try {
62
+ const response = await client.transactionalEmails.sendTransacEmail({
63
+ sender: toContact(message.from),
64
+ to: message.to.map(toContact),
65
+ subject: message.subject,
66
+ htmlContent: message.html,
67
+ textContent: message.text,
68
+ replyTo: message.replyTo ? toContact(message.replyTo) : undefined,
69
+ cc: message.cc?.map(toContact),
70
+ bcc: message.bcc?.map(toContact),
71
+ headers: message.headers,
72
+ tags: message.tags
73
+ ? Object.entries(message.tags).map(([k, val]) => `${k}:${val}`)
74
+ : undefined,
75
+ attachment: message.attachments?.map((a) => ({
76
+ name: a.filename,
77
+ content: toBase64(a.content),
78
+ })),
79
+ })
80
+
81
+ const id = response.messageId ?? response.messageIds?.[0]
82
+ if (!id) throw new MailerError('Brevo returned no messageId', { adapter: 'brevo' })
83
+ return { id }
84
+ } catch (cause) {
85
+ if (cause instanceof MailerError) throw cause
86
+ throw new MailerError('Brevo request failed', { adapter: 'brevo', cause })
87
+ }
88
+ },
89
+ }
90
+ }
@@ -0,0 +1,7 @@
1
+ import * as v from 'valibot'
2
+
3
+ export const ResendConfigSchema = v.object({
4
+ apiKey: v.pipe(v.string(), v.minLength(1, 'RESEND_API_KEY is required')),
5
+ })
6
+
7
+ export type ResendConfig = v.InferOutput<typeof ResendConfigSchema>
@@ -0,0 +1,75 @@
1
+ import { Resend } from 'resend'
2
+ import * as v from 'valibot'
3
+ import { formatAddress } from '../../core/address.js'
4
+ import { type MailerAdapter, MailerError, type RenderedMessage } from '../../core/port.js'
5
+ import { ResendConfigSchema } from './config.js'
6
+
7
+ /** Minimal structural view of the Resend client we depend on (eases testing). */
8
+ export interface ResendClient {
9
+ emails: {
10
+ send(payload: ResendSendPayload): Promise<{
11
+ data: { id: string } | null
12
+ error: { message: string; name?: string } | null
13
+ }>
14
+ }
15
+ }
16
+
17
+ interface ResendSendPayload {
18
+ from: string
19
+ to: string[]
20
+ subject: string
21
+ html: string
22
+ text: string
23
+ replyTo?: string
24
+ cc?: string[]
25
+ bcc?: string[]
26
+ headers?: Record<string, string>
27
+ tags?: { name: string; value: string }[]
28
+ attachments?: { filename: string; content: Buffer | string; contentType?: string }[]
29
+ }
30
+
31
+ export interface ResendAdapterOptions {
32
+ apiKey: string
33
+ /** Inject a custom/mock client. Defaults to a real `Resend` instance. */
34
+ client?: ResendClient
35
+ }
36
+
37
+ function toAttachmentContent(content: Uint8Array | string): Buffer | string {
38
+ return typeof content === 'string' ? content : Buffer.from(content)
39
+ }
40
+
41
+ export function resendAdapter(options: ResendAdapterOptions): MailerAdapter {
42
+ // Validate config early so a missing key fails at construction, not at send().
43
+ const config = v.parse(ResendConfigSchema, { apiKey: options.apiKey })
44
+ const client: ResendClient =
45
+ options.client ?? (new Resend(config.apiKey) as unknown as ResendClient)
46
+
47
+ return {
48
+ name: 'resend',
49
+ async send(message: RenderedMessage) {
50
+ const { data, error } = await client.emails.send({
51
+ from: formatAddress(message.from),
52
+ to: message.to.map(formatAddress),
53
+ subject: message.subject,
54
+ html: message.html,
55
+ text: message.text,
56
+ replyTo: message.replyTo ? formatAddress(message.replyTo) : undefined,
57
+ cc: message.cc?.map(formatAddress),
58
+ bcc: message.bcc?.map(formatAddress),
59
+ headers: message.headers,
60
+ tags: message.tags
61
+ ? Object.entries(message.tags).map(([name, value]) => ({ name, value }))
62
+ : undefined,
63
+ attachments: message.attachments?.map((a) => ({
64
+ filename: a.filename,
65
+ content: toAttachmentContent(a.content),
66
+ contentType: a.contentType,
67
+ })),
68
+ })
69
+
70
+ if (error) throw new MailerError(error.message, { adapter: 'resend', cause: error })
71
+ if (!data) throw new MailerError('Resend returned no data', { adapter: 'resend' })
72
+ return { id: data.id }
73
+ },
74
+ }
75
+ }
@@ -0,0 +1,13 @@
1
+ import * as v from 'valibot'
2
+
3
+ export const SesConfigSchema = v.object({
4
+ /** AWS region. Falls back to the SDK's resolution (e.g. `AWS_REGION`). */
5
+ region: v.optional(v.string()),
6
+ /** Static credentials. Omit to let the SDK resolve them (env, profile, IAM). */
7
+ accessKeyId: v.optional(v.string()),
8
+ secretAccessKey: v.optional(v.string()),
9
+ /** SES configuration set to attach (event publishing, dedicated IPs, …). */
10
+ configurationSetName: v.optional(v.string()),
11
+ })
12
+
13
+ export type SesConfig = v.InferOutput<typeof SesConfigSchema>
@@ -0,0 +1,103 @@
1
+ // biome-ignore-all lint/style/useNamingConvention: SESv2's API uses PascalCase keys
2
+ import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2'
3
+ import * as v from 'valibot'
4
+ import { formatAddress } from '../../core/address.js'
5
+ import { type MailerAdapter, MailerError, type RenderedMessage } from '../../core/port.js'
6
+ import { SesConfigSchema } from './config.js'
7
+
8
+ /** Minimal structural view of the SESv2 client we depend on (eases testing). */
9
+ export interface SesClientLike {
10
+ send(command: { input: SesSendEmailInput }): Promise<{ MessageId?: string }>
11
+ }
12
+
13
+ interface SesSendEmailInput {
14
+ FromEmailAddress: string
15
+ Destination: { ToAddresses: string[]; CcAddresses?: string[]; BccAddresses?: string[] }
16
+ Content: {
17
+ Simple: {
18
+ Subject: { Data: string }
19
+ Body: { Html: { Data: string }; Text: { Data: string } }
20
+ Headers?: { Name: string; Value: string }[]
21
+ }
22
+ }
23
+ ReplyToAddresses?: string[]
24
+ EmailTags?: { Name: string; Value: string }[]
25
+ ConfigurationSetName?: string
26
+ }
27
+
28
+ export interface SesAdapterOptions {
29
+ /** AWS region. Falls back to the SDK's resolution (e.g. `AWS_REGION`). */
30
+ region?: string
31
+ accessKeyId?: string
32
+ secretAccessKey?: string
33
+ configurationSetName?: string
34
+ /** Inject a custom/mock client. Defaults to a real `SESv2Client`. */
35
+ client?: SesClientLike
36
+ }
37
+
38
+ export function sesAdapter(options: SesAdapterOptions = {}): MailerAdapter {
39
+ const config = v.parse(SesConfigSchema, {
40
+ region: options.region,
41
+ accessKeyId: options.accessKeyId,
42
+ secretAccessKey: options.secretAccessKey,
43
+ configurationSetName: options.configurationSetName,
44
+ })
45
+
46
+ const client: SesClientLike =
47
+ options.client ??
48
+ (new SESv2Client({
49
+ region: config.region,
50
+ credentials:
51
+ config.accessKeyId && config.secretAccessKey
52
+ ? { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey }
53
+ : undefined,
54
+ }) as unknown as SesClientLike)
55
+
56
+ return {
57
+ name: 'ses',
58
+ async send(message: RenderedMessage) {
59
+ // SES's Simple content can't carry attachments — that needs a raw MIME
60
+ // message, which this adapter intentionally doesn't build. Fail loudly.
61
+ if (message.attachments?.length) {
62
+ throw new MailerError('SES adapter does not support attachments (requires raw MIME)', {
63
+ adapter: 'ses',
64
+ })
65
+ }
66
+
67
+ const input: SesSendEmailInput = {
68
+ FromEmailAddress: formatAddress(message.from),
69
+ Destination: {
70
+ ToAddresses: message.to.map(formatAddress),
71
+ CcAddresses: message.cc?.map(formatAddress),
72
+ BccAddresses: message.bcc?.map(formatAddress),
73
+ },
74
+ Content: {
75
+ Simple: {
76
+ Subject: { Data: message.subject },
77
+ Body: { Html: { Data: message.html }, Text: { Data: message.text } },
78
+ Headers: message.headers
79
+ ? Object.entries(message.headers).map(([Name, Value]) => ({ Name, Value }))
80
+ : undefined,
81
+ },
82
+ },
83
+ ReplyToAddresses: message.replyTo ? [formatAddress(message.replyTo)] : undefined,
84
+ EmailTags: message.tags
85
+ ? Object.entries(message.tags).map(([Name, Value]) => ({ Name, Value }))
86
+ : undefined,
87
+ ConfigurationSetName: config.configurationSetName,
88
+ }
89
+
90
+ try {
91
+ const result = await client.send(
92
+ new SendEmailCommand(input) as { input: SesSendEmailInput },
93
+ )
94
+ if (!result.MessageId)
95
+ throw new MailerError('SES returned no MessageId', { adapter: 'ses' })
96
+ return { id: result.MessageId }
97
+ } catch (cause) {
98
+ if (cause instanceof MailerError) throw cause
99
+ throw new MailerError('SES request failed', { adapter: 'ses', cause })
100
+ }
101
+ },
102
+ }
103
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "../../capability.schema.json",
3
+ "name": "storage",
4
+ "description": "Object storage behind a swappable port: put/get/delete/exists plus signed URLs. Adapters for S3, Cloudflare R2, Google Cloud Storage and the local filesystem.",
5
+ "port": "src/core/port.ts",
6
+ "factory": "src/index.ts",
7
+ "defaultAdapter": "s3",
8
+ "adapters": {
9
+ "s3": {
10
+ "deps": ["@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner"],
11
+ "env": ["S3_BUCKET", "S3_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
12
+ "files": ["src/adapters/s3"]
13
+ },
14
+ "r2": {
15
+ "deps": ["@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner"],
16
+ "env": ["R2_BUCKET", "R2_ACCOUNT_ID", "R2_ACCESS_KEY_ID", "R2_SECRET_ACCESS_KEY"],
17
+ "files": ["src/adapters/r2", "src/adapters/s3"]
18
+ },
19
+ "gcs": {
20
+ "deps": ["@google-cloud/storage"],
21
+ "env": ["GCS_BUCKET", "GOOGLE_CLOUD_PROJECT"],
22
+ "files": ["src/adapters/gcs"]
23
+ },
24
+ "local": {
25
+ "deps": [],
26
+ "env": ["STORAGE_LOCAL_DIR"],
27
+ "files": ["src/adapters/local"]
28
+ }
29
+ },
30
+ "sharedDeps": ["valibot"],
31
+ "sharedFiles": ["src/core", "src/index.ts"]
32
+ }
@@ -0,0 +1,58 @@
1
+ # Patterns
2
+
3
+ Foundational, framework-coupled **patterns** the `bootstrap` skill vendors into a
4
+ freshly scaffolded app — the counterpart to `packages/` (swappable capabilities).
5
+
6
+ A *capability* is a provider behind a port (mailer, storage, …), swappable by
7
+ changing one line. A *pattern* is a foundation you don't swap but always set up
8
+ the same way: tRPC wiring, the better-auth instance, the Drizzle client. They are
9
+ **framework-coupled** (currently `tanstack-start`, mirrored from the reference
10
+ base apps) and depend on each other.
11
+
12
+ **The code lives in the base apps, not here.** `patterns/` is a pure *manifest
13
+ layer*: each `pattern.json` describes a foundation (how to detect it, its deps,
14
+ env, framework, dependencies) and lists the files that make it up — by pointing
15
+ **into the base apps** (`apps/tanstack-base`, `apps/next-base`), the single source
16
+ of truth. No code is duplicated.
17
+
18
+ Each pattern is `<name>/pattern.json` (see `../pattern.schema.json`).
19
+ `_baseline/` is special: real always-applied config files (Biome, tsconfig, env
20
+ skeleton, the `# Author` README footer) that a standalone fork needs but the base
21
+ apps don't carry on their own (they inherit the monorepo's Biome).
22
+
23
+ ## How the skills use these
24
+
25
+ The manifests drive two flows:
26
+
27
+ - **bootstrap — create mode** (empty folder): fork a base app, then *strip* every
28
+ foundation/capability the user didn't pick, using each manifest's `files`/`deps`/
29
+ `env` to know its exact footprint.
30
+ - **bootstrap — existing project / add-capability**: match each manifest's `detect`
31
+ against the project → the opt-in set, then *vendor* the listed files (copied from
32
+ the base apps) + deps + env, wire `integratesWith` when both sides are opt-in, and
33
+ pull required `capabilities`. A pattern not referenced is never pulled.
34
+
35
+ ## Available patterns
36
+
37
+ - **drizzle** — Drizzle ORM + drizzle-kit (Postgres). Client, schema barrel,
38
+ cursor pagination, seed harness.
39
+ - **better-auth** — better-auth v1 with the Drizzle adapter. Email+password,
40
+ verification, optional Google OAuth, rate limiting, auth tables, client +
41
+ session helpers, route guard. `dependsOn` drizzle; needs the mailer + email-kit
42
+ capabilities.
43
+ - **trpc** — tRPC v11 + TanStack React Query. Context, procedure tiers, error
44
+ formatter, client + SSR caller, fetch handler. `dependsOn` drizzle,
45
+ `integratesWith` better-auth.
46
+ - **data-table** — headless tables with TanStack Table (table + skeleton
47
+ primitives, DataTable, InfiniteDataTable, SortableHeader). `framework: agnostic`
48
+ — works in both Next and TanStack Start.
49
+
50
+ Next.js variants (App Router) of the framework-coupled patterns:
51
+
52
+ - **better-auth-next** — better-auth with `next/headers` session, `toNextJsHandler`
53
+ catch-all, server-component guards (`requireAuth`).
54
+ - **trpc-next** — tRPC with the classic `api.x.useQuery` hooks (createTRPCReact) +
55
+ RSC hydration. `integratesWith` better-auth-next.
56
+
57
+ bootstrap picks the variant matching the project's framework: `trpc`/`better-auth`
58
+ for TanStack Start, `trpc-next`/`better-auth-next` for Next.
@@ -0,0 +1,10 @@
1
+ # Author
2
+
3
+ Alfred MOUELLE | FullStack Developer
4
+
5
+ [![ComeUp](https://img.shields.io/static/v1?style=for-the-badge&label=&message=ComeUp&color=yellow)](https://comeup.com/@alfredmouelle)
6
+ [![GitHub](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/alfredmouelle)
7
+ [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/alfredmouelle)
8
+ [![Twitter](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/kali47_)
9
+ [![Gmail](https://img.shields.io/badge/Gmail-D14836?style=for-the-badge&logo=gmail&logoColor=white)](mailto:alfredmouelle@gmail.com)
10
+ [![Portfolio](https://img.shields.io/static/v1?style=for-the-badge&label=&message=Portfolio&color=blue)](https://alfredmouelle.com)
@@ -0,0 +1,119 @@
1
+ {
2
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3
+ "root": true,
4
+ "files": {
5
+ "includes": [
6
+ "**",
7
+ "!public",
8
+ "!**/dist",
9
+ "!**/.next",
10
+ "!**/.output",
11
+ "!**/.nitro",
12
+ "!**/routeTree.gen.ts",
13
+ "!**/components/ui"
14
+ ]
15
+ },
16
+ "vcs": {
17
+ "enabled": true,
18
+ "useIgnoreFile": true,
19
+ "clientKind": "git"
20
+ },
21
+ "assist": {
22
+ "enabled": true,
23
+ "actions": {
24
+ "source": {
25
+ "organizeImports": "on",
26
+ "useSortedAttributes": "on"
27
+ }
28
+ }
29
+ },
30
+ "formatter": {
31
+ "enabled": true,
32
+ "indentWidth": 2,
33
+ "indentStyle": "space",
34
+ "lineWidth": 100
35
+ },
36
+ "linter": {
37
+ "enabled": true,
38
+ "domains": {
39
+ "react": "recommended",
40
+ "test": "recommended"
41
+ },
42
+ "rules": {
43
+ "suspicious": {
44
+ "noArrayIndexKey": "off",
45
+ "noConsole": {
46
+ "level": "warn",
47
+ "options": { "allow": ["warn", "error"] }
48
+ }
49
+ },
50
+ "complexity": {
51
+ "noExcessiveCognitiveComplexity": "warn"
52
+ },
53
+ "correctness": {
54
+ "noUndeclaredDependencies": "error"
55
+ },
56
+ "style": {
57
+ "noInferrableTypes": "error",
58
+ "useNamingConvention": {
59
+ "level": "warn",
60
+ "options": {
61
+ "strictCase": false,
62
+ "conventions": [{ "selector": { "kind": "objectLiteralProperty" }, "match": ".*" }]
63
+ }
64
+ },
65
+ "useConsistentArrayType": {
66
+ "level": "error",
67
+ "options": { "syntax": "shorthand" }
68
+ }
69
+ },
70
+ "a11y": {
71
+ "noStaticElementInteractions": "off",
72
+ "useKeyWithClickEvents": "off"
73
+ },
74
+ "nursery": {
75
+ "useSortedClasses": {
76
+ "level": "warn",
77
+ "fix": "safe",
78
+ "options": {
79
+ "functions": ["clsx", "cva", "cn"]
80
+ }
81
+ }
82
+ }
83
+ }
84
+ },
85
+ "html": {
86
+ "formatter": {
87
+ "enabled": true
88
+ }
89
+ },
90
+ "javascript": {
91
+ "assist": {
92
+ "enabled": true
93
+ },
94
+ "formatter": {
95
+ "enabled": true,
96
+ "quoteStyle": "single",
97
+ "semicolons": "asNeeded",
98
+ "trailingCommas": "all"
99
+ },
100
+ "linter": {
101
+ "enabled": true
102
+ }
103
+ },
104
+ "css": {
105
+ "assist": {
106
+ "enabled": true
107
+ },
108
+ "formatter": {
109
+ "enabled": true
110
+ },
111
+ "linter": {
112
+ "enabled": true
113
+ },
114
+ "parser": {
115
+ "cssModules": true,
116
+ "tailwindDirectives": true
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,31 @@
1
+ import { createEnv } from '@t3-oss/env-core';
2
+ import * as v from 'valibot';
3
+
4
+ /** Makes a var required only in production (optional in dev/test). */
5
+ export const requiredInProduction = <T extends v.GenericSchema>(schema: T) =>
6
+ process.env.NODE_ENV === 'production' ? schema : v.optional(schema);
7
+
8
+ /**
9
+ * Typed environment. Start minimal — patterns (drizzle, better-auth, …) and
10
+ * capabilities (add-capability) extend the `server` block and `runtimeEnv` with
11
+ * the keys they need.
12
+ */
13
+ export const env = createEnv({
14
+ shared: {
15
+ NODE_ENV: v.optional(
16
+ v.picklist(['development', 'test', 'production']),
17
+ 'development',
18
+ ),
19
+ },
20
+
21
+ server: {
22
+ // Extended by patterns/capabilities (DATABASE_URL, BETTER_AUTH_SECRET, …).
23
+ },
24
+
25
+ runtimeEnv: {
26
+ NODE_ENV: process.env.NODE_ENV,
27
+ },
28
+
29
+ skipValidation: !!process.env.SKIP_ENV_VALIDATION,
30
+ emptyStringAsUndefined: true,
31
+ });
@@ -0,0 +1,27 @@
1
+ {
2
+ "include": ["**/*.ts", "**/*.tsx"],
3
+ "compilerOptions": {
4
+ "target": "ES2022",
5
+ "jsx": "react-jsx",
6
+ "module": "ESNext",
7
+ "paths": {
8
+ "~/*": ["./src/*"]
9
+ },
10
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
11
+ "types": ["vite/client"],
12
+
13
+ /* Bundler mode */
14
+ "moduleResolution": "bundler",
15
+ "allowImportingTsExtensions": true,
16
+ "verbatimModuleSyntax": true,
17
+ "noEmit": true,
18
+
19
+ /* Linting */
20
+ "skipLibCheck": true,
21
+ "strict": true,
22
+ "noUnusedLocals": true,
23
+ "noUnusedParameters": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUncheckedSideEffectImports": true
26
+ }
27
+ }