@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,328 @@
1
+ /**
2
+ * Next.js Proxy
3
+ * Handles auth guards, rate limiting, CORS, and security headers.
4
+ *
5
+ * Guards (in order):
6
+ * 1. Metrics guard - optionally protects /api/metrics with a bearer
7
+ * token. Set METRICS_TOKEN in env to enable.
8
+ * 2. Rate limiting, CORS, security headers for all API routes.
9
+ */
10
+
11
+ import { getAllowedOrigins } from "@techstream/quark-config/app-url";
12
+ import { NextResponse } from "next/server";
13
+
14
+ import { buildContentSecurityPolicy } from "./lib/analytics/umami-csp.js";
15
+ import { getRateLimitBucket } from "./lib/proxy-auth";
16
+
17
+ // ─── 1. Metrics guard ────────────────────────────────────────────────────────
18
+
19
+ function metricsGuard(request) {
20
+ const { pathname } = request.nextUrl;
21
+ if (pathname !== "/api/metrics") return null;
22
+
23
+ // If METRICS_TOKEN is not configured, the endpoint is unprotected (existing
24
+ // behaviour preserved). Set METRICS_TOKEN in env to enable protection.
25
+ const expectedToken = process.env.METRICS_TOKEN;
26
+ if (!expectedToken) return null;
27
+
28
+ const authHeader = request.headers.get("authorization") ?? "";
29
+ const providedToken = authHeader.startsWith("Bearer ")
30
+ ? authHeader.slice(7)
31
+ : (request.headers.get("x-metrics-token") ?? "");
32
+
33
+ if (providedToken !== expectedToken) {
34
+ return NextResponse.json({ error: "Forbidden" }, { status: 403 });
35
+ }
36
+
37
+ return null; // authorised - continue
38
+ }
39
+
40
+ // Simple in-memory rate limiter (use Redis for production)
41
+ const rateLimit = new Map();
42
+
43
+ /**
44
+ * Resolve the client IP for rate-limit keying.
45
+ *
46
+ * `NextRequest.ip` was removed in Next 15, so `request.ip` is always
47
+ * `undefined` here and every caller collapsed onto the literal key
48
+ * "unknown" — one shared bucket for the whole server. That let a single
49
+ * burst of 5 credential posts (the `auth` bucket) lock out sign-in for
50
+ * everyone for 15 minutes.
51
+ *
52
+ * Trust `x-forwarded-for`/`x-real-ip` only when you are actually behind a
53
+ * reverse proxy (Railway, Docker, nginx) that overwrites them; otherwise a
54
+ * client can spoof them to evade the limiter.
55
+ *
56
+ * @param {import("next/server").NextRequest} request
57
+ * @returns {string}
58
+ */
59
+ function getClientIp(request) {
60
+ const forwardedFor = request.headers.get("x-forwarded-for");
61
+ if (forwardedFor) {
62
+ // Left-most entry is the originating client; later entries are proxies.
63
+ const client = forwardedFor
64
+ .split(",")
65
+ .map((value) => value.trim())
66
+ .filter(Boolean)[0];
67
+ if (client) return client;
68
+ }
69
+
70
+ const realIp = request.headers.get("x-real-ip");
71
+ if (realIp) return realIp.trim();
72
+
73
+ return "unknown";
74
+ }
75
+
76
+ const RATE_LIMIT_CONFIG = {
77
+ windowMs: 15 * 60 * 1000, // 15 minutes
78
+ maxRequests: {
79
+ api: 100, // 100 requests per 15 minutes for general API
80
+ auth: 5, // 5 requests per 15 minutes for auth endpoints
81
+ },
82
+ };
83
+
84
+ /**
85
+ * Rate limiting implementation
86
+ */
87
+ function checkRateLimit(ip, path, method) {
88
+ const now = Date.now();
89
+ const key = `${ip}:${path}`;
90
+
91
+ const maxRequests =
92
+ RATE_LIMIT_CONFIG.maxRequests[getRateLimitBucket(path, method)];
93
+
94
+ // Get or create rate limit record
95
+ const record = rateLimit.get(key) || {
96
+ count: 0,
97
+ resetTime: now + RATE_LIMIT_CONFIG.windowMs,
98
+ };
99
+
100
+ // Reset if window has passed
101
+ if (now > record.resetTime) {
102
+ record.count = 0;
103
+ record.resetTime = now + RATE_LIMIT_CONFIG.windowMs;
104
+ }
105
+
106
+ // Check if limit exceeded
107
+ if (record.count >= maxRequests) {
108
+ return {
109
+ limited: true,
110
+ resetTime: record.resetTime,
111
+ remaining: 0,
112
+ };
113
+ }
114
+
115
+ // Increment counter
116
+ record.count++;
117
+ rateLimit.set(key, record);
118
+
119
+ return {
120
+ limited: false,
121
+ resetTime: record.resetTime,
122
+ remaining: maxRequests - record.count,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Clean up old rate limit records periodically.
128
+ * `unref()` so the timer never holds the Node process open on shutdown.
129
+ */
130
+ const rateLimitCleanup = setInterval(() => {
131
+ const now = Date.now();
132
+ for (const [key, record] of rateLimit.entries()) {
133
+ if (now > record.resetTime) {
134
+ rateLimit.delete(key);
135
+ }
136
+ }
137
+ }, 60 * 1000); // Clean up every minute
138
+ if (typeof rateLimitCleanup.unref === "function") rateLimitCleanup.unref();
139
+
140
+ /**
141
+ * CORS configuration
142
+ */
143
+ const CORS_CONFIG = {
144
+ allowedOrigins: getAllowedOrigins(),
145
+ allowedMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
146
+ allowedHeaders: ["Content-Type", "Authorization", "X-CSRF-Token"],
147
+ exposedHeaders: [
148
+ "X-RateLimit-Limit",
149
+ "X-RateLimit-Remaining",
150
+ "X-RateLimit-Reset",
151
+ ],
152
+ credentials: true,
153
+ maxAge: 86400, // 24 hours
154
+ };
155
+
156
+ /**
157
+ * Security headers configuration
158
+ *
159
+ * In development, Turbopack's hot-reload runtime uses eval() for module
160
+ * evaluation. 'unsafe-eval' is therefore added to script-src only when
161
+ * NODE_ENV is not 'production' - it must never reach a production build.
162
+ */
163
+ const SECURITY_HEADERS = {
164
+ "X-DNS-Prefetch-Control": "on",
165
+ "Strict-Transport-Security": "max-age=63072000; includeSubDomains",
166
+ "X-Frame-Options": "SAMEORIGIN",
167
+ "X-Content-Type-Options": "nosniff",
168
+ "Referrer-Policy": "strict-origin-when-cross-origin",
169
+ "Permissions-Policy": "camera=(), microphone=(), geolocation=()",
170
+ "Content-Security-Policy": buildContentSecurityPolicy(),
171
+ };
172
+
173
+ /**
174
+ * Request size limits (in bytes)
175
+ */
176
+ const REQUEST_SIZE_LIMITS = {
177
+ api: parseInt(process.env.API_BODY_SIZE_LIMIT || "2097152", 10), // 2MB default
178
+ upload: parseInt(process.env.UPLOAD_SIZE_LIMIT || "10485760", 10), // 10MB for uploads
179
+ };
180
+
181
+ export async function proxy(request) {
182
+ const metricsResponse = metricsGuard(request);
183
+ if (metricsResponse) return metricsResponse;
184
+
185
+ const { pathname } = request.nextUrl;
186
+ const origin = request.headers.get("origin") || "";
187
+
188
+ // Create response
189
+ const response = NextResponse.next();
190
+
191
+ // Check request body size for API routes
192
+ if (
193
+ pathname.startsWith("/api/") &&
194
+ ["POST", "PUT", "PATCH"].includes(request.method)
195
+ ) {
196
+ const contentLength = request.headers.get("content-length");
197
+ if (contentLength) {
198
+ const size = parseInt(contentLength, 10);
199
+ const limit = pathname.startsWith("/api/upload")
200
+ ? REQUEST_SIZE_LIMITS.upload
201
+ : REQUEST_SIZE_LIMITS.api;
202
+
203
+ if (size > limit) {
204
+ return new NextResponse(
205
+ JSON.stringify({
206
+ error: "Payload too large",
207
+ message: `Request body size exceeds limit of ${Math.round(limit / 1024 / 1024)}MB`,
208
+ maxSize: limit,
209
+ }),
210
+ {
211
+ status: 413,
212
+ headers: {
213
+ "Content-Type": "application/json",
214
+ },
215
+ },
216
+ );
217
+ }
218
+ }
219
+ }
220
+
221
+ // Apply rate limiting to API routes only
222
+ if (pathname.startsWith("/api/")) {
223
+ const ip = getClientIp(request);
224
+ const rateLimitBucket = getRateLimitBucket(pathname, request.method);
225
+ const maxRequests = RATE_LIMIT_CONFIG.maxRequests[rateLimitBucket];
226
+ const rateLimitResult = checkRateLimit(ip, pathname, request.method);
227
+
228
+ if (rateLimitResult.limited) {
229
+ const retryAfter = Math.ceil(
230
+ (rateLimitResult.resetTime - Date.now()) / 1000,
231
+ );
232
+ return new NextResponse(
233
+ JSON.stringify({
234
+ error: "Too many requests",
235
+ message: "You have exceeded the rate limit. Please try again later.",
236
+ retryAfter,
237
+ }),
238
+ {
239
+ status: 429,
240
+ headers: {
241
+ "Content-Type": "application/json",
242
+ "Retry-After": retryAfter.toString(),
243
+ "X-RateLimit-Limit": maxRequests.toString(),
244
+ "X-RateLimit-Remaining": "0",
245
+ "X-RateLimit-Reset": new Date(
246
+ rateLimitResult.resetTime,
247
+ ).toISOString(),
248
+ },
249
+ },
250
+ );
251
+ }
252
+
253
+ // Add rate limit headers to response
254
+ response.headers.set("X-RateLimit-Limit", maxRequests.toString());
255
+ response.headers.set(
256
+ "X-RateLimit-Remaining",
257
+ rateLimitResult.remaining.toString(),
258
+ );
259
+ response.headers.set(
260
+ "X-RateLimit-Reset",
261
+ new Date(rateLimitResult.resetTime).toISOString(),
262
+ );
263
+ }
264
+
265
+ // Handle CORS for API routes
266
+ if (pathname.startsWith("/api/")) {
267
+ // Check if origin is allowed
268
+ const isAllowedOrigin =
269
+ CORS_CONFIG.allowedOrigins.includes("*") ||
270
+ CORS_CONFIG.allowedOrigins.includes(origin);
271
+
272
+ if (isAllowedOrigin || !origin) {
273
+ response.headers.set(
274
+ "Access-Control-Allow-Origin",
275
+ origin || CORS_CONFIG.allowedOrigins[0],
276
+ );
277
+ response.headers.set(
278
+ "Access-Control-Allow-Methods",
279
+ CORS_CONFIG.allowedMethods.join(", "),
280
+ );
281
+ response.headers.set(
282
+ "Access-Control-Allow-Headers",
283
+ CORS_CONFIG.allowedHeaders.join(", "),
284
+ );
285
+ response.headers.set(
286
+ "Access-Control-Expose-Headers",
287
+ CORS_CONFIG.exposedHeaders.join(", "),
288
+ );
289
+ response.headers.set(
290
+ "Access-Control-Max-Age",
291
+ CORS_CONFIG.maxAge.toString(),
292
+ );
293
+
294
+ if (CORS_CONFIG.credentials) {
295
+ response.headers.set("Access-Control-Allow-Credentials", "true");
296
+ }
297
+ }
298
+
299
+ // Handle preflight requests
300
+ if (request.method === "OPTIONS") {
301
+ return new NextResponse(null, {
302
+ status: 204,
303
+ headers: response.headers,
304
+ });
305
+ }
306
+ }
307
+
308
+ // Apply security headers
309
+ for (const [key, value] of Object.entries(SECURITY_HEADERS)) {
310
+ response.headers.set(key, value);
311
+ }
312
+
313
+ return response;
314
+ }
315
+
316
+ // Configure which routes the proxy runs on
317
+ export const config = {
318
+ matcher: [
319
+ /*
320
+ * Match all request paths except:
321
+ * - _next/static (static files)
322
+ * - _next/image (image optimization files)
323
+ * - favicon.ico (favicon file)
324
+ * - public folder
325
+ */
326
+ "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
327
+ ],
328
+ };
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false,
10
+ "includes": [
11
+ "**",
12
+ "!**/.next",
13
+ "!**/dist",
14
+ "!**/.turbo",
15
+ "!**/coverage",
16
+ "!packages/db/src/generated"
17
+ ]
18
+ },
19
+ "formatter": {
20
+ "enabled": true,
21
+ "indentStyle": "tab"
22
+ },
23
+ "linter": {
24
+ "enabled": true,
25
+ "rules": {
26
+ "preset": "recommended"
27
+ }
28
+ },
29
+ "javascript": {
30
+ "formatter": {
31
+ "quoteStyle": "double"
32
+ }
33
+ },
34
+ "css": {
35
+ "parser": {
36
+ "cssModules": true,
37
+ "tailwindDirectives": true
38
+ },
39
+ "linter": {
40
+ "enabled": true
41
+ }
42
+ },
43
+ "assist": {
44
+ "enabled": true,
45
+ "actions": {
46
+ "source": {
47
+ "organizeImports": "on"
48
+ }
49
+ }
50
+ },
51
+ "root": false
52
+ }
@@ -0,0 +1,17 @@
1
+ # Dev-only services - auto-merged by `docker compose up`.
2
+ # Not read by Railway, Kubernetes, or other deployment platforms.
3
+ services:
4
+ # --- Mailpit (Local SMTP Server) ---
5
+ mailpit:
6
+ image: ghcr.io/axllent/mailpit
7
+ restart: always
8
+ ports:
9
+ # SMTP port (used by application to send mail)
10
+ - "${MAIL_SMTP_PORT:-1025}:1025"
11
+ # Web UI port (to view sent emails)
12
+ - "${MAIL_UI_PORT:-8025}:8025"
13
+ healthcheck:
14
+ test: ["CMD", "wget", "--spider", "-q", "http://localhost:8025"]
15
+ interval: 10s
16
+ timeout: 5s
17
+ retries: 3
@@ -0,0 +1,37 @@
1
+ services:
2
+ # --- 1. PostgreSQL Database ---
3
+ postgres:
4
+ image: postgres:16-alpine
5
+ restart: always
6
+ ports:
7
+ - "${POSTGRES_PORT:-5432}:5432"
8
+ environment:
9
+ POSTGRES_USER: ${POSTGRES_USER:-quark_user}
10
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-CHANGE_ME_IN_ENV_FILE}
11
+ POSTGRES_DB: ${POSTGRES_DB:-quark_dev}
12
+ volumes:
13
+ - postgres_data:/var/lib/postgresql/data
14
+ healthcheck:
15
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-quark_user}"]
16
+ interval: 5s
17
+ timeout: 5s
18
+ retries: 5
19
+
20
+ # --- 2. Redis Cache & Job Queue ---
21
+ redis:
22
+ image: redis:7-alpine
23
+ restart: always
24
+ ports:
25
+ - "${REDIS_PORT:-6379}:6379"
26
+ command: redis-server --appendonly yes
27
+ volumes:
28
+ - redis_data:/data
29
+ healthcheck:
30
+ test: ["CMD", "redis-cli", "ping"]
31
+ interval: 5s
32
+ timeout: 5s
33
+ retries: 5
34
+
35
+ volumes:
36
+ postgres_data:
37
+ redis_data:
@@ -0,0 +1,74 @@
1
+ # First Feature
2
+
3
+ Use this guide when you are ready to add the first real feature to your project.
4
+
5
+ ## Goal
6
+
7
+ Prove the full workflow once:
8
+
9
+ 1. add a Prisma model
10
+ 2. run the migration
11
+ 3. add query helpers
12
+ 4. build a page or mutation flow
13
+ 5. add a test near the changed code
14
+
15
+ ## 1. Add the model
16
+
17
+ Edit `packages/db/prisma/schema.prisma`.
18
+
19
+ Every model should include:
20
+
21
+ - `id String @id @default(cuid())`
22
+ - `createdAt DateTime @default(now())`
23
+ - `updatedAt DateTime @updatedAt`
24
+
25
+ If the new data belongs to a signed-in user, relate it to `User`.
26
+
27
+ ## 2. Generate and migrate
28
+
29
+ ```bash
30
+ pnpm db:generate
31
+ pnpm db:migrate
32
+ ```
33
+
34
+ ## 3. Add query helpers
35
+
36
+ Edit `packages/db/src/queries.js`.
37
+
38
+ Start with the smallest useful surface:
39
+
40
+ - `findAll`
41
+ - `findById`
42
+ - `create`
43
+
44
+ Add `update` and `delete` when the feature actually needs them.
45
+
46
+ ## 4. Build the web flow
47
+
48
+ Use `apps/web/src/app/` for the first route.
49
+
50
+ Typical first files:
51
+
52
+ - `apps/web/src/app/<feature>/page.js`
53
+ - `apps/web/src/app/<feature>/_actions/*.js` for Server Actions
54
+ - `apps/web/src/app/api/<feature>/route.js` if this feature needs an API surface
55
+
56
+ If the project includes `ui`, use components from `@yourapp/ui`.
57
+
58
+ ## 5. Add one test
59
+
60
+ Start in the layer you changed first:
61
+
62
+ - query helper test
63
+ - route test
64
+ - form helper test
65
+
66
+ Do not wait for a perfect end-to-end suite before writing the first useful test.
67
+
68
+ ## Optional next step
69
+
70
+ If the feature needs more infrastructure, add it only when necessary:
71
+
72
+ - `ui` for reusable components
73
+ - `jobs` for async work
74
+ - `cms` for editorial content workflows
@@ -0,0 +1,70 @@
1
+ # Start Here
2
+
3
+ This is the shortest current path through a newly scaffolded Quark project.
4
+
5
+ ## Read these first
6
+
7
+ 1. `README.md` - quick start, included features, and first files to edit
8
+ 2. `docs/FIRST_FEATURE.md` - the canonical first product feature walkthrough
9
+ 3. `CLAUDE.md` - AI tool context for code generation and refactors
10
+
11
+ ## Know the project contract
12
+
13
+ Every Quark scaffold gives you:
14
+
15
+ - `apps/web` - the Next.js app
16
+ - `packages/db` - Prisma schema, client, and query helpers
17
+ - `packages/config` - validated environment config
18
+ - optional local packages like `ui` and `jobs`
19
+
20
+ The package READMEs explain optional features if they are present:
21
+
22
+ - `packages/ui/README.md`
23
+ - `packages/jobs/README.md`
24
+ - `packages/cms/README.md`
25
+
26
+ ## Best order to learn the codebase
27
+
28
+ 1. `packages/db/prisma/schema.prisma`
29
+ 2. `packages/db/src/queries.js`
30
+ 3. `apps/web/src/app/`
31
+ 4. `apps/web/src/lib/auth.js`
32
+ 5. `apps/worker/src/handlers/` if your project includes background jobs
33
+
34
+ ## Build next
35
+
36
+ Follow `docs/FIRST_FEATURE.md` and ship one thin vertical slice:
37
+
38
+ 1. add a model
39
+ 2. migrate the database
40
+ 3. add query helpers
41
+ 4. build the page or mutation
42
+ 5. add one focused test
43
+
44
+ That is the fastest way to move from "starter" to "product code."
45
+
46
+ ## Keep your scaffold visible
47
+
48
+ Before taking Quark infrastructure updates, review scaffold-managed drift from the project root:
49
+
50
+ ```bash
51
+ npx @techstream/quark-create-app update --scaffold-check
52
+ ```
53
+
54
+ If you want CI to fail when scaffold-managed files drift, use:
55
+
56
+ ```bash
57
+ npx @techstream/quark-create-app update --scaffold-check --fail-on-drift
58
+ ```
59
+
60
+ ## Recover disk space
61
+
62
+ When local build artifacts pile up, run these from the project root:
63
+
64
+ ```bash
65
+ pnpm clean:check # Preview reclaimable build artifacts
66
+ pnpm clean # Remove .turbo, .next, build, dist, and coverage output
67
+ pnpm clean:deep # Also remove repo-local temp workspaces
68
+ ```
69
+
70
+ `pnpm dev` also auto-cleans stale oversized local artifacts at most once per day. Set `QUARK_SKIP_AUTO_CLEAN=1` if you want to skip that behavior for a run.
@@ -0,0 +1,42 @@
1
+ # Architecture Decision Records
2
+
3
+ This directory contains Architecture Decision Records (ADRs) for __QUARK_PROJECT_NAME__.
4
+
5
+ An ADR documents a significant architectural decision: the context, the decision made, and its consequences.
6
+
7
+ ## Index
8
+
9
+ | ID | Title | Status |
10
+ |----|-------|--------|
11
+
12
+ <!-- Add entries here as you make architectural decisions. -->
13
+ <!-- Example: | [ADR-001](./001-example-decision.md) | Why we chose X over Y | Accepted | -->
14
+
15
+ ## Format
16
+
17
+ Each ADR uses this structure:
18
+
19
+ - **Status:** Accepted / Superseded / Deprecated
20
+ - **Context:** Why a decision was needed
21
+ - **Decision:** What was decided
22
+ - **Consequences:** Trade-offs and implications
23
+
24
+ ## Template
25
+
26
+ ```markdown
27
+ # ADR-XXX: Title
28
+
29
+ **Status:** Accepted
30
+
31
+ ## Context
32
+
33
+ Why was this decision needed?
34
+
35
+ ## Decision
36
+
37
+ What was decided?
38
+
39
+ ## Consequences
40
+
41
+ What are the trade-offs?
42
+ ```
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@myquark/root",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "description": "My Quark project",
7
+ "main": "index.js",
8
+ "scripts": {
9
+ "clean": "node scripts/clean-workspace.mjs",
10
+ "clean:check": "node scripts/clean-workspace.mjs --check",
11
+ "clean:deep": "node scripts/clean-workspace.mjs --deep",
12
+ "build": "dotenv -- turbo run build",
13
+ "dev": "dotenv -- node scripts/dev-preflight.mjs && dotenv -- turbo run dev",
14
+ "dev:mobile": "pnpm --filter @myquark/mobile start",
15
+ "build:mobile": "pnpm --filter @myquark/mobile export",
16
+ "lint": "turbo run lint",
17
+ "standards": "node scripts/check-standards.mjs",
18
+ "check:loading": "node scripts/check-loading.mjs",
19
+ "test": "turbo run test",
20
+ "docker:up": "docker compose up -d",
21
+ "docker:down": "docker compose down",
22
+ "docker:clean": "docker compose down -v",
23
+ "db:generate": "turbo run db:generate",
24
+ "db:migrate": "pnpm --filter ./packages/db db:migrate",
25
+ "db:migrate:deploy": "pnpm --filter ./packages/db db:migrate:deploy",
26
+ "db:push": "turbo run db:push",
27
+ "db:seed": "turbo run db:seed",
28
+ "db:studio": "turbo run db:studio",
29
+ "doctor": "node scripts/doctor.js",
30
+ "doctor:fix": "node scripts/doctor.js --fix",
31
+ "doctor:ci": "node scripts/doctor.js --ci",
32
+ "prepare": "node scripts/prepare.js"
33
+ },
34
+ "simple-git-hooks": {
35
+ "pre-commit": "pnpm nano-staged",
36
+ "pre-push": "pnpm check:loading && pnpm test"
37
+ },
38
+ "nano-staged": {
39
+ "**/*.{js,jsx,mjs,json,css}": [
40
+ "biome format --no-errors-on-unmatched --write",
41
+ "biome check --no-errors-on-unmatched --write"
42
+ ]
43
+ },
44
+ "keywords": [],
45
+ "author": "",
46
+ "license": "MIT",
47
+ "packageManager": "pnpm@10.12.1",
48
+ "devDependencies": {
49
+ "@biomejs/biome": "^2.4.0",
50
+ "@types/node": "^24.10.9",
51
+ "dotenv-cli": "^11.0.0",
52
+ "nano-staged": "^0.9.0",
53
+ "simple-git-hooks": "^2.13.1",
54
+ "tsx": "^4.21.0",
55
+ "turbo": "^2.8.1"
56
+ },
57
+ "pnpm": {}
58
+ }