@usequark/quark-create-app 1.23.11

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 (237) hide show
  1. package/README.md +210 -0
  2. package/package.json +56 -0
  3. package/src/dependabot-config.test.js +128 -0
  4. package/src/deploy/adapters/iac.js +203 -0
  5. package/src/deploy/adapters/index.js +27 -0
  6. package/src/deploy/adapters/railway.js +508 -0
  7. package/src/deploy/adapters/railway.test.js +318 -0
  8. package/src/deploy/contract.js +59 -0
  9. package/src/deploy/deploy.integration.test.js +183 -0
  10. package/src/deploy/deploy.js +430 -0
  11. package/src/deploy/deploy.test.js +105 -0
  12. package/src/deploy/discovery.js +115 -0
  13. package/src/deploy/discovery.test.js +141 -0
  14. package/src/deploy/index.js +38 -0
  15. package/src/deploy/inspect.js +130 -0
  16. package/src/index.js +2920 -0
  17. package/src/mobile-template.test.js +232 -0
  18. package/src/prepare-hooks.test.js +535 -0
  19. package/src/scaffold-guards.test.js +140 -0
  20. package/src/scaffold-output.test.js +228 -0
  21. package/src/template-scripts.test.js +160 -0
  22. package/src/utils.js +77 -0
  23. package/src/utils.test.js +95 -0
  24. package/src/worker-dockerfile.test.js +85 -0
  25. package/templates/base-project/.cursor/rules/quark.mdc +172 -0
  26. package/templates/base-project/.dockerignore +14 -0
  27. package/templates/base-project/.env.railway.example +87 -0
  28. package/templates/base-project/.github/copilot-instructions.md +65 -0
  29. package/templates/base-project/.github/dependabot.yml +31 -0
  30. package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
  31. package/templates/base-project/.github/workflows/ci.yml +116 -0
  32. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  33. package/templates/base-project/.github/workflows/install-windows.yml +27 -0
  34. package/templates/base-project/.github/workflows/release.yml +67 -0
  35. package/templates/base-project/.railway/railway.ts +40 -0
  36. package/templates/base-project/CLAUDE.md +163 -0
  37. package/templates/base-project/MAIN.md +21 -0
  38. package/templates/base-project/README.md +164 -0
  39. package/templates/base-project/apps/web/.env.example +99 -0
  40. package/templates/base-project/apps/web/Dockerfile +38 -0
  41. package/templates/base-project/apps/web/biome.json +8 -0
  42. package/templates/base-project/apps/web/jsconfig.json +9 -0
  43. package/templates/base-project/apps/web/next.config.js +154 -0
  44. package/templates/base-project/apps/web/package.json +42 -0
  45. package/templates/base-project/apps/web/postcss.config.mjs +7 -0
  46. package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
  47. package/templates/base-project/apps/web/public/quark.svg +47 -0
  48. package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
  49. package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
  50. package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
  51. package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
  52. package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
  53. package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
  54. package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
  55. package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
  56. package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
  57. package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
  58. package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
  59. package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
  60. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
  61. package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
  62. package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
  63. package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
  64. package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
  65. package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
  66. package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
  67. package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
  68. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
  69. package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
  70. package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
  71. package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
  72. package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
  73. package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
  74. package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
  75. package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
  76. package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
  77. package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
  78. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  79. package/templates/base-project/apps/web/src/app/global-error.js +58 -0
  80. package/templates/base-project/apps/web/src/app/globals.css +1166 -0
  81. package/templates/base-project/apps/web/src/app/icon.svg +47 -0
  82. package/templates/base-project/apps/web/src/app/layout.js +57 -0
  83. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  84. package/templates/base-project/apps/web/src/app/not-found.js +39 -0
  85. package/templates/base-project/apps/web/src/app/page.js +262 -0
  86. package/templates/base-project/apps/web/src/app/robots.js +23 -0
  87. package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
  88. package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
  89. package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
  90. package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
  91. package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
  92. package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
  93. package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
  94. package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
  95. package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
  96. package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
  97. package/templates/base-project/apps/web/src/lib/auth.js +191 -0
  98. package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
  99. package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
  100. package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
  101. package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
  102. package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
  103. package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
  104. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
  105. package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
  106. package/templates/base-project/apps/web/src/preflight.js +55 -0
  107. package/templates/base-project/apps/web/src/proxy.js +328 -0
  108. package/templates/base-project/biome.json +52 -0
  109. package/templates/base-project/docker-compose.override.yml +17 -0
  110. package/templates/base-project/docker-compose.yml +37 -0
  111. package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
  112. package/templates/base-project/docs/START_HERE.md +70 -0
  113. package/templates/base-project/docs/adr/README.md +42 -0
  114. package/templates/base-project/package.json +58 -0
  115. package/templates/base-project/packages/db/package.json +38 -0
  116. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
  117. package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
  118. package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
  119. package/templates/base-project/packages/db/prisma/seed.js +152 -0
  120. package/templates/base-project/packages/db/prisma.config.js +27 -0
  121. package/templates/base-project/packages/db/src/client.js +75 -0
  122. package/templates/base-project/packages/db/src/connection.js +44 -0
  123. package/templates/base-project/packages/db/src/context.js +275 -0
  124. package/templates/base-project/packages/db/src/index.js +5 -0
  125. package/templates/base-project/packages/db/src/ping.js +66 -0
  126. package/templates/base-project/packages/db/src/queries.js +292 -0
  127. package/templates/base-project/packages/db/src/schemas.js +30 -0
  128. package/templates/base-project/packages/db/src/umami.js +299 -0
  129. package/templates/base-project/pnpm-workspace.yaml +20 -0
  130. package/templates/base-project/scripts/check-loading.mjs +49 -0
  131. package/templates/base-project/scripts/check-standards.mjs +186 -0
  132. package/templates/base-project/scripts/clean-workspace.mjs +479 -0
  133. package/templates/base-project/scripts/dev-preflight.mjs +48 -0
  134. package/templates/base-project/scripts/doctor.js +408 -0
  135. package/templates/base-project/scripts/prepare.js +490 -0
  136. package/templates/base-project/scripts/run-tests.mjs +71 -0
  137. package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
  138. package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
  139. package/templates/base-project/skills/add-model/SKILL.md +28 -0
  140. package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
  141. package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
  142. package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
  143. package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
  144. package/templates/base-project/skills/i18n/SKILL.md +211 -0
  145. package/templates/base-project/skills/payment/SKILL.md +280 -0
  146. package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
  147. package/templates/base-project/turbo.json +143 -0
  148. package/templates/config/package.json +20 -0
  149. package/templates/config/src/app-url.js +134 -0
  150. package/templates/config/src/environment.js +284 -0
  151. package/templates/config/src/index.js +16 -0
  152. package/templates/config/src/load-config.js +157 -0
  153. package/templates/config/src/rate-limit.js +136 -0
  154. package/templates/config/src/redis.js +43 -0
  155. package/templates/config/src/validate-env.js +532 -0
  156. package/templates/jobs/README.md +52 -0
  157. package/templates/jobs/package.json +9 -0
  158. package/templates/jobs/src/definitions.js +13 -0
  159. package/templates/jobs/src/index.js +1 -0
  160. package/templates/mobile/.env.example +16 -0
  161. package/templates/mobile/CHECKLIST.md +40 -0
  162. package/templates/mobile/README.md +130 -0
  163. package/templates/mobile/app/(app)/_layout.tsx +83 -0
  164. package/templates/mobile/app/(app)/index.tsx +147 -0
  165. package/templates/mobile/app/(app)/profile.tsx +204 -0
  166. package/templates/mobile/app/(app)/settings.tsx +80 -0
  167. package/templates/mobile/app/(auth)/_layout.tsx +10 -0
  168. package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
  169. package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
  170. package/templates/mobile/app/+not-found.tsx +32 -0
  171. package/templates/mobile/app/_layout.tsx +18 -0
  172. package/templates/mobile/app.json +47 -0
  173. package/templates/mobile/assets/adaptive-icon.png +0 -0
  174. package/templates/mobile/assets/icon.png +0 -0
  175. package/templates/mobile/assets/notification-icon.png +0 -0
  176. package/templates/mobile/assets/splash.png +0 -0
  177. package/templates/mobile/biome.json +27 -0
  178. package/templates/mobile/components/AuthGate.tsx +19 -0
  179. package/templates/mobile/components/ThemeProvider.tsx +23 -0
  180. package/templates/mobile/eas.json +29 -0
  181. package/templates/mobile/hooks/use-auth.ts +127 -0
  182. package/templates/mobile/lib/api-client.ts +138 -0
  183. package/templates/mobile/lib/api.ts +83 -0
  184. package/templates/mobile/lib/auth-providers-google.ts +74 -0
  185. package/templates/mobile/lib/auth-providers.ts +74 -0
  186. package/templates/mobile/lib/auth.ts +100 -0
  187. package/templates/mobile/lib/config.ts +14 -0
  188. package/templates/mobile/lib/errors.ts +46 -0
  189. package/templates/mobile/lib/notifications.ts +77 -0
  190. package/templates/mobile/lib/storage.ts +25 -0
  191. package/templates/mobile/lib/theme-tokens.ts +16 -0
  192. package/templates/mobile/package.json +37 -0
  193. package/templates/mobile/tsconfig.json +10 -0
  194. package/templates/pwa/app/_components/PWARegister.js +15 -0
  195. package/templates/pwa/app/manifest.js +32 -0
  196. package/templates/pwa/public/sw.js +72 -0
  197. package/templates/ui/README.md +44 -0
  198. package/templates/ui/package.json +21 -0
  199. package/templates/ui/src/badge.js +23 -0
  200. package/templates/ui/src/button.js +111 -0
  201. package/templates/ui/src/card.js +128 -0
  202. package/templates/ui/src/checkbox.js +20 -0
  203. package/templates/ui/src/container.js +13 -0
  204. package/templates/ui/src/dialog.js +137 -0
  205. package/templates/ui/src/error-banner.js +15 -0
  206. package/templates/ui/src/footer.js +239 -0
  207. package/templates/ui/src/form-field.integration-test.js +142 -0
  208. package/templates/ui/src/form-field.js +50 -0
  209. package/templates/ui/src/index.js +24 -0
  210. package/templates/ui/src/input.js +11 -0
  211. package/templates/ui/src/label.js +14 -0
  212. package/templates/ui/src/lightbox.integration-test.js +167 -0
  213. package/templates/ui/src/lightbox.js +243 -0
  214. package/templates/ui/src/logo.js +52 -0
  215. package/templates/ui/src/navbar.js +602 -0
  216. package/templates/ui/src/password-input.js +31 -0
  217. package/templates/ui/src/rich-text.js +514 -0
  218. package/templates/ui/src/select.js +317 -0
  219. package/templates/ui/src/skeleton.js +10 -0
  220. package/templates/ui/src/spinner.js +31 -0
  221. package/templates/ui/src/table.js +169 -0
  222. package/templates/ui/src/textarea.js +11 -0
  223. package/templates/ui/src/theme-constants.js +24 -0
  224. package/templates/ui/src/theme.js +196 -0
  225. package/templates/ui/src/toast.js +206 -0
  226. package/templates/worker/Dockerfile +64 -0
  227. package/templates/worker/README.md +56 -0
  228. package/templates/worker/package.json +36 -0
  229. package/templates/worker/src/config/pricing.js +46 -0
  230. package/templates/worker/src/handlers/email-job-validation.js +36 -0
  231. package/templates/worker/src/handlers/email.js +98 -0
  232. package/templates/worker/src/handlers/files.js +59 -0
  233. package/templates/worker/src/handlers/index.js +20 -0
  234. package/templates/worker/src/handlers/push-notification.js +234 -0
  235. package/templates/worker/src/index.js +505 -0
  236. package/templates/worker/src/lib/railway.js +438 -0
  237. package/templates/worker/src/lib/tokens.js +81 -0
@@ -0,0 +1,195 @@
1
+ ---
2
+ name: ecommerce-checkout
3
+ description: Checkout flow, order lifecycle, and inventory management for Quark ecommerce. Use when building checkout, orders, payments, fulfillment, or stock management.
4
+ ---
5
+
6
+ # Ecommerce Checkout Skill
7
+
8
+ Build the checkout flow, order lifecycle, and inventory management for an ecommerce system on Quark.
9
+
10
+ ## Models
11
+
12
+ ```prisma
13
+ enum OrderStatus {
14
+ PENDING
15
+ PAID
16
+ PROCESSING
17
+ FULFILLED
18
+ DELIVERED
19
+ REFUNDED
20
+ CANCELLED
21
+ }
22
+
23
+ model Order {
24
+ id String @id @default(cuid())
25
+ userId String?
26
+ user User? @relation(fields: [userId], references: [id])
27
+ status OrderStatus @default(PENDING)
28
+ email String
29
+ shippingName String
30
+ shippingAddress String @db.Text
31
+ shippingCity String
32
+ shippingZip String
33
+ shippingCountry String
34
+ total Decimal @db.Decimal(10, 2)
35
+ currency String @default("USD")
36
+ stripeSessionId String? @unique
37
+ paidAt DateTime?
38
+ fulfilledAt DateTime?
39
+ deliveredAt DateTime?
40
+ createdAt DateTime @default(now())
41
+ updatedAt DateTime @updatedAt
42
+ items OrderItem[]
43
+
44
+ @@index([userId, status])
45
+ @@index([status])
46
+ @@index([stripeSessionId])
47
+ @@index([createdAt])
48
+ }
49
+
50
+ model OrderItem {
51
+ id String @id @default(cuid())
52
+ orderId String
53
+ order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
54
+ variantId String
55
+ variant ProductVariant @relation(fields: [variantId], references: [id])
56
+ quantity Int
57
+ unitPrice Decimal @db.Decimal(10, 2)
58
+ createdAt DateTime @default(now())
59
+ updatedAt DateTime @updatedAt
60
+
61
+ @@index([orderId])
62
+ }
63
+ ```
64
+
65
+ ## Checkout action
66
+
67
+ ```js
68
+ "use server";
69
+ import { z } from "zod";
70
+ import { prisma } from "@__QUARK_SCOPE__/db";
71
+ import { AppError, ValidationError } from "@techstream/quark-core/errors";
72
+ import { createLogger } from "@techstream/quark-core";
73
+
74
+ const log = createLogger("action:checkout");
75
+
76
+ const checkoutSchema = z.object({
77
+ cartId: z.string().min(1),
78
+ email: z.string().email(),
79
+ shippingName: z.string().min(1),
80
+ shippingAddress: z.string().min(1),
81
+ shippingCity: z.string().min(1),
82
+ shippingZip: z.string().min(1),
83
+ shippingCountry: z.string().min(2).max(2),
84
+ });
85
+
86
+ export async function createOrder(formData) {
87
+ const parsed = checkoutSchema.safeParse(Object.fromEntries(formData));
88
+ if (!parsed.success) throw new ValidationError(parsed.error.flatten());
89
+
90
+ const { cartId } = parsed.data;
91
+
92
+ const order = await prisma.$transaction(async (tx) => {
93
+ const cart = await tx.cart.findUnique({
94
+ where: { id: cartId },
95
+ include: { items: { include: { variant: true } } },
96
+ });
97
+ if (!cart || cart.items.length === 0) {
98
+ throw new AppError("Cart is empty", 400, "EMPTY_CART");
99
+ }
100
+
101
+ for (const item of cart.items) {
102
+ const updated = await tx.productVariant.updateMany({
103
+ where: { id: item.variantId, stock: { gte: item.quantity } },
104
+ data: { stock: { decrement: item.quantity } },
105
+ });
106
+ if (updated.count === 0) {
107
+ throw new AppError(`Out of stock: ${item.variant.name}`, 409, "OUT_OF_STOCK");
108
+ }
109
+ }
110
+
111
+ const total = cart.items.reduce(
112
+ (sum, item) => sum + Number(item.variant.price) * item.quantity, 0
113
+ );
114
+
115
+ const order = await tx.order.create({
116
+ data: {
117
+ ...parsed.data,
118
+ total,
119
+ items: {
120
+ create: cart.items.map((item) => ({
121
+ variantId: item.variantId,
122
+ quantity: item.quantity,
123
+ unitPrice: item.variant.price,
124
+ })),
125
+ },
126
+ },
127
+ });
128
+
129
+ await tx.cartItem.deleteMany({ where: { cartId } });
130
+ log.info("order created", { orderId: order.id, total });
131
+ return order;
132
+ });
133
+
134
+ return order;
135
+ }
136
+ ```
137
+
138
+ ## Order lifecycle
139
+
140
+ ```
141
+ PENDING → PAID (payment webhook)
142
+ PAID → PROCESSING (admin starts fulfillment)
143
+ PROCESSING → FULFILLED (admin ships)
144
+ FULFILLED → DELIVERED (carrier/admin)
145
+ PAID/PROCESSING/FULFILLED → REFUNDED
146
+ Any → CANCELLED (before fulfillment, admin only)
147
+ ```
148
+
149
+ ```js
150
+ const ORDER_TRANSITIONS = {
151
+ PENDING: ["PAID", "CANCELLED"],
152
+ PAID: ["PROCESSING", "REFUNDED", "CANCELLED"],
153
+ PROCESSING: ["FULFILLED", "REFUNDED"],
154
+ FULFILLED: ["DELIVERED", "REFUNDED"],
155
+ DELIVERED: ["REFUNDED"],
156
+ REFUNDED: [],
157
+ CANCELLED: [],
158
+ };
159
+
160
+ export function canTransitionOrder(from, to) {
161
+ return ORDER_TRANSITIONS[from]?.includes(to) ?? false;
162
+ }
163
+
164
+ export function applyOrderTransition(order, newStatus, opts = {}) {
165
+ if (!canTransitionOrder(order.status, newStatus)) {
166
+ throw new ValidationError(`Cannot transition from ${order.status} to ${newStatus}`);
167
+ }
168
+ const data = { status: newStatus };
169
+ if (newStatus === "PAID") data.paidAt = new Date();
170
+ if (newStatus === "FULFILLED") data.fulfilledAt = new Date();
171
+ if (newStatus === "DELIVERED") data.deliveredAt = new Date();
172
+ return data;
173
+ }
174
+ ```
175
+
176
+ ## Inventory management
177
+
178
+ Each `ProductVariant` tracks `stock` (total) and `reserved` (pending orders). **Available** = `stock - reserved`. Use atomic decrement in a transaction to prevent overselling.
179
+
180
+ Optional reserve/release pattern:
181
+
182
+ ```js
183
+ // Reserve on cart add
184
+ await tx.productVariant.update({ where: { id: variantId }, data: { reserved: { increment: qty } } });
185
+ // Release on cart abandon/cancel
186
+ await tx.productVariant.update({ where: { id: variantId }, data: { reserved: { decrement: qty } } });
187
+ ```
188
+
189
+ ## Payment integration
190
+
191
+ Reference the payment skill for Stripe Checkout:
192
+ 1. Create a Checkout Session with cart line items.
193
+ 2. Store `stripeSessionId` on the Order.
194
+ 3. Handle `checkout.session.completed` webhook → transition to `PAID`.
195
+ 4. Handle refunds via Stripe API → transition to `REFUNDED`.
@@ -0,0 +1,211 @@
1
+ ---
2
+ name: i18n
3
+ description: Add internationalization and localization to Quark apps. Use when adding multi-language support, locale routing, hreflang SEO, locale-aware sitemaps, or localized CMS content.
4
+ ---
5
+
6
+ # i18n Skill
7
+
8
+ Add multi-language support to a Quark app using `next-intl` with prefix-based routing (`/en/...`), locale detection, and localized CMS content queries. This skill gives you the domain context, Quark framework patterns, and end-result shape. You generate the code that fits the user's exact requirements.
9
+
10
+ ## Context
11
+
12
+ A localized app must handle locale detection, routing, content translation, and SEO. The core concerns:
13
+
14
+ - **Locale detection** — resolve the active locale from URL prefix, cookie, `Accept-Language` header, then fallback to default.
15
+ - **Routing** — prefix-based (`/en/about`, `/es/acerca-de`) for SEO and shareable URLs.
16
+ - **Content localization** — locale field on CMS models, queries filtered by locale, fallback chains.
17
+ - **SEO** — `hreflang` alternates, locale-aware sitemaps, `lang` attribute on `<html>`.
18
+ - **UI** — locale switcher component, localized formatting (dates, numbers).
19
+
20
+ ## Locale detection chain
21
+
22
+ ```
23
+ 1. URL path prefix (/en/...) → explicit user choice
24
+ 2. Cookie (n-lang) ← set by LocaleSwitcher component
25
+ 3. Accept-Language ← browser default
26
+ 4. defaultLocale ← configured fallback ("en")
27
+ ```
28
+
29
+ ## Framework context (build on Quark)
30
+
31
+ - **Library:** `next-intl` — purpose-built for Next.js App Router, native Server Component support, middleware-based locale negotiation, ~2 KB bundle.
32
+ - **Routing:** prefix-based with `localePrefix: 'always'`. All public URLs include the locale prefix. Admin (`/admin`), auth (`/auth`), and API (`/api`) routes stay outside `[locale]`.
33
+ - **Middleware:** `next-intl/middleware` handles locale detection, redirects, and 404s. Configure in `src/i18n/middleware.js`.
34
+ - **Request config:** `getRequestConfig` from `next-intl/server` resolves the locale per request and loads message files.
35
+ - **Models:** CMS models get a `locale String` field. Queries filter by locale with fallback chain.
36
+ - **Validation** — Zod for all Server Actions and API routes. Use a locale enum for localized content submissions.
37
+ - **Auth** via `getCurrentSession` from `@techstream/quark-core`.
38
+ - **Locale utilities** from `@techstream/quark-core/locale`: `getDefaultLocale()`, `getSupportedLocales()`, `isLocaleSupported()`.
39
+
40
+ ## Workflow
41
+
42
+ 1. Read `CLAUDE.md` and `MAIN.md` for project conventions.
43
+ 2. Install `next-intl` and configure `getRequestConfig`, middleware, and routing.
44
+ 3. Create message files per locale under `messages/` (e.g., `messages/en.json`, `messages/es.json`).
45
+ 4. Wrap the app root with `NextIntlClientProvider`; set `html lang` attribute.
46
+ 5. Add a locale switcher component (client-side, updates cookie + router).
47
+ 6. Add CMS localization: locale field on models, query helpers filtered by locale.
48
+ 7. Add SEO: `hreflang` alternates, locale-aware sitemap, metadata per locale.
49
+
50
+ ## Example: routing config
51
+
52
+ ```js
53
+ // src/i18n/routing.js
54
+ import { defineRouting } from "next-intl/routing";
55
+ import { getDefaultLocale, getSupportedLocales } from "@techstream/quark-core/locale";
56
+
57
+ const supportedLocales = getSupportedLocales();
58
+
59
+ export const routing = defineRouting({
60
+ locales: supportedLocales,
61
+ defaultLocale: getDefaultLocale(),
62
+ localePrefix: "always",
63
+ });
64
+ ```
65
+
66
+ ## Example: request config
67
+
68
+ ```js
69
+ // src/i18n/request.js
70
+ import { getRequestConfig } from "next-intl/server";
71
+ import { hasLocale } from "next-intl";
72
+ import { routing } from "./routing.js";
73
+
74
+ export default getRequestConfig(async ({ requestLocale }) => {
75
+ const requested = await requestLocale;
76
+ const locale = hasLocale(routing.locales, requested)
77
+ ? requested
78
+ : routing.defaultLocale;
79
+
80
+ return {
81
+ locale,
82
+ messages: (await import(`../../messages/${locale}.json`)).default,
83
+ };
84
+ });
85
+ ```
86
+
87
+ ## Example: middleware setup
88
+
89
+ ```js
90
+ // src/i18n/middleware.js
91
+ import createMiddleware from "next-intl/middleware";
92
+ import { routing } from "./routing.js";
93
+
94
+ export default createMiddleware(routing);
95
+
96
+ export const config = {
97
+ matcher: ["/((?!api|admin|auth|_next|.*\\..*).*)"],
98
+ };
99
+ ```
100
+
101
+ ## Example: locale switcher component
102
+
103
+ ```jsx
104
+ "use client";
105
+ import { useLocale, useTranslations } from "next-intl";
106
+ import { useRouter, usePathname } from "next/navigation";
107
+ import { getSupportedLocales } from "@techstream/quark-core/locale";
108
+
109
+ export function LocaleSwitcher() {
110
+ const locale = useLocale();
111
+ const router = useRouter();
112
+ const pathname = usePathname();
113
+ const t = useTranslations("LocaleSwitcher");
114
+ const locales = getSupportedLocales();
115
+
116
+ function onSelect(next) {
117
+ const path = pathname.replace(`/${locale}`, `/${next}`);
118
+ document.cookie = `n-lang=${next};path=/;max-age=31536000`;
119
+ router.push(path);
120
+ }
121
+
122
+ return (
123
+ <select aria-label={t("label")} value={locale} onChange={(e) => onSelect(e.target.value)}>
124
+ {locales.map((l) => (
125
+ <option key={l} value={l}>{l.toUpperCase()}</option>
126
+ ))}
127
+ </select>
128
+ );
129
+ }
130
+ ```
131
+
132
+ ## Example: CMS model with locale
133
+
134
+ ```prisma
135
+ model Page {
136
+ id String @id @default(cuid())
137
+ title String
138
+ slug String
139
+ body String @db.Text
140
+ locale String @default("en")
141
+ status ContentStatus @default(DRAFT)
142
+ authorId String
143
+ author User @relation(fields: [authorId], references: [id])
144
+ createdAt DateTime @default(now())
145
+ updatedAt DateTime @updatedAt
146
+
147
+ @@unique([slug, locale])
148
+ @@index([locale])
149
+ }
150
+ ```
151
+
152
+ ## Example: locale-aware query
153
+
154
+ ```js
155
+ import { getDefaultLocale, isLocaleSupported } from "@techstream/quark-core/locale";
156
+
157
+ export async function getPageBySlug(slug, locale) {
158
+ const validLocale = isLocaleSupported(locale) ? locale : getDefaultLocale();
159
+ return prisma.page.findFirst({
160
+ where: { slug, locale: validLocale, status: "PUBLISHED" },
161
+ });
162
+ }
163
+ ```
164
+
165
+ ## SEO: hreflang and sitemap
166
+
167
+ ```jsx
168
+ // In your page component's generateMetadata:
169
+ export async function generateMetadata({ params }) {
170
+ const { locale } = await params;
171
+ const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
172
+
173
+ return {
174
+ alternates: {
175
+ languages: Object.fromEntries(
176
+ getSupportedLocales().map((l) => [l, `${baseUrl}/${l}/${slug}`])
177
+ ),
178
+ },
179
+ lang: locale,
180
+ };
181
+ }
182
+ ```
183
+
184
+ ## Example: Zod validation for localized content
185
+
186
+ ```js
187
+ import { z } from "zod";
188
+ import { isLocaleSupported } from "@techstream/quark-core/locale";
189
+
190
+ export const localizedPageSchema = z.object({
191
+ title: z.string().min(1).max(255),
192
+ slug: z.string().min(1).max(255),
193
+ body: z.string().min(1),
194
+ locale: z.string().refine(isLocaleSupported, "Unsupported locale"),
195
+ status: z.enum(["DRAFT", "PUBLISHED"]).default("DRAFT"),
196
+ });
197
+ ```
198
+
199
+ ## Workflow summary
200
+
201
+ 1. Install `next-intl`, configure routing + middleware + request config.
202
+ 2. Create `messages/{locale}.json` with translation keys.
203
+ 3. Wrap root layout with `NextIntlClientProvider`, set `html lang`.
204
+ 4. Build a `LocaleSwitcher` client component.
205
+ 5. Add `locale` field to CMS models, update queries with fallback.
206
+ 6. Add `hreflang` alternates and locale-aware sitemap.
207
+ 7. Test detection chain: URL → cookie → Accept-Language → default.
208
+
209
+ ## End result
210
+
211
+ A localized Quark app where users switch languages via a prefix-based URL scheme, locale is detected from URL → cookie → header → default, CMS content is filtered by locale with fallback, SEO metadata includes `hreflang` alternates and locale-aware sitemaps, and all content submissions validate locale against the configured supported set — following Quark conventions throughout.
@@ -0,0 +1,280 @@
1
+ ---
2
+ name: payment
3
+ description: Stripe payment integration - checkout sessions, webhooks, customer management, and fulfillment. Use when building payment flows, checkout, subscriptions, or handling Stripe webhooks.
4
+ ---
5
+
6
+ # Payment Skill
7
+
8
+ Build Stripe payment flows on Quark. This skill covers the full Stripe lifecycle: customer management, checkout sessions, webhook verification, and post-payment fulfillment.
9
+
10
+ ## Domain context
11
+
12
+ Stripe integration lifecycle:
13
+
14
+ ```
15
+ Customer → Checkout Session → Payment → Webhook → Fulfillment
16
+ ```
17
+
18
+ Key concepts:
19
+ - **Stripe Customer** — persistent record of a payer; store the `stripeCustomerId` on your User model.
20
+ - **Checkout Session** — hosted payment page for one-time or subscription payments.
21
+ - **Webhook** — Stripe sends HTTP events to your server after payment events occur.
22
+ - **Fulfillment** — your server's response to a successful payment (update status, send email, grant access).
23
+ - **Idempotency** — webhook handler must be safe to retry; use `eventId` deduplication.
24
+
25
+ ## Framework context
26
+
27
+ - **Stripe client** — `createStripeClient()` from `@techstream/quark-core/stripe`. Reads `STRIPE_SECRET_KEY` from env.
28
+ - **Webhook route** — `apps/web/src/app/api/stripe/webhook/route.js`. Use `getStripeWebhookEvent()` from `@techstream/quark-core/stripe` to verify signatures.
29
+ - **Validation** — Zod for all incoming data. Webhook event data is already validated by Stripe's SDK.
30
+ - **Errors** — `AppError` / `ValidationError` from `@techstream/quark-core/errors` for app errors. Never throw on webhook failures — return 200 to prevent Stripe retries for unfixable errors.
31
+ - **Logging** — `createLogger("stripe:webhook")` from `@techstream/quark-core`.
32
+ - **Models** — add `stripeCustomerId String?` to User, `stripeSessionId String?` / `stripeSubscriptionId String?` to relevant models. Every model needs `createdAt` / `updatedAt`.
33
+
34
+ ## Checkout sessions
35
+
36
+ ### Zod validation for checkout input
37
+
38
+ ```js
39
+ import { z } from "zod";
40
+
41
+ export const checkoutSchema = z.object({
42
+ priceId: z.string().min(1),
43
+ quantity: z.number().int().positive().default(1),
44
+ successUrl: z.string().url().optional(),
45
+ cancelUrl: z.string().url().optional(),
46
+ });
47
+ ```
48
+
49
+ ### One-time payment
50
+
51
+ ```js
52
+ import { createStripeClient } from "@techstream/quark-core/stripe";
53
+
54
+ const stripe = await createStripeClient();
55
+ const session = await stripe.checkout.sessions.create({
56
+ mode: "payment",
57
+ customer: stripeCustomerId, // or omit to create anonymous checkout
58
+ line_items: [{ price: priceId, quantity: 1 }],
59
+ success_url: `${origin}/payment/success?session_id={CHECKOUT_SESSION_ID}`,
60
+ cancel_url: `${origin}/payment/cancel`,
61
+ metadata: { orderId },
62
+ });
63
+ ```
64
+
65
+ ### Subscription
66
+
67
+ ```js
68
+ const session = await stripe.checkout.sessions.create({
69
+ mode: "subscription",
70
+ customer: stripeCustomerId,
71
+ line_items: [{ price: priceId, quantity: 1 }],
72
+ success_url: `${origin}/account?upgraded=true`,
73
+ cancel_url: `${origin}/pricing`,
74
+ metadata: { userId },
75
+ });
76
+ ```
77
+
78
+ ## Webhooks
79
+
80
+ ### Webhook route handler
81
+
82
+ ```js
83
+ // apps/web/src/app/api/stripe/webhook/route.js
84
+ import { NextResponse } from "next/server";
85
+ import { getStripeWebhookEvent } from "@techstream/quark-core/stripe";
86
+ import { createLogger } from "@techstream/quark-core";
87
+ import { prisma } from "@__QUARK_SCOPE__/db";
88
+
89
+ const log = createLogger("stripe:webhook");
90
+
91
+ export async function POST(request) {
92
+ const body = await request.text();
93
+ const signature = request.headers.get("stripe-signature");
94
+
95
+ let event;
96
+ try {
97
+ event = await getStripeWebhookEvent(body, signature);
98
+ } catch (err) {
99
+ log.error("webhook signature verification failed", { error: err.message });
100
+ return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
101
+ }
102
+
103
+ log.info("webhook received", { type: event.type, id: event.id });
104
+
105
+ // Idempotency check — skip if already processed
106
+ const existing = await prisma.webhookEvent.findUnique({
107
+ where: { stripeEventId: event.id },
108
+ });
109
+ if (existing) {
110
+ return NextResponse.json({ received: true });
111
+ }
112
+
113
+ // Record event for idempotency
114
+ await prisma.webhookEvent.create({
115
+ data: {
116
+ stripeEventId: event.id,
117
+ type: event.type,
118
+ payload: event,
119
+ },
120
+ });
121
+
122
+ try {
123
+ switch (event.type) {
124
+ case "checkout.session.completed":
125
+ await handleCheckoutCompleted(event.data.object);
126
+ break;
127
+ case "invoice.paid":
128
+ await handleInvoicePaid(event.data.object);
129
+ break;
130
+ case "customer.subscription.deleted":
131
+ await handleSubscriptionDeleted(event.data.object);
132
+ break;
133
+ default:
134
+ log.info("unhandled event type", { type: event.type });
135
+ }
136
+ } catch (err) {
137
+ log.error("webhook handler error", { type: event.type, error: err.message });
138
+ // Return 200 to prevent Stripe retry for handler errors
139
+ // Log and alert instead — retrying won't fix app bugs
140
+ }
141
+
142
+ return NextResponse.json({ received: true });
143
+ }
144
+ ```
145
+
146
+ ### Event verification
147
+
148
+ `await getStripeWebhookEvent(body, signature)` calls `stripe.webhooks.constructEvent()` under the hood. It reads `STRIPE_WEBHOOK_SECRET` from env and throws if the signature is invalid.
149
+
150
+ ### Common events to handle
151
+
152
+ | Event | Use case |
153
+ |---|---|
154
+ | `checkout.session.completed` | Mark order/booking as paid, grant access |
155
+ | `invoice.paid` | Subscription renewal succeeded |
156
+ | `invoice.payment_failed` | Subscription payment failed, notify user |
157
+ | `customer.subscription.deleted` | Revoke access, update status |
158
+ | `charge.refunded` | Process refund, update order status |
159
+
160
+ ### Idempotency
161
+
162
+ Use a `WebhookEvent` model to record processed event IDs. Check before processing to prevent duplicate fulfillment:
163
+
164
+ ```prisma
165
+ model WebhookEvent {
166
+ id String @id @default(cuid())
167
+ stripeEventId String @unique
168
+ type String
169
+ payload Json
170
+ createdAt DateTime @default(now())
171
+ updatedAt DateTime @updatedAt
172
+
173
+ @@index([stripeEventId])
174
+ @@index([type])
175
+ }
176
+ ```
177
+
178
+ ## Customer management
179
+
180
+ ### Create or retrieve Stripe customer
181
+
182
+ ```js
183
+ import { createStripeClient } from "@techstream/quark-core/stripe";
184
+
185
+ export async function getOrCreateStripeCustomer(user) {
186
+ if (user.stripeCustomerId) return user.stripeCustomerId;
187
+
188
+ const stripe = await createStripeClient();
189
+ const customer = await stripe.customers.create({
190
+ email: user.email,
191
+ name: user.name,
192
+ metadata: { userId: user.id },
193
+ });
194
+
195
+ await prisma.user.update({
196
+ where: { id: user.id },
197
+ data: { stripeCustomerId: customer.id },
198
+ });
199
+
200
+ return customer.id;
201
+ }
202
+ ```
203
+
204
+ ### User model additions
205
+
206
+ Add to your User model in Prisma schema:
207
+
208
+ ```prisma
209
+ model User {
210
+ // ... existing fields
211
+ stripeCustomerId String? @unique
212
+ }
213
+ ```
214
+
215
+ ## Integration patterns
216
+
217
+ ### Booking deposits
218
+
219
+ Collect a deposit at booking time:
220
+
221
+ 1. Create booking in `PENDING` status with `stripeSessionId`.
222
+ 2. Redirect to Stripe Checkout.
223
+ 3. On `checkout.session.completed` webhook: update booking to `CONFIRMED`.
224
+ 4. On cancellation: refund via `stripe.refunds.create({ payment_intent })`.
225
+
226
+ ### Ecommerce checkout
227
+
228
+ 1. Create an Order model with `stripeSessionId` and status `PENDING`.
229
+ 2. Checkout Session with line items from cart.
230
+ 3. Webhook confirms payment → update Order to `PAID`, decrement inventory.
231
+ 4. Handle `payment_failed` → notify user, keep order in `PENDING`.
232
+
233
+ ## Testing
234
+
235
+ ### Stripe test mode
236
+
237
+ - Use Stripe test keys (`sk_test_...`) in `.env` for development.
238
+ - Test card: `4242 4242 4242 4242` (any future expiry, any CVC).
239
+ - Stripe Dashboard → Developers → Webhooks → add `http://localhost:3000/api/stripe/webhook` for local testing.
240
+
241
+ ### Webhook CLI
242
+
243
+ Install the Stripe CLI and forward webhooks to your local server:
244
+
245
+ ```bash
246
+ stripe listen --forward-to localhost:3000/api/stripe/webhook
247
+ ```
248
+
249
+ The CLI prints a webhook signing secret (`whsec_...`) — add it to `.env` as `STRIPE_WEBHOOK_SECRET`.
250
+
251
+ ### Test pattern
252
+
253
+ ```js
254
+ import { test } from "node:test";
255
+ import assert from "node:assert";
256
+
257
+ test("handleCheckoutCompleted updates booking status", async () => {
258
+ // Create a mock Stripe session object
259
+ // Assert booking status changes from PENDING to CONFIRMED
260
+ });
261
+ ```
262
+
263
+ ## Workflow
264
+
265
+ 1. Read `CLAUDE.md` and project conventions.
266
+ 2. Add `stripe` dependency to the consuming app if not already present.
267
+ 3. Set up env vars: `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`.
268
+ 4. Create Stripe Customer ID field on User model (if customer management is needed).
269
+ 5. Build checkout session creation (Server Action or API route).
270
+ 6. Implement webhook route handler with signature verification and idempotency.
271
+ 7. Add fulfillment logic (update models, send emails, grant access).
272
+ 8. Test with Stripe CLI webhook forwarding and test card numbers.
273
+
274
+ ## Reference
275
+
276
+ Stripe docs: https://docs.stripe.com
277
+
278
+ ## End result
279
+
280
+ A working payment system where users can complete Stripe Checkout for one-time or subscription payments, webhooks verify signatures and deduplicate events via `WebhookEvent`, Stripe customers are created lazily and stored on the User model, and post-payment fulfillment (booking confirmation, order status, access grants) runs idempotently — all following Quark conventions.