@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,171 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "@fontsource-variable/geist";
4
+
5
+ @custom-variant dark (&:is(.dark *));
6
+
7
+ :root {
8
+ --background: oklch(1 0 0);
9
+ --foreground: oklch(0.145 0 0);
10
+ --card: oklch(1 0 0);
11
+ --card-foreground: oklch(0.145 0 0);
12
+ --popover: oklch(1 0 0);
13
+ --popover-foreground: oklch(0.145 0 0);
14
+ --primary: oklch(0.511 0.096 186.391);
15
+ --primary-foreground: oklch(0.984 0.014 180.72);
16
+ --secondary: oklch(0.967 0.001 286.375);
17
+ --secondary-foreground: oklch(0.21 0.006 285.885);
18
+ --muted: oklch(0.97 0 0);
19
+ --muted-foreground: oklch(0.556 0 0);
20
+ --accent: oklch(0.97 0 0);
21
+ --accent-foreground: oklch(0.205 0 0);
22
+ --destructive: oklch(0.577 0.245 27.325);
23
+ --destructive-foreground: oklch(0.577 0.245 27.325);
24
+ --border: oklch(0.922 0 0);
25
+ --input: oklch(0.922 0 0);
26
+ --ring: oklch(0.708 0 0);
27
+ --chart-1: oklch(0.646 0.222 41.116);
28
+ --chart-2: oklch(0.6 0.118 184.704);
29
+ --chart-3: oklch(0.398 0.07 227.392);
30
+ --chart-4: oklch(0.828 0.189 84.429);
31
+ --chart-5: oklch(0.769 0.188 70.08);
32
+ --radius: 0.45rem;
33
+ --sidebar: oklch(0.985 0 0);
34
+ --sidebar-foreground: oklch(0.145 0 0);
35
+ --sidebar-primary: oklch(0.6 0.118 184.704);
36
+ --sidebar-primary-foreground: oklch(0.984 0.014 180.72);
37
+ --sidebar-accent: oklch(0.97 0 0);
38
+ --sidebar-accent-foreground: oklch(0.205 0 0);
39
+ --sidebar-border: oklch(0.922 0 0);
40
+ --sidebar-ring: oklch(0.708 0 0);
41
+ }
42
+
43
+ .dark {
44
+ --background: oklch(0.145 0 0);
45
+ --foreground: oklch(0.985 0 0);
46
+ --card: oklch(0.205 0 0);
47
+ --card-foreground: oklch(0.985 0 0);
48
+ --popover: oklch(0.205 0 0);
49
+ --popover-foreground: oklch(0.985 0 0);
50
+ --primary: oklch(0.437 0.078 188.216);
51
+ --primary-foreground: oklch(0.984 0.014 180.72);
52
+ --secondary: oklch(0.274 0.006 286.033);
53
+ --secondary-foreground: oklch(0.985 0 0);
54
+ --muted: oklch(0.269 0 0);
55
+ --muted-foreground: oklch(0.708 0 0);
56
+ --accent: oklch(0.269 0 0);
57
+ --accent-foreground: oklch(0.985 0 0);
58
+ --destructive: oklch(0.704 0.191 22.216);
59
+ --destructive-foreground: oklch(0.637 0.237 25.331);
60
+ --border: oklch(1 0 0 / 10%);
61
+ --input: oklch(1 0 0 / 15%);
62
+ --ring: oklch(0.556 0 0);
63
+ --chart-1: oklch(0.488 0.243 264.376);
64
+ --chart-2: oklch(0.696 0.17 162.48);
65
+ --chart-3: oklch(0.769 0.188 70.08);
66
+ --chart-4: oklch(0.627 0.265 303.9);
67
+ --chart-5: oklch(0.645 0.246 16.439);
68
+ --sidebar: oklch(0.205 0 0);
69
+ --sidebar-foreground: oklch(0.985 0 0);
70
+ --sidebar-primary: oklch(0.704 0.14 182.503);
71
+ --sidebar-primary-foreground: oklch(0.277 0.046 192.524);
72
+ --sidebar-accent: oklch(0.269 0 0);
73
+ --sidebar-accent-foreground: oklch(0.985 0 0);
74
+ --sidebar-border: oklch(1 0 0 / 10%);
75
+ --sidebar-ring: oklch(0.556 0 0);
76
+ }
77
+
78
+ @theme inline {
79
+ --font-sans: "Geist Variable", sans-serif;
80
+ --font-heading: var(--font-sans);
81
+ --color-background: var(--background);
82
+ --color-foreground: var(--foreground);
83
+ --color-card: var(--card);
84
+ --color-card-foreground: var(--card-foreground);
85
+ --color-popover: var(--popover);
86
+ --color-popover-foreground: var(--popover-foreground);
87
+ --color-primary: var(--primary);
88
+ --color-primary-foreground: var(--primary-foreground);
89
+ --color-secondary: var(--secondary);
90
+ --color-secondary-foreground: var(--secondary-foreground);
91
+ --color-muted: var(--muted);
92
+ --color-muted-foreground: var(--muted-foreground);
93
+ --color-accent: var(--accent);
94
+ --color-accent-foreground: var(--accent-foreground);
95
+ --color-destructive: var(--destructive);
96
+ --color-destructive-foreground: var(--destructive-foreground);
97
+ --color-border: var(--border);
98
+ --color-input: var(--input);
99
+ --color-ring: var(--ring);
100
+ --color-chart-1: var(--chart-1);
101
+ --color-chart-2: var(--chart-2);
102
+ --color-chart-3: var(--chart-3);
103
+ --color-chart-4: var(--chart-4);
104
+ --color-chart-5: var(--chart-5);
105
+ --color-sidebar: var(--sidebar);
106
+ --color-sidebar-foreground: var(--sidebar-foreground);
107
+ --color-sidebar-primary: var(--sidebar-primary);
108
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
109
+ --color-sidebar-accent: var(--sidebar-accent);
110
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
111
+ --color-sidebar-border: var(--sidebar-border);
112
+ --color-sidebar-ring: var(--sidebar-ring);
113
+ --radius-sm: calc(var(--radius) - 4px);
114
+ --radius-md: calc(var(--radius) - 2px);
115
+ --radius-lg: var(--radius);
116
+ --radius-xl: calc(var(--radius) + 4px);
117
+ --radius-2xl: calc(var(--radius) * 1.8);
118
+ --radius-3xl: calc(var(--radius) * 2.2);
119
+ --radius-4xl: calc(var(--radius) * 2.6);
120
+ }
121
+
122
+ @layer base {
123
+ * {
124
+ @apply border-border outline-ring/50;
125
+ }
126
+ body {
127
+ @apply bg-background text-foreground;
128
+ }
129
+ html {
130
+ @apply font-sans;
131
+ }
132
+ }
133
+
134
+ @layer utilities {
135
+ .scrollbar {
136
+ scrollbar-width: thin;
137
+ scrollbar-color: var(--border) transparent;
138
+ }
139
+ .scrollbar::-webkit-scrollbar {
140
+ width: 8px;
141
+ height: 8px;
142
+ }
143
+ .scrollbar::-webkit-scrollbar-track {
144
+ background: transparent;
145
+ }
146
+ .scrollbar::-webkit-scrollbar-thumb {
147
+ background-color: var(--border);
148
+ border-radius: var(--radius-3xl);
149
+ }
150
+ .scrollbar:hover::-webkit-scrollbar-thumb {
151
+ background-color: var(--muted-foreground);
152
+ }
153
+
154
+ .scrollbar-padded-x::-webkit-scrollbar {
155
+ width: 16px;
156
+ }
157
+ .scrollbar-padded-x::-webkit-scrollbar-thumb {
158
+ border-left: 4px solid transparent;
159
+ border-right: 4px solid transparent;
160
+ background-clip: padding-box;
161
+ }
162
+
163
+ .scrollbar-padded-y::-webkit-scrollbar {
164
+ height: 16px;
165
+ }
166
+ .scrollbar-padded-y::-webkit-scrollbar-thumb {
167
+ border-top: 4px solid transparent;
168
+ border-bottom: 4px solid transparent;
169
+ background-clip: padding-box;
170
+ }
171
+ }
@@ -0,0 +1,6 @@
1
+ import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
2
+
3
+ export default {
4
+ name: 'Tanstack Query',
5
+ render: <ReactQueryDevtoolsPanel />,
6
+ }
@@ -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,49 @@
1
+ import { type QueryClient, QueryClientProvider } from '@tanstack/react-query'
2
+ import { createTRPCClient, httpBatchStreamLink, loggerLink } from '@trpc/client'
3
+ import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'
4
+ import { createTRPCContext, createTRPCOptionsProxy } from '@trpc/tanstack-react-query'
5
+ import type { ReactNode } from 'react'
6
+ import superjson from 'superjson'
7
+ import type { AppRouter } from '~/server/api/root'
8
+
9
+ export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>()
10
+
11
+ export type RouterInputs = inferRouterInputs<AppRouter>
12
+
13
+ export type RouterOutputs = inferRouterOutputs<AppRouter>
14
+
15
+ function getBaseUrl() {
16
+ if (typeof window !== 'undefined') return window.location.origin
17
+ return `http://localhost:${process.env.PORT ?? 3000}`
18
+ }
19
+
20
+ export const trpcClient = createTRPCClient<AppRouter>({
21
+ links: [
22
+ loggerLink({
23
+ enabled: (op) =>
24
+ process.env.NODE_ENV === 'development' ||
25
+ (op.direction === 'down' && op.result instanceof Error),
26
+ }),
27
+ httpBatchStreamLink({
28
+ transformer: superjson,
29
+ url: `${getBaseUrl()}/api/trpc`,
30
+ }),
31
+ ],
32
+ })
33
+
34
+ export function createServerHelpers(queryClient: QueryClient) {
35
+ return createTRPCOptionsProxy<AppRouter>({
36
+ client: trpcClient,
37
+ queryClient,
38
+ })
39
+ }
40
+
41
+ export function TRPCReactProvider(props: { children: ReactNode; queryClient: QueryClient }) {
42
+ return (
43
+ <QueryClientProvider client={props.queryClient}>
44
+ <TRPCProvider queryClient={props.queryClient} trpcClient={trpcClient}>
45
+ {props.children}
46
+ </TRPCProvider>
47
+ </QueryClientProvider>
48
+ )
49
+ }
@@ -0,0 +1,11 @@
1
+ import { getRequest } from '@tanstack/react-start/server'
2
+ import { createCaller } from '~/server/api/root'
3
+ import { createTRPCContext } from '~/server/api/trpc'
4
+
5
+ export const getServerCaller = async () => {
6
+ const request = getRequest()
7
+
8
+ const context = await createTRPCContext({ headers: request.headers })
9
+
10
+ return createCaller(context)
11
+ }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "target": "react"
3
+ }
@@ -0,0 +1,15 @@
1
+ import tailwindcss from '@tailwindcss/vite'
2
+ import { devtools } from '@tanstack/devtools-vite'
3
+
4
+ import { tanstackStart } from '@tanstack/react-start/plugin/vite'
5
+
6
+ import viteReact from '@vitejs/plugin-react'
7
+ import { nitro } from 'nitro/vite'
8
+ import { defineConfig } from 'vite'
9
+
10
+ const config = defineConfig({
11
+ resolve: { tsconfigPaths: true },
12
+ plugins: [devtools(), nitro(), tailwindcss(), tanstackStart(), viteReact()],
13
+ })
14
+
15
+ export default config
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "../../capability.schema.json",
3
+ "name": "analytics",
4
+ "description": "Product analytics behind a swappable port. Capture events and identify users; flush/shutdown to drain pending events.",
5
+ "port": "src/core/port.ts",
6
+ "defaultAdapter": "posthog",
7
+ "adapters": {
8
+ "posthog": {
9
+ "deps": ["posthog-node"],
10
+ "env": ["POSTHOG_API_KEY", "POSTHOG_HOST"],
11
+ "files": ["src/adapters/posthog"]
12
+ },
13
+ "plausible": {
14
+ "deps": ["@alfredmouelle/http"],
15
+ "env": ["PLAUSIBLE_DOMAIN", "PLAUSIBLE_API_HOST"],
16
+ "files": ["src/adapters/plausible"]
17
+ },
18
+ "noop": {
19
+ "deps": [],
20
+ "env": [],
21
+ "files": ["src/adapters/noop"]
22
+ }
23
+ },
24
+ "sharedDeps": ["valibot"],
25
+ "sharedFiles": ["src/core", "src/index.ts"]
26
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "../../capability.schema.json",
3
+ "name": "cache",
4
+ "description": "Key/value cache behind a swappable port. Values are serialized as JSON for remote stores.",
5
+ "port": "src/core/port.ts",
6
+ "defaultAdapter": "redis",
7
+ "adapters": {
8
+ "redis": {
9
+ "deps": ["ioredis"],
10
+ "env": ["REDIS_URL"],
11
+ "files": ["src/adapters/redis"]
12
+ },
13
+ "memory": {
14
+ "deps": [],
15
+ "env": [],
16
+ "files": ["src/adapters/memory"]
17
+ }
18
+ },
19
+ "sharedDeps": ["valibot"],
20
+ "sharedFiles": ["src/core", "src/index.ts"]
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "../../capability.schema.json",
3
+ "name": "error-tracking",
4
+ "description": "Error reporting behind a swappable port. Capture exceptions, messages, breadcrumbs and user context, then ship them to a provider.",
5
+ "port": "src/core/port.ts",
6
+ "defaultAdapter": "sentry",
7
+ "adapters": {
8
+ "sentry": {
9
+ "deps": ["@sentry/node"],
10
+ "env": ["SENTRY_DSN", "SENTRY_ENVIRONMENT"],
11
+ "files": ["src/adapters/sentry"]
12
+ },
13
+ "console": {
14
+ "deps": [],
15
+ "env": [],
16
+ "files": ["src/adapters/console"]
17
+ }
18
+ },
19
+ "sharedDeps": ["valibot"],
20
+ "sharedFiles": ["src/core", "src/index.ts"]
21
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "../../capability.schema.json",
3
+ "name": "jobs",
4
+ "description": "Background jobs / events behind a swappable port. Event-driven: define jobs against named events and trigger them; the adapter handles delivery and execution.",
5
+ "port": "src/core/port.ts",
6
+ "defaultAdapter": "inngest",
7
+ "adapters": {
8
+ "inngest": {
9
+ "deps": ["inngest"],
10
+ "env": ["INNGEST_EVENT_KEY", "INNGEST_SIGNING_KEY"],
11
+ "files": ["src/adapters/inngest"]
12
+ },
13
+ "trigger": {
14
+ "deps": ["@trigger.dev/sdk"],
15
+ "env": ["TRIGGER_SECRET_KEY"],
16
+ "files": ["src/adapters/trigger"]
17
+ },
18
+ "memory": {
19
+ "deps": [],
20
+ "env": [],
21
+ "files": ["src/adapters/memory"]
22
+ }
23
+ },
24
+ "sharedDeps": ["valibot"],
25
+ "sharedFiles": ["src/core", "src/index.ts"]
26
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "../../capability.schema.json",
3
+ "name": "logger",
4
+ "description": "Structured logging behind a swappable port. Application code depends only on the Logger interface; pick an adapter (pino, console) at the composition root.",
5
+ "port": "src/core/port.ts",
6
+ "defaultAdapter": "pino",
7
+ "adapters": {
8
+ "pino": {
9
+ "deps": ["pino"],
10
+ "env": [],
11
+ "files": ["src/adapters/pino"]
12
+ },
13
+ "console": {
14
+ "deps": [],
15
+ "env": [],
16
+ "files": ["src/adapters/console"]
17
+ }
18
+ },
19
+ "sharedDeps": ["valibot"],
20
+ "sharedFiles": ["src/core", "src/index.ts"]
21
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "../../capability.schema.json",
3
+ "name": "mailer",
4
+ "description": "Transactional email. Bodies are always React Email components, rendered to HTML + plain text.",
5
+ "port": "src/core/port.ts",
6
+ "factory": "src/factory.ts",
7
+ "defaultAdapter": "resend",
8
+ "adapters": {
9
+ "resend": {
10
+ "deps": ["resend"],
11
+ "env": ["RESEND_API_KEY"],
12
+ "files": ["src/adapters/resend"]
13
+ },
14
+ "brevo": {
15
+ "deps": ["@getbrevo/brevo"],
16
+ "env": ["BREVO_API_KEY"],
17
+ "files": ["src/adapters/brevo"]
18
+ },
19
+ "ses": {
20
+ "deps": ["@aws-sdk/client-sesv2"],
21
+ "env": ["AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
22
+ "files": ["src/adapters/ses"]
23
+ }
24
+ },
25
+ "sharedDeps": ["valibot", "@react-email/render"],
26
+ "peerDeps": ["react", "react-dom"],
27
+ "sharedFiles": ["src/core", "src/factory.ts", "src/index.ts"]
28
+ }