@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,89 @@
1
+ import type { ReactElement } from 'react'
2
+
3
+ /** A structured email address. */
4
+ export interface MailAddress {
5
+ email: string
6
+ name?: string
7
+ }
8
+
9
+ /**
10
+ * A recipient, either as a plain string (`"hi@acme.com"` or
11
+ * `"Acme <hi@acme.com>"`) or a structured {@link MailAddress}.
12
+ */
13
+ export type MailRecipient = string | MailAddress
14
+
15
+ export interface MailAttachment {
16
+ filename: string
17
+ /** Raw bytes or a base64 string. */
18
+ content: Uint8Array | string
19
+ contentType?: string
20
+ }
21
+
22
+ /**
23
+ * A message as authored by application code. We never send raw HTML strings:
24
+ * the body is always a React Email component, rendered to HTML + plain text by
25
+ * the mailer before it reaches the adapter.
26
+ */
27
+ export interface MailMessage {
28
+ to: MailRecipient | MailRecipient[]
29
+ subject: string
30
+ /** A React Email component. Rendered to HTML and plain text automatically. */
31
+ react: ReactElement
32
+ /** Overrides the default `from` configured on the mailer. */
33
+ from?: MailRecipient
34
+ replyTo?: MailRecipient
35
+ cc?: MailRecipient | MailRecipient[]
36
+ bcc?: MailRecipient | MailRecipient[]
37
+ headers?: Record<string, string>
38
+ /** Provider-agnostic key/value tags for analytics & filtering. */
39
+ tags?: Record<string, string>
40
+ attachments?: MailAttachment[]
41
+ }
42
+
43
+ export interface SentMail {
44
+ /** Provider message id. */
45
+ id: string
46
+ }
47
+
48
+ /**
49
+ * The port the application depends on. Swapping providers means swapping the
50
+ * adapter passed to {@link createMailer}; this interface never changes.
51
+ */
52
+ export interface Mailer {
53
+ send(message: MailMessage): Promise<SentMail>
54
+ }
55
+
56
+ /**
57
+ * The message shape an adapter receives: addresses are normalized and the body
58
+ * is already rendered to `html` + `text`. Adapters do no rendering.
59
+ */
60
+ export interface RenderedMessage {
61
+ to: MailAddress[]
62
+ from: MailAddress
63
+ subject: string
64
+ html: string
65
+ text: string
66
+ replyTo?: MailAddress
67
+ cc?: MailAddress[]
68
+ bcc?: MailAddress[]
69
+ headers?: Record<string, string>
70
+ tags?: Record<string, string>
71
+ attachments?: MailAttachment[]
72
+ }
73
+
74
+ /** The contract each provider implements. Kept intentionally tiny. */
75
+ export interface MailerAdapter {
76
+ readonly name: string
77
+ send(message: RenderedMessage): Promise<SentMail>
78
+ }
79
+
80
+ /** Normalized error thrown by adapters so callers never catch provider types. */
81
+ export class MailerError extends Error {
82
+ readonly adapter: string
83
+
84
+ constructor(message: string, options: { adapter: string; cause?: unknown }) {
85
+ super(message, { cause: options.cause })
86
+ this.name = 'MailerError'
87
+ this.adapter = options.adapter
88
+ }
89
+ }
@@ -0,0 +1,16 @@
1
+ import type { ReactElement } from 'react'
2
+ import { render } from 'react-email'
3
+
4
+ export interface RenderedBody {
5
+ html: string
6
+ text: string
7
+ }
8
+
9
+ /** A function that turns a React Email component into HTML + plain text. */
10
+ export type EmailRenderer = (react: ReactElement) => Promise<RenderedBody>
11
+
12
+ /** Default renderer backed by `@react-email/render`. */
13
+ export const renderEmail: EmailRenderer = async (react) => {
14
+ const [html, text] = await Promise.all([render(react), render(react, { plainText: true })])
15
+ return { html, text }
16
+ }
@@ -0,0 +1,47 @@
1
+ import { normalizeAddress, normalizeRecipients } from './core/address'
2
+ import type { Mailer, MailerAdapter, MailRecipient, RenderedMessage } from './core/port'
3
+ import { type EmailRenderer, renderEmail } from './core/render'
4
+
5
+ export interface CreateMailerOptions {
6
+ /** The provider implementation (Resend, Brevo, …). */
7
+ adapter: MailerAdapter
8
+ /** Default sender used when a message omits `from`. */
9
+ from: MailRecipient
10
+ /** Override the React Email renderer (mostly for tests). */
11
+ render?: EmailRenderer
12
+ }
13
+
14
+ /**
15
+ * Build a {@link Mailer}. This is the composition root: pick the adapter here,
16
+ * the rest of the app depends only on the `Mailer` port.
17
+ *
18
+ * The mailer renders the React body to HTML + plain text, normalizes every
19
+ * address, applies the default sender, then hands a {@link RenderedMessage} to
20
+ * the adapter.
21
+ */
22
+ export function createMailer(options: CreateMailerOptions): Mailer {
23
+ const defaultFrom = normalizeAddress(options.from)
24
+ const render = options.render ?? renderEmail
25
+
26
+ return {
27
+ async send(message) {
28
+ const { html, text } = await render(message.react)
29
+
30
+ const rendered: RenderedMessage = {
31
+ to: normalizeRecipients(message.to),
32
+ from: message.from ? normalizeAddress(message.from) : defaultFrom,
33
+ subject: message.subject,
34
+ html,
35
+ text,
36
+ replyTo: message.replyTo ? normalizeAddress(message.replyTo) : undefined,
37
+ cc: message.cc ? normalizeRecipients(message.cc) : undefined,
38
+ bcc: message.bcc ? normalizeRecipients(message.bcc) : undefined,
39
+ headers: message.headers,
40
+ tags: message.tags,
41
+ attachments: message.attachments,
42
+ }
43
+
44
+ return options.adapter.send(rendered)
45
+ },
46
+ }
47
+ }
@@ -0,0 +1,36 @@
1
+ import type { ReactElement } from 'react'
2
+ import { env } from '~/env'
3
+ import { resendAdapter } from './adapters/resend/index'
4
+ import type { MailAddress, Mailer } from './core/port'
5
+ import { createMailer } from './factory'
6
+
7
+ export type EmailRecipient = MailAddress
8
+
9
+ function required(value: string | undefined, name: string): string {
10
+ if (!value) throw new Error(`${name} is required to send email`)
11
+ return value
12
+ }
13
+
14
+ // Lazy so the app boots without RESEND_API_KEY; the adapter is built on first send.
15
+ let mailer: Mailer | null = null
16
+ function getMailer(): Mailer {
17
+ if (!mailer) {
18
+ mailer = createMailer({
19
+ from: env.EMAIL_FROM,
20
+ adapter: resendAdapter({ apiKey: required(env.RESEND_API_KEY, 'RESEND_API_KEY') }),
21
+ })
22
+ }
23
+ return mailer
24
+ }
25
+
26
+ export async function sendEmail(params: {
27
+ to: EmailRecipient
28
+ subject: string
29
+ template: ReactElement
30
+ }) {
31
+ return getMailer().send({
32
+ to: params.to,
33
+ subject: params.subject,
34
+ react: params.template,
35
+ })
36
+ }
@@ -0,0 +1,19 @@
1
+ import { defaultShouldDehydrateQuery, QueryClient } from '@tanstack/react-query'
2
+ import SuperJSON from 'superjson'
3
+
4
+ export const createQueryClient = () =>
5
+ new QueryClient({
6
+ defaultOptions: {
7
+ queries: {
8
+ staleTime: 30 * 1000,
9
+ },
10
+ dehydrate: {
11
+ serializeData: SuperJSON.serialize,
12
+ shouldDehydrateQuery: (query) =>
13
+ defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
14
+ },
15
+ hydrate: {
16
+ deserializeData: SuperJSON.deserialize,
17
+ },
18
+ },
19
+ })
@@ -0,0 +1,62 @@
1
+ 'use client'
2
+
3
+ import { type QueryClient, QueryClientProvider } from '@tanstack/react-query'
4
+ import { httpBatchStreamLink, loggerLink } from '@trpc/client'
5
+ import { createTRPCReact } from '@trpc/react-query'
6
+ import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'
7
+ import type { ReactNode } from 'react'
8
+ import { useState } from 'react'
9
+ import SuperJSON from 'superjson'
10
+ import type { AppRouter } from '~/server/api/root'
11
+ import { createQueryClient } from './query-client'
12
+
13
+ let clientQueryClientSingleton: QueryClient | undefined
14
+ const getQueryClient = () => {
15
+ if (typeof window === 'undefined') {
16
+ // Server: always make a new query client.
17
+ return createQueryClient()
18
+ }
19
+ // Browser: reuse the same query client across renders.
20
+ clientQueryClientSingleton ??= createQueryClient()
21
+ return clientQueryClientSingleton
22
+ }
23
+
24
+ export const api = createTRPCReact<AppRouter>()
25
+
26
+ export type RouterInputs = inferRouterInputs<AppRouter>
27
+
28
+ export type RouterOutputs = inferRouterOutputs<AppRouter>
29
+
30
+ function getBaseUrl() {
31
+ if (typeof window !== 'undefined') return window.location.origin
32
+ if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`
33
+ return `http://localhost:${process.env.PORT ?? 3000}`
34
+ }
35
+
36
+ export function TRPCReactProvider(props: { children: ReactNode }) {
37
+ const queryClient = getQueryClient()
38
+
39
+ const [trpcClient] = useState(() =>
40
+ api.createClient({
41
+ links: [
42
+ loggerLink({
43
+ enabled: (op) =>
44
+ process.env.NODE_ENV === 'development' ||
45
+ (op.direction === 'down' && op.result instanceof Error),
46
+ }),
47
+ httpBatchStreamLink({
48
+ transformer: SuperJSON,
49
+ url: `${getBaseUrl()}/api/trpc`,
50
+ }),
51
+ ],
52
+ }),
53
+ )
54
+
55
+ return (
56
+ <QueryClientProvider client={queryClient}>
57
+ <api.Provider client={trpcClient} queryClient={queryClient}>
58
+ {props.children}
59
+ </api.Provider>
60
+ </QueryClientProvider>
61
+ )
62
+ }
@@ -0,0 +1,23 @@
1
+ import 'server-only'
2
+
3
+ import { createHydrationHelpers } from '@trpc/react-query/rsc'
4
+ import { headers } from 'next/headers'
5
+ import { cache } from 'react'
6
+ import { type AppRouter, createCaller } from '~/server/api/root'
7
+ import { createTRPCContext } from '~/server/api/trpc'
8
+ import { createQueryClient } from './query-client'
9
+
10
+ /** Context for tRPC calls made from React Server Components. */
11
+ const createContext = cache(async () => {
12
+ const heads = new Headers(await headers())
13
+ heads.set('x-trpc-source', 'rsc')
14
+ return createTRPCContext({ headers: heads })
15
+ })
16
+
17
+ const getQueryClient = cache(createQueryClient)
18
+ const caller = createCaller(createContext)
19
+
20
+ export const { trpc: api, HydrateClient } = createHydrationHelpers<AppRouter>(
21
+ caller,
22
+ getQueryClient,
23
+ )
@@ -0,0 +1,37 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "plugins": [
20
+ {
21
+ "name": "next"
22
+ }
23
+ ],
24
+ "paths": {
25
+ "~/*": ["./src/*"]
26
+ }
27
+ },
28
+ "include": [
29
+ "next-env.d.ts",
30
+ "**/*.ts",
31
+ "**/*.tsx",
32
+ ".next/types/**/*.ts",
33
+ ".next/dev/types/**/*.ts",
34
+ "**/*.mts"
35
+ ],
36
+ "exclude": ["node_modules"]
37
+ }
@@ -0,0 +1,13 @@
1
+ node_modules
2
+ .output
3
+ .nitro
4
+ .tanstack
5
+ dist
6
+ .git
7
+ .env
8
+ .env.*
9
+ !.env.example
10
+ src/routeTree.gen.ts
11
+ Dockerfile
12
+ .dockerignore
13
+ README.md
@@ -0,0 +1,28 @@
1
+ # syntax=docker/dockerfile:1
2
+ # Optimized multi-stage build for VPS deployment (TanStack Start + Nitro).
3
+ # `vite build` emits a self-contained Nitro server under .output/ (deps bundled),
4
+ # so the runner image carries only that — no node_modules, no dev deps.
5
+
6
+ FROM node:22-alpine AS base
7
+ RUN corepack enable
8
+ WORKDIR /app
9
+
10
+ # ---- build (needs dev deps for vite/nitro) ----
11
+ FROM base AS build
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
+ COPY . .
16
+ # Env is validated at runtime, not build — skip it so the build needs no secrets.
17
+ ENV SKIP_ENV_VALIDATION=1
18
+ RUN pnpm build
19
+
20
+ # ---- runner: just the Nitro output ----
21
+ FROM base AS runner
22
+ ENV NODE_ENV=production
23
+ RUN addgroup -g 1001 nodejs && adduser -u 1001 -G nodejs -S tanstack
24
+ COPY --from=build --chown=tanstack:nodejs /app/.output ./.output
25
+ USER tanstack
26
+ EXPOSE 3000
27
+ ENV PORT=3000 HOST=0.0.0.0
28
+ CMD ["node", ".output/server/index.mjs"]
@@ -0,0 +1,31 @@
1
+ # tanstack-base
2
+
3
+ Reference TanStack Start app — fork it to start a new project. Comes wired with
4
+ 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 `@tanstack/react-form`
11
+ - **email** — mailer (Resend) + email-kit templates (`email:dev` studio)
12
+ - **API** — tRPC (`useTRPC`) + TanStack React Query with SSR hydration
13
+ - **UI utilities** — theme toggle (light/dark/system), DataTable, DatePicker
14
+ - **deploy** — multi-stage `Dockerfile` (Nitro output) for a VPS
15
+
16
+ ```bash
17
+ pnpm --filter @alfredmouelle/tanstack-base dev # http://localhost:3000
18
+ ```
19
+
20
+ Add more swappable tools with **add-capability**.
21
+
22
+ # Author
23
+
24
+ Alfred MOUELLE | FullStack Developer
25
+
26
+ [![ComeUp](https://img.shields.io/static/v1?style=for-the-badge&label=&message=ComeUp&color=yellow)](https://comeup.com/@alfredmouelle)
27
+ [![GitHub](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/alfredmouelle)
28
+ [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/alfredmouelle)
29
+ [![Twitter](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/kali47_)
30
+ [![Gmail](https://img.shields.io/badge/Gmail-D14836?style=for-the-badge&logo=gmail&logoColor=white)](mailto:alfredmouelle@gmail.com)
31
+ [![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": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/styles.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,85 @@
1
+ {
2
+ "name": "@alfredmouelle/tanstack-base",
3
+ "private": true,
4
+ "type": "module",
5
+ "author": {
6
+ "name": "Alfred MOUELLE",
7
+ "email": "alfredmouelle@gmail.com",
8
+ "url": "https://alfredmouelle.com"
9
+ },
10
+ "scripts": {
11
+ "dev": "vite dev --port 3000",
12
+ "generate-routes": "tsr generate",
13
+ "build": "vite build",
14
+ "preview": "vite preview",
15
+ "test": "vitest run",
16
+ "email:dev": "email dev --dir src/emails --port 3001",
17
+ "typecheck": "tsr generate && tsc --noEmit",
18
+ "check": "biome check .",
19
+ "check:unsafe": "biome check --write --unsafe .",
20
+ "check:write": "biome check --write .",
21
+ "db:generate": "drizzle-kit generate",
22
+ "db:migrate": "drizzle-kit migrate",
23
+ "db:push": "drizzle-kit push",
24
+ "db:studio": "drizzle-kit studio",
25
+ "db:seed": "tsx src/server/db/seed.ts"
26
+ },
27
+ "dependencies": {
28
+ "@fontsource-variable/geist": "^5.2.9",
29
+ "@t3-oss/env-core": "^0.13.11",
30
+ "@tailwindcss/vite": "^4.3.1",
31
+ "@tanstack/react-devtools": "^0.10.7",
32
+ "@tanstack/react-router": "^1.170.16",
33
+ "@tanstack/react-router-devtools": "^1.167.0",
34
+ "@tanstack/react-router-ssr-query": "^1.167.1",
35
+ "@tanstack/react-form": "^1.33.0",
36
+ "@tanstack/react-query": "^5.101.0",
37
+ "@tanstack/react-query-devtools": "^5.101.0",
38
+ "@tanstack/react-table": "^8.21.3",
39
+ "@tanstack/react-start": "^1.168.26",
40
+ "@tanstack/router-plugin": "^1.132.0",
41
+ "@trpc/client": "^11.17.0",
42
+ "@trpc/server": "^11.17.0",
43
+ "@trpc/tanstack-react-query": "^11.17.0",
44
+ "better-auth": "^1.6.19",
45
+ "class-variance-authority": "^0.7.1",
46
+ "clsx": "^2.1.1",
47
+ "date-fns": "^4.4.0",
48
+ "drizzle-orm": "^0.45.2",
49
+ "lucide-react": "^0.545.0",
50
+ "nitro": "3.0.260522-beta",
51
+ "pg": "^8.21.0",
52
+ "radix-ui": "^1.6.0",
53
+ "react": "^19.2.0",
54
+ "react-dom": "^19.2.0",
55
+ "react-day-picker": "^10.0.1",
56
+ "react-email": "^6.6.3",
57
+ "resend": "^6.14.0",
58
+ "superjson": "^2.2.2",
59
+ "tailwind-merge": "^3.6.0",
60
+ "tailwindcss": "^4.3.1",
61
+ "valibot": "^1.4.1"
62
+ },
63
+ "devDependencies": {
64
+ "@biomejs/biome": "^2.5.0",
65
+ "@faker-js/faker": "^10.5.0",
66
+ "@tailwindcss/typography": "^0.5.16",
67
+ "@tanstack/devtools-vite": "^0.8.0",
68
+ "@tanstack/router-cli": "^1.132.0",
69
+ "@testing-library/dom": "^10.4.1",
70
+ "@testing-library/react": "^16.3.0",
71
+ "@types/node": "^22.10.2",
72
+ "@types/pg": "^8.15.6",
73
+ "@types/react": "^19.2.0",
74
+ "@types/react-dom": "^19.2.0",
75
+ "@vitejs/plugin-react": "^6.0.1",
76
+ "drizzle-kit": "^0.31.10",
77
+ "dotenv": "^17.2.4",
78
+ "jsdom": "^28.1.0",
79
+ "tsx": "^4.22.4",
80
+ "tw-animate-css": "^1.4.0",
81
+ "typescript": "^6.0.2",
82
+ "vite": "^8.0.0",
83
+ "vitest": "^4.1.5"
84
+ }
85
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "short_name": "TanStack App",
3
+ "name": "Create TanStack App Sample",
4
+ "icons": [
5
+ {
6
+ "src": "favicon.ico",
7
+ "sizes": "64x64 32x32 24x24 16x16",
8
+ "type": "image/x-icon"
9
+ },
10
+ {
11
+ "src": "logo192.png",
12
+ "type": "image/png",
13
+ "sizes": "192x192"
14
+ },
15
+ {
16
+ "src": "logo512.png",
17
+ "type": "image/png",
18
+ "sizes": "512x512"
19
+ }
20
+ ],
21
+ "start_url": ".",
22
+ "display": "standalone",
23
+ "theme_color": "#000000",
24
+ "background_color": "#ffffff"
25
+ }
@@ -0,0 +1,3 @@
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow:
@@ -0,0 +1,77 @@
1
+ import { flexRender, type Table as ReactTable } from '@tanstack/react-table'
2
+ import { Skeleton } from '~/components/ui/skeleton'
3
+ import {
4
+ Table,
5
+ TableBody,
6
+ TableCell,
7
+ TableHead,
8
+ TableHeader,
9
+ TableRow,
10
+ } from '~/components/ui/table'
11
+
12
+ interface DataTableProps<TData> {
13
+ table: ReactTable<TData>
14
+ columnCount: number
15
+ isLoading: boolean
16
+ emptyLabel: string
17
+ skeletonRows?: number
18
+ }
19
+
20
+ export function DataTable<TData>({
21
+ table,
22
+ columnCount,
23
+ isLoading,
24
+ emptyLabel,
25
+ skeletonRows = 3,
26
+ }: DataTableProps<TData>) {
27
+ const rows = table.getRowModel().rows
28
+
29
+ return (
30
+ <div className="overflow-hidden rounded-lg border">
31
+ <Table>
32
+ <TableHeader>
33
+ {table.getHeaderGroups().map((headerGroup) => (
34
+ <TableRow key={headerGroup.id}>
35
+ {headerGroup.headers.map((header) => (
36
+ <TableHead key={header.id}>
37
+ {header.isPlaceholder
38
+ ? null
39
+ : flexRender(header.column.columnDef.header, header.getContext())}
40
+ </TableHead>
41
+ ))}
42
+ </TableRow>
43
+ ))}
44
+ </TableHeader>
45
+ <TableBody>
46
+ {isLoading ? (
47
+ Array.from({ length: skeletonRows }, (_, rowIndex) => (
48
+ <TableRow key={rowIndex}>
49
+ {Array.from({ length: columnCount }, (_, cellIndex) => (
50
+ <TableCell key={cellIndex}>
51
+ <Skeleton className="h-6 w-full" />
52
+ </TableCell>
53
+ ))}
54
+ </TableRow>
55
+ ))
56
+ ) : rows.length === 0 ? (
57
+ <TableRow>
58
+ <TableCell className="h-24 text-center text-muted-foreground" colSpan={columnCount}>
59
+ {emptyLabel}
60
+ </TableCell>
61
+ </TableRow>
62
+ ) : (
63
+ rows.map((row) => (
64
+ <TableRow className="group" key={row.id}>
65
+ {row.getVisibleCells().map((cell) => (
66
+ <TableCell key={cell.id}>
67
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
68
+ </TableCell>
69
+ ))}
70
+ </TableRow>
71
+ ))
72
+ )}
73
+ </TableBody>
74
+ </Table>
75
+ </div>
76
+ )
77
+ }
@@ -0,0 +1,18 @@
1
+ import type { AnyFieldApi } from '@tanstack/react-form'
2
+
3
+ const errorMessage = (error: unknown): string =>
4
+ typeof error === 'string' ? error : ((error as { message?: string } | undefined)?.message ?? '')
5
+
6
+ export function hasFieldError(field: AnyFieldApi): boolean {
7
+ return field.state.meta.isTouched && field.state.meta.errors.length > 0
8
+ }
9
+
10
+ export function FieldError({ field }: { field: AnyFieldApi }) {
11
+ const { isTouched, errors } = field.state.meta
12
+ if (!isTouched || errors.length === 0) return null
13
+
14
+ const message = errors.map(errorMessage).filter(Boolean).join(', ')
15
+ if (!message) return null
16
+
17
+ return <p className="text-destructive text-xs">{message}</p>
18
+ }