@prmichaelsen/acp-visualizer 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 (180) hide show
  1. package/README.md +68 -0
  2. package/agent/commands/acp.clarification-address.md +417 -0
  3. package/agent/commands/acp.clarification-capture.md +386 -0
  4. package/agent/commands/acp.clarification-create.md +437 -0
  5. package/agent/commands/acp.clarifications-research.md +326 -0
  6. package/agent/commands/acp.command-create.md +432 -0
  7. package/agent/commands/acp.design-create.md +286 -0
  8. package/agent/commands/acp.design-reference.md +355 -0
  9. package/agent/commands/acp.handoff.md +270 -0
  10. package/agent/commands/acp.index.md +423 -0
  11. package/agent/commands/acp.init.md +546 -0
  12. package/agent/commands/acp.package-create.md +895 -0
  13. package/agent/commands/acp.package-info.md +212 -0
  14. package/agent/commands/acp.package-install.md +539 -0
  15. package/agent/commands/acp.package-list.md +280 -0
  16. package/agent/commands/acp.package-publish.md +541 -0
  17. package/agent/commands/acp.package-remove.md +293 -0
  18. package/agent/commands/acp.package-search.md +307 -0
  19. package/agent/commands/acp.package-update.md +361 -0
  20. package/agent/commands/acp.package-validate.md +540 -0
  21. package/agent/commands/acp.pattern-create.md +386 -0
  22. package/agent/commands/acp.plan.md +587 -0
  23. package/agent/commands/acp.proceed.md +882 -0
  24. package/agent/commands/acp.project-create.md +675 -0
  25. package/agent/commands/acp.project-info.md +312 -0
  26. package/agent/commands/acp.project-list.md +226 -0
  27. package/agent/commands/acp.project-remove.md +379 -0
  28. package/agent/commands/acp.project-set.md +227 -0
  29. package/agent/commands/acp.project-update.md +307 -0
  30. package/agent/commands/acp.projects-restore.md +228 -0
  31. package/agent/commands/acp.projects-sync.md +347 -0
  32. package/agent/commands/acp.report.md +407 -0
  33. package/agent/commands/acp.resume.md +239 -0
  34. package/agent/commands/acp.sessions.md +301 -0
  35. package/agent/commands/acp.status.md +293 -0
  36. package/agent/commands/acp.sync.md +364 -0
  37. package/agent/commands/acp.task-create.md +500 -0
  38. package/agent/commands/acp.update.md +302 -0
  39. package/agent/commands/acp.validate.md +466 -0
  40. package/agent/commands/acp.version-check-for-updates.md +276 -0
  41. package/agent/commands/acp.version-check.md +191 -0
  42. package/agent/commands/acp.version-update.md +289 -0
  43. package/agent/commands/command.template.md +339 -0
  44. package/agent/commands/git.commit.md +526 -0
  45. package/agent/commands/git.init.md +514 -0
  46. package/agent/commands/tanstack-cloudflare.deploy.md +272 -0
  47. package/agent/commands/tanstack-cloudflare.tail.md +275 -0
  48. package/agent/design/.gitkeep +0 -0
  49. package/agent/design/design.template.md +154 -0
  50. package/agent/design/local.dashboard-layout-routing.md +288 -0
  51. package/agent/design/local.data-model-yaml-parsing.md +310 -0
  52. package/agent/design/local.search-filtering.md +331 -0
  53. package/agent/design/local.server-api-auto-refresh.md +235 -0
  54. package/agent/design/local.table-tree-views.md +299 -0
  55. package/agent/design/local.visualizer-requirements.md +349 -0
  56. package/agent/design/requirements.template.md +387 -0
  57. package/agent/index/.gitkeep +0 -0
  58. package/agent/index/acp.core.yaml +137 -0
  59. package/agent/index/local.main.template.yaml +37 -0
  60. package/agent/manifest.template.yaml +13 -0
  61. package/agent/manifest.yaml +302 -0
  62. package/agent/milestones/.gitkeep +0 -0
  63. package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +67 -0
  64. package/agent/milestones/milestone-1-{title}.template.md +206 -0
  65. package/agent/milestones/milestone-2-dashboard-views-interaction.md +79 -0
  66. package/agent/package.template.yaml +86 -0
  67. package/agent/patterns/.gitkeep +0 -0
  68. package/agent/patterns/bootstrap.template.md +1237 -0
  69. package/agent/patterns/pattern.template.md +382 -0
  70. package/agent/patterns/tanstack-cloudflare.acl-permissions.md +332 -0
  71. package/agent/patterns/tanstack-cloudflare.action-bar-item.md +416 -0
  72. package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +401 -0
  73. package/agent/patterns/tanstack-cloudflare.auth-session-management.md +387 -0
  74. package/agent/patterns/tanstack-cloudflare.card-and-list.md +271 -0
  75. package/agent/patterns/tanstack-cloudflare.chat-engine.md +353 -0
  76. package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +346 -0
  77. package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +516 -0
  78. package/agent/patterns/tanstack-cloudflare.email-service.md +431 -0
  79. package/agent/patterns/tanstack-cloudflare.expander.md +98 -0
  80. package/agent/patterns/tanstack-cloudflare.fcm-push.md +115 -0
  81. package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +441 -0
  82. package/agent/patterns/tanstack-cloudflare.firebase-auth.md +348 -0
  83. package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +550 -0
  84. package/agent/patterns/tanstack-cloudflare.firebase-storage.md +369 -0
  85. package/agent/patterns/tanstack-cloudflare.form-controls.md +145 -0
  86. package/agent/patterns/tanstack-cloudflare.global-search-context.md +93 -0
  87. package/agent/patterns/tanstack-cloudflare.image-carousel.md +126 -0
  88. package/agent/patterns/tanstack-cloudflare.library-services.md +553 -0
  89. package/agent/patterns/tanstack-cloudflare.lightbox.md +169 -0
  90. package/agent/patterns/tanstack-cloudflare.markdown-content.md +115 -0
  91. package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +98 -0
  92. package/agent/patterns/tanstack-cloudflare.modal.md +156 -0
  93. package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +461 -0
  94. package/agent/patterns/tanstack-cloudflare.notifications-engine.md +151 -0
  95. package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +90 -0
  96. package/agent/patterns/tanstack-cloudflare.og-metadata.md +296 -0
  97. package/agent/patterns/tanstack-cloudflare.pagination.md +442 -0
  98. package/agent/patterns/tanstack-cloudflare.pill-input.md +220 -0
  99. package/agent/patterns/tanstack-cloudflare.provider-adapter.md +401 -0
  100. package/agent/patterns/tanstack-cloudflare.rate-limiting.md +323 -0
  101. package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +338 -0
  102. package/agent/patterns/tanstack-cloudflare.searchable-settings.md +375 -0
  103. package/agent/patterns/tanstack-cloudflare.slide-over.md +129 -0
  104. package/agent/patterns/tanstack-cloudflare.ssr-preload.md +571 -0
  105. package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +508 -0
  106. package/agent/patterns/tanstack-cloudflare.toast-system.md +142 -0
  107. package/agent/patterns/tanstack-cloudflare.unified-header.md +280 -0
  108. package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +628 -0
  109. package/agent/patterns/tanstack-cloudflare.websocket-manager.md +237 -0
  110. package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +358 -0
  111. package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +336 -0
  112. package/agent/progress.template.yaml +161 -0
  113. package/agent/progress.yaml +145 -0
  114. package/agent/schemas/package.schema.yaml +276 -0
  115. package/agent/scripts/acp.common.sh +1781 -0
  116. package/agent/scripts/acp.install.sh +333 -0
  117. package/agent/scripts/acp.package-create.sh +924 -0
  118. package/agent/scripts/acp.package-info.sh +288 -0
  119. package/agent/scripts/acp.package-install.sh +893 -0
  120. package/agent/scripts/acp.package-list.sh +311 -0
  121. package/agent/scripts/acp.package-publish.sh +420 -0
  122. package/agent/scripts/acp.package-remove.sh +348 -0
  123. package/agent/scripts/acp.package-search.sh +156 -0
  124. package/agent/scripts/acp.package-update.sh +517 -0
  125. package/agent/scripts/acp.package-validate.sh +1018 -0
  126. package/agent/scripts/acp.uninstall.sh +85 -0
  127. package/agent/scripts/acp.version-check-for-updates.sh +98 -0
  128. package/agent/scripts/acp.version-check.sh +47 -0
  129. package/agent/scripts/acp.version-update.sh +176 -0
  130. package/agent/scripts/acp.yaml-parser.sh +985 -0
  131. package/agent/scripts/acp.yaml-validate.sh +205 -0
  132. package/agent/tasks/.gitkeep +0 -0
  133. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +210 -0
  134. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +294 -0
  135. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +193 -0
  136. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +262 -0
  137. package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +156 -0
  138. package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +178 -0
  139. package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +141 -0
  140. package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +153 -0
  141. package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +174 -0
  142. package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +233 -0
  143. package/agent/tasks/task-1-{title}.template.md +244 -0
  144. package/bin/visualize.mjs +84 -0
  145. package/package.json +48 -0
  146. package/src/components/ExtraFieldsBadge.tsx +15 -0
  147. package/src/components/FilterBar.tsx +33 -0
  148. package/src/components/Header.tsx +23 -0
  149. package/src/components/MilestoneTable.tsx +167 -0
  150. package/src/components/MilestoneTree.tsx +84 -0
  151. package/src/components/ProgressBar.tsx +20 -0
  152. package/src/components/SearchInput.tsx +22 -0
  153. package/src/components/Sidebar.tsx +54 -0
  154. package/src/components/StatusBadge.tsx +23 -0
  155. package/src/components/StatusDot.tsx +12 -0
  156. package/src/components/TaskList.tsx +36 -0
  157. package/src/components/ViewToggle.tsx +31 -0
  158. package/src/lib/config.ts +8 -0
  159. package/src/lib/file-watcher.ts +43 -0
  160. package/src/lib/search.ts +48 -0
  161. package/src/lib/types.ts +73 -0
  162. package/src/lib/useAutoRefresh.ts +31 -0
  163. package/src/lib/useCollapse.ts +31 -0
  164. package/src/lib/useFilteredData.ts +55 -0
  165. package/src/lib/yaml-loader-real.spec.ts +47 -0
  166. package/src/lib/yaml-loader.spec.ts +201 -0
  167. package/src/lib/yaml-loader.ts +265 -0
  168. package/src/routeTree.gen.ts +140 -0
  169. package/src/router.tsx +10 -0
  170. package/src/routes/__root.tsx +75 -0
  171. package/src/routes/api/watch.ts +29 -0
  172. package/src/routes/index.tsx +115 -0
  173. package/src/routes/milestones.tsx +50 -0
  174. package/src/routes/search.tsx +84 -0
  175. package/src/routes/tasks.tsx +63 -0
  176. package/src/services/progress-database.service.ts +46 -0
  177. package/src/styles.css +25 -0
  178. package/tsconfig.json +24 -0
  179. package/vite.config.ts +16 -0
  180. package/vitest.config.ts +27 -0
@@ -0,0 +1,323 @@
1
+ # Rate Limiting Pattern
2
+
3
+ **Category**: Infrastructure
4
+ **Applicable To**: TanStack Start + Cloudflare Workers applications requiring request throttling
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ Cloudflare Workers provides a built-in Rate Limiting API via `unsafe.bindings` in `wrangler.toml`. This pattern documents how to configure rate limiting namespaces for different endpoint categories (auth, API, WebSocket), create a reusable rate limiting utility, and enforce limits in API route handlers.
12
+
13
+ Rate limiters are configured per-namespace with distinct limits — strict limits for auth endpoints (prevent brute force), moderate limits for API endpoints, and connection-based limits for WebSocket upgrades.
14
+
15
+ ---
16
+
17
+ ## When to Use This Pattern
18
+
19
+ ✅ **Use this pattern when:**
20
+ - Need to protect auth endpoints from brute force attacks
21
+ - Want to prevent API abuse
22
+ - Need to limit WebSocket connection frequency
23
+ - Building production applications exposed to the internet
24
+
25
+ ❌ **Don't use this pattern when:**
26
+ - Building internal-only applications with trusted clients
27
+ - Development/testing environments (rate limiting adds friction)
28
+ - Using an external rate limiting service (Cloudflare WAF rules, etc.)
29
+
30
+ ---
31
+
32
+ ## Core Principles
33
+
34
+ 1. **Namespace Separation**: Different rate limit tiers for different endpoint categories
35
+ 2. **Fail Open**: If the rate limiter errors, allow the request (don't block users due to infra issues)
36
+ 3. **User-Based Keys**: Authenticated requests are rate-limited by user ID, unauthenticated by IP
37
+ 4. **Standard Headers**: Return `Retry-After`, `X-RateLimit-Limit`, and `X-RateLimit-Remaining` headers
38
+ 5. **429 Response**: Consistently return HTTP 429 with JSON error body when rate limited
39
+
40
+ ---
41
+
42
+ ## Implementation
43
+
44
+ ### Step 1: Configure Wrangler
45
+
46
+ ```toml
47
+ # wrangler.toml
48
+
49
+ # Rate Limiting (Cloudflare Workers Rate Limiting API)
50
+ # namespace_id must be a string containing a positive integer
51
+
52
+ [[unsafe.bindings]]
53
+ name = "AUTH_RATE_LIMITER"
54
+ type = "ratelimit"
55
+ namespace_id = "1001"
56
+ simple = { limit = 5, period = 60 } # 5 attempts per minute
57
+
58
+ [[unsafe.bindings]]
59
+ name = "API_RATE_LIMITER"
60
+ type = "ratelimit"
61
+ namespace_id = "1002"
62
+ simple = { limit = 100, period = 60 } # 100 requests per minute
63
+
64
+ [[unsafe.bindings]]
65
+ name = "WS_RATE_LIMITER"
66
+ type = "ratelimit"
67
+ namespace_id = "1003"
68
+ simple = { limit = 10, period = 60 } # 10 connections per minute
69
+ ```
70
+
71
+ ### Step 2: Rate Limiting Utility
72
+
73
+ ```typescript
74
+ // src/lib/rate-limiter.ts
75
+
76
+ export interface RateLimitConfig {
77
+ limit: number
78
+ period: number // seconds
79
+ keyPrefix: string
80
+ }
81
+
82
+ export interface RateLimitResult {
83
+ success: boolean
84
+ limit: number
85
+ remaining: number
86
+ retryAfter?: number
87
+ }
88
+
89
+ /**
90
+ * Check rate limit for a request
91
+ */
92
+ export async function checkRateLimit(
93
+ rateLimiter: any, // Cloudflare Rate Limiter binding
94
+ identifier: string,
95
+ config: RateLimitConfig
96
+ ): Promise<RateLimitResult> {
97
+ const key = `${config.keyPrefix}:${identifier}`
98
+
99
+ try {
100
+ const { success, limit, remaining, retryAfter } = await rateLimiter.limit({ key })
101
+
102
+ return { success, limit, remaining, retryAfter }
103
+ } catch (error) {
104
+ console.error('[RateLimit] Error checking rate limit:', error)
105
+ // Fail open — allow request if rate limiter fails
106
+ return {
107
+ success: true,
108
+ limit: config.limit,
109
+ remaining: config.limit
110
+ }
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Create rate limit error response with standard headers
116
+ */
117
+ export function createRateLimitResponse(result: RateLimitResult): Response {
118
+ const retryAfter = result.retryAfter ?? 60
119
+ const limit = result.limit ?? 100
120
+ const remaining = result.remaining ?? 0
121
+
122
+ return new Response(
123
+ JSON.stringify({
124
+ error: 'Too many requests',
125
+ message: 'Rate limit exceeded. Please try again later.',
126
+ retryAfter
127
+ }),
128
+ {
129
+ status: 429,
130
+ headers: {
131
+ 'Content-Type': 'application/json',
132
+ 'Retry-After': retryAfter.toString(),
133
+ 'X-RateLimit-Limit': limit.toString(),
134
+ 'X-RateLimit-Remaining': remaining.toString()
135
+ }
136
+ }
137
+ )
138
+ }
139
+
140
+ /**
141
+ * Get rate limit identifier from request.
142
+ * Uses user ID if authenticated, IP address otherwise.
143
+ */
144
+ export function getRateLimitIdentifier(request: Request, userId?: string): string {
145
+ if (userId) return `user:${userId}`
146
+
147
+ const ip = request.headers.get('cf-connecting-ip') ||
148
+ request.headers.get('x-forwarded-for') ||
149
+ 'unknown'
150
+
151
+ return `ip:${ip}`
152
+ }
153
+ ```
154
+
155
+ ### Step 3: Use in API Route
156
+
157
+ ```typescript
158
+ // src/routes/api/auth/session.tsx
159
+ import { createFileRoute } from '@tanstack/react-router'
160
+ import { checkRateLimit, createRateLimitResponse, getRateLimitIdentifier } from '@/lib/rate-limiter'
161
+
162
+ export const Route = createFileRoute('/api/auth/session')({
163
+ server: {
164
+ handlers: {
165
+ POST: async ({ request, context }) => {
166
+ const env = context.cloudflare.env as Env
167
+
168
+ // Rate limit auth endpoints strictly
169
+ const identifier = getRateLimitIdentifier(request)
170
+ const rateLimitResult = await checkRateLimit(
171
+ env.AUTH_RATE_LIMITER,
172
+ identifier,
173
+ { limit: 5, period: 60, keyPrefix: 'auth:session' }
174
+ )
175
+
176
+ if (!rateLimitResult.success) {
177
+ return createRateLimitResponse(rateLimitResult)
178
+ }
179
+
180
+ // ... handle session creation
181
+ },
182
+ },
183
+ },
184
+ })
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Rate Limit Tiers
190
+
191
+ | Namespace | Binding | Limit | Use Case |
192
+ |-----------|---------|-------|----------|
193
+ | Auth | `AUTH_RATE_LIMITER` | 5/min | Login, register, password reset |
194
+ | API | `API_RATE_LIMITER` | 100/min | CRUD operations, data queries |
195
+ | WebSocket | `WS_RATE_LIMITER` | 10/min | WebSocket connection upgrades |
196
+
197
+ ---
198
+
199
+ ## Examples
200
+
201
+ ### Example 1: Rate Limiting API Endpoints
202
+
203
+ ```typescript
204
+ // src/routes/api/conversations/create.tsx
205
+ POST: async ({ request, context }) => {
206
+ const env = context.cloudflare.env as Env
207
+ const user = await getAuthSession()
208
+ if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 })
209
+
210
+ const identifier = getRateLimitIdentifier(request, user.uid)
211
+ const result = await checkRateLimit(
212
+ env.API_RATE_LIMITER,
213
+ identifier,
214
+ { limit: 100, period: 60, keyPrefix: 'api:conversations' }
215
+ )
216
+
217
+ if (!result.success) return createRateLimitResponse(result)
218
+
219
+ // ... handle request
220
+ }
221
+ ```
222
+
223
+ ### Example 2: Rate Limiting WebSocket Connections
224
+
225
+ ```typescript
226
+ // src/routes/api/chat-ws.tsx
227
+ GET: async ({ request, context }) => {
228
+ const env = context.cloudflare.env as Env
229
+ const user = await getAuthSession()
230
+ if (!user) return new Response('Unauthorized', { status: 401 })
231
+
232
+ const identifier = getRateLimitIdentifier(request, user.uid)
233
+ const result = await checkRateLimit(
234
+ env.WS_RATE_LIMITER,
235
+ identifier,
236
+ { limit: 10, period: 60, keyPrefix: 'ws:chat' }
237
+ )
238
+
239
+ if (!result.success) return createRateLimitResponse(result)
240
+
241
+ // Forward to Durable Object for WebSocket upgrade
242
+ const id = env.CHAT_ROOM.idFromName(user.uid)
243
+ return env.CHAT_ROOM.get(id).fetch(request)
244
+ }
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Benefits
250
+
251
+ ### 1. Built-In Infrastructure
252
+ No external services needed — rate limiting runs at Cloudflare's edge.
253
+
254
+ ### 2. Per-Namespace Isolation
255
+ Different limits for different concerns (auth vs API vs WebSocket).
256
+
257
+ ### 3. Fail-Open Safety
258
+ If the rate limiter errors, requests are allowed through — no service disruption.
259
+
260
+ ### 4. Standard HTTP Headers
261
+ Clients can programmatically handle rate limits via `Retry-After` header.
262
+
263
+ ---
264
+
265
+ ## Trade-offs
266
+
267
+ ### 1. `unsafe.bindings` Label
268
+ **Downside**: Rate limiting uses Cloudflare's `unsafe.bindings`, which may change in future API versions.
269
+ **Mitigation**: Abstract behind a utility module (as shown) for easy migration.
270
+
271
+ ### 2. Simple Counter Only
272
+ **Downside**: Only supports simple fixed-window rate limiting (not sliding window or token bucket).
273
+ **Mitigation**: Sufficient for most applications. Use external services for advanced algorithms.
274
+
275
+ ---
276
+
277
+ ## Anti-Patterns
278
+
279
+ ### ❌ Anti-Pattern: Fail Closed on Rate Limiter Error
280
+
281
+ ```typescript
282
+ // ❌ BAD: Blocks all requests if rate limiter fails
283
+ const result = await rateLimiter.limit({ key })
284
+ // If this throws, the entire request fails
285
+
286
+ // ✅ GOOD: Fail open
287
+ try {
288
+ const result = await rateLimiter.limit({ key })
289
+ if (!result.success) return createRateLimitResponse(result)
290
+ } catch {
291
+ // Allow request through if rate limiter is unavailable
292
+ }
293
+ ```
294
+
295
+ ---
296
+
297
+ ## Related Patterns
298
+
299
+ - **[API Route Handlers](./tanstack-cloudflare.api-route-handlers.md)**: Rate limiting applied in API routes
300
+ - **[Auth Session Management](./tanstack-cloudflare.auth-session-management.md)**: Auth endpoints need strict rate limits
301
+ - **[Durable Objects WebSocket](./tanstack-cloudflare.durable-objects-websocket.md)**: WebSocket connections rate limited
302
+ - **[Wrangler Configuration](./tanstack-cloudflare.wrangler-configuration.md)**: Rate limiter bindings configured in wrangler.toml
303
+
304
+ ---
305
+
306
+ ## Checklist for Implementation
307
+
308
+ - [ ] Rate limiter bindings configured in `wrangler.toml`
309
+ - [ ] Separate namespaces for auth, API, and WebSocket
310
+ - [ ] Utility functions for check, response, and identifier extraction
311
+ - [ ] Auth endpoints use strict limits (5/min)
312
+ - [ ] API endpoints use moderate limits (100/min)
313
+ - [ ] WebSocket connections use connection-based limits (10/min)
314
+ - [ ] Rate limiter errors fail open (allow request)
315
+ - [ ] 429 responses include `Retry-After` header
316
+ - [ ] Rate limit identifier uses user ID when authenticated, IP otherwise
317
+
318
+ ---
319
+
320
+ **Status**: Stable - Production-ready rate limiting for Cloudflare Workers
321
+ **Recommendation**: Use for all production applications exposed to the internet
322
+ **Last Updated**: 2026-02-28
323
+ **Contributors**: Patrick Michaelsen
@@ -0,0 +1,338 @@
1
+ # Scheduled Tasks Pattern
2
+
3
+ **Category**: Infrastructure
4
+ **Applicable To**: TanStack Start + Cloudflare Workers applications requiring cron jobs
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ Cloudflare Workers supports Cron Triggers — scheduled tasks that run on a configurable schedule without requiring external cron services. This pattern replaces the common Next.js approach of exposing `/api/scheduled/*` endpoints that are hit by external cron services (Vercel Cron, Google Cloud Scheduler, etc.).
12
+
13
+ With Cron Triggers, the schedule is defined in `wrangler.toml` and the handler is a `scheduled` event in your worker, eliminating the need for external schedulers, API keys for cron endpoints, or separate Cloud Run services.
14
+
15
+ ---
16
+
17
+ ## When to Use This Pattern
18
+
19
+ ✅ **Use this pattern when:**
20
+ - Need periodic background tasks (daily digests, reminders, token refresh)
21
+ - Migrating from external cron-triggered API endpoints
22
+ - Want scheduled tasks co-located with your application code
23
+ - Need reliable execution without external scheduler dependencies
24
+
25
+ ❌ **Don't use this pattern when:**
26
+ - Tasks need to run for more than the Workers CPU time limit (300s on paid plan)
27
+ - Tasks require interactive user input
28
+ - Need sub-minute scheduling precision
29
+
30
+ ---
31
+
32
+ ## Core Principles
33
+
34
+ 1. **Declarative Schedules**: Cron schedules defined in `wrangler.toml`, not application code
35
+ 2. **Event-Based Handler**: Uses `scheduled` event, not HTTP endpoints
36
+ 3. **Routing by Cron Expression**: Use `event.cron` to dispatch to the right handler
37
+ 4. **Fail-Safe Execution**: Errors are logged but don't crash the worker
38
+ 5. **Debug Endpoint**: Keep an HTTP endpoint for manual testing/triggering
39
+
40
+ ---
41
+
42
+ ## Implementation
43
+
44
+ ### Step 1: Configure Cron Triggers in wrangler.toml
45
+
46
+ ```toml
47
+ # wrangler.toml
48
+
49
+ [triggers]
50
+ crons = [
51
+ "0 7 * * *", # Daily at 7:00 AM UTC — daily digest
52
+ "*/15 * * * *", # Every 15 minutes — clean reminders
53
+ "0 */6 * * *", # Every 6 hours — token refresh
54
+ ]
55
+ ```
56
+
57
+ ### Step 2: Handle Scheduled Events in Server Entry
58
+
59
+ ```typescript
60
+ // src/server.ts (or worker entry point)
61
+
62
+ export default {
63
+ // Standard fetch handler for HTTP requests (TanStack Start handles this)
64
+ fetch: app.fetch,
65
+
66
+ // Scheduled handler for cron triggers
67
+ async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
68
+ ctx.waitUntil(handleScheduledEvent(event, env))
69
+ },
70
+ }
71
+
72
+ async function handleScheduledEvent(event: ScheduledEvent, env: Env): Promise<void> {
73
+ console.log(`[Cron] Triggered: ${event.cron} at ${new Date(event.scheduledTime).toISOString()}`)
74
+
75
+ try {
76
+ switch (event.cron) {
77
+ case '0 7 * * *':
78
+ await handleDailyDigest(env)
79
+ break
80
+ case '*/15 * * * *':
81
+ await handleCleanReminders(env)
82
+ break
83
+ case '0 */6 * * *':
84
+ await handleTokenRefresh(env)
85
+ break
86
+ default:
87
+ console.warn(`[Cron] Unknown cron expression: ${event.cron}`)
88
+ }
89
+ } catch (error) {
90
+ console.error(`[Cron] Failed for ${event.cron}:`, error)
91
+ // Don't rethrow — cron failures should be logged, not crash the worker
92
+ }
93
+ }
94
+ ```
95
+
96
+ ### Step 3: Implement Task Handlers
97
+
98
+ ```typescript
99
+ // src/lib/scheduled/daily-digest.ts
100
+
101
+ export async function handleDailyDigest(env: Env): Promise<void> {
102
+ console.log('[DailyDigest] Starting daily digest...')
103
+
104
+ // 1. Query data
105
+ const checkIns = await ReservationDatabaseService.getTodayCheckIns()
106
+ const checkOuts = await ReservationDatabaseService.getTodayCheckOuts()
107
+ const unclaimedCleans = await AppointmentDatabaseService.getUnclaimedNextWeek()
108
+
109
+ // 2. Build email content
110
+ const html = buildDigestEmail({
111
+ checkIns,
112
+ checkOuts,
113
+ unclaimedCleans,
114
+ })
115
+
116
+ // 3. Send email
117
+ await sendEmail({
118
+ to: env.MANAGER_EMAILS,
119
+ subject: `Daily Digest — ${new Date().toLocaleDateString()}`,
120
+ html,
121
+ })
122
+
123
+ console.log('[DailyDigest] Complete')
124
+ }
125
+ ```
126
+
127
+ ```typescript
128
+ // src/lib/scheduled/token-refresh.ts
129
+
130
+ export async function handleTokenRefresh(env: Env): Promise<void> {
131
+ console.log('[TokenRefresh] Refreshing external API tokens...')
132
+
133
+ // Refresh Guesty OAuth token
134
+ try {
135
+ const guestyToken = await GuestyAuthService.refreshToken()
136
+ await GuestyTokenStorage.store(guestyToken)
137
+ console.log('[TokenRefresh] Guesty token refreshed')
138
+ } catch (error) {
139
+ console.error('[TokenRefresh] Guesty refresh failed:', error)
140
+ // Continue with other refreshes
141
+ }
142
+
143
+ // Refresh other tokens...
144
+ }
145
+ ```
146
+
147
+ ### Step 4: Keep Debug HTTP Endpoint
148
+
149
+ ```typescript
150
+ // routes/api/scheduled/daily-digest.tsx
151
+ // Manual trigger for testing — protected by admin auth
152
+
153
+ import { createFileRoute } from '@tanstack/react-router'
154
+ import { getAuthSession } from '@/lib/auth/server-fn'
155
+ import { handleDailyDigest } from '@/lib/scheduled/daily-digest'
156
+
157
+ export const Route = createFileRoute('/api/scheduled/daily-digest')({
158
+ server: {
159
+ handlers: {
160
+ POST: async ({ request, context }) => {
161
+ const user = await getAuthSession()
162
+ if (!user?.isAdmin) {
163
+ return new Response(JSON.stringify({ error: 'Forbidden' }), {
164
+ status: 403,
165
+ headers: { 'Content-Type': 'application/json' },
166
+ })
167
+ }
168
+
169
+ const env = context.cloudflare.env as Env
170
+ const body = await request.json()
171
+ const { debug = false } = body
172
+
173
+ if (debug) {
174
+ // Return preview data without sending
175
+ const data = await buildDigestPreview()
176
+ return new Response(JSON.stringify({ success: true, preview: data }), {
177
+ headers: { 'Content-Type': 'application/json' },
178
+ })
179
+ }
180
+
181
+ await handleDailyDigest(env)
182
+ return new Response(JSON.stringify({ success: true }), {
183
+ headers: { 'Content-Type': 'application/json' },
184
+ })
185
+ },
186
+ },
187
+ },
188
+ })
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Migrating from Next.js Scheduled Endpoints
194
+
195
+ ### Before (Next.js + External Cron)
196
+
197
+ ```
198
+ External Cron (Vercel/GCP)
199
+ → POST /api/scheduled/daily-digest (with API key auth)
200
+ → Next.js route handler
201
+ → Business logic
202
+ ```
203
+
204
+ ### After (Cloudflare Cron Triggers)
205
+
206
+ ```
207
+ Cloudflare Cron Trigger (built-in)
208
+ → scheduled event handler
209
+ → Business logic
210
+
211
+ Manual trigger (admin-only):
212
+ → POST /api/scheduled/daily-digest (with session auth)
213
+ → Same business logic
214
+ ```
215
+
216
+ ### Migration Steps
217
+
218
+ 1. **Extract logic**: Move business logic from Next.js route handler into a standalone function
219
+ 2. **Add wrangler cron**: Define the schedule in `wrangler.toml`
220
+ 3. **Add scheduled handler**: Wire up the `scheduled` event in your server entry
221
+ 4. **Keep HTTP endpoint**: Add an admin-protected manual trigger for testing
222
+ 5. **Remove external cron**: Delete external scheduler configuration
223
+
224
+ ---
225
+
226
+ ## Cron Expression Reference
227
+
228
+ | Expression | Schedule |
229
+ |-----------|----------|
230
+ | `* * * * *` | Every minute |
231
+ | `*/15 * * * *` | Every 15 minutes |
232
+ | `0 * * * *` | Every hour |
233
+ | `0 */6 * * *` | Every 6 hours |
234
+ | `0 7 * * *` | Daily at 7:00 AM UTC |
235
+ | `0 7 * * 1-5` | Weekdays at 7:00 AM UTC |
236
+ | `0 0 * * 0` | Weekly on Sunday at midnight |
237
+ | `0 0 1 * *` | Monthly on the 1st at midnight |
238
+
239
+ ---
240
+
241
+ ## Benefits
242
+
243
+ ### 1. No External Dependencies
244
+ Schedule lives in `wrangler.toml` — no external cron service to configure or maintain.
245
+
246
+ ### 2. Co-Located Code
247
+ Scheduled tasks and application code deploy together — no version drift.
248
+
249
+ ### 3. Automatic Retries
250
+ Cloudflare retries failed cron triggers automatically.
251
+
252
+ ### 4. Free on All Plans
253
+ Cron Triggers are included in all Cloudflare Workers plans at no additional cost.
254
+
255
+ ---
256
+
257
+ ## Trade-offs
258
+
259
+ ### 1. CPU Time Limits
260
+ **Downside**: Tasks are subject to Workers CPU limits (10ms free, 300s paid).
261
+ **Mitigation**: Break long tasks into smaller chunks. Use Durable Objects for long-running work.
262
+
263
+ ### 2. No Sub-Minute Precision
264
+ **Downside**: Minimum interval is 1 minute.
265
+ **Mitigation**: Use Durable Objects `alarm()` for sub-minute scheduling.
266
+
267
+ ### 3. UTC Only
268
+ **Downside**: Cron expressions use UTC, not local time.
269
+ **Mitigation**: Calculate UTC offset for your target timezone.
270
+
271
+ ---
272
+
273
+ ## Anti-Patterns
274
+
275
+ ### ❌ Anti-Pattern: Unprotected HTTP Cron Endpoints
276
+
277
+ ```typescript
278
+ // ❌ BAD: No auth on cron endpoint (anyone can trigger it)
279
+ POST: async ({ request }) => {
280
+ await handleDailyDigest() // No auth check!
281
+ return Response.json({ success: true })
282
+ }
283
+
284
+ // ✅ GOOD: Admin-only access
285
+ POST: async ({ request }) => {
286
+ const user = await getAuthSession()
287
+ if (!user?.isAdmin) return Response.json({ error: 'Forbidden' }, { status: 403 })
288
+ await handleDailyDigest()
289
+ return Response.json({ success: true })
290
+ }
291
+ ```
292
+
293
+ ### ❌ Anti-Pattern: Rethrowing Cron Errors
294
+
295
+ ```typescript
296
+ // ❌ BAD: Rethrowing crashes the worker
297
+ async scheduled(event, env, ctx) {
298
+ await handleDailyDigest(env) // If this throws, worker crashes
299
+ }
300
+
301
+ // ✅ GOOD: Catch and log
302
+ async scheduled(event, env, ctx) {
303
+ try {
304
+ await handleDailyDigest(env)
305
+ } catch (error) {
306
+ console.error('[Cron] Failed:', error)
307
+ }
308
+ }
309
+ ```
310
+
311
+ ---
312
+
313
+ ## Related Patterns
314
+
315
+ - **[Wrangler Configuration](./tanstack-cloudflare.wrangler-configuration.md)**: Cron triggers configured in wrangler.toml
316
+ - **[Email Service](./tanstack-cloudflare.email-service.md)**: Scheduled tasks commonly send emails
317
+ - **[Third-Party API Integration](./tanstack-cloudflare.third-party-api-integration.md)**: Token refresh as scheduled task
318
+ - **[API Route Handlers](./tanstack-cloudflare.api-route-handlers.md)**: Debug endpoints for manual triggers
319
+
320
+ ---
321
+
322
+ ## Checklist for Implementation
323
+
324
+ - [ ] Cron expressions defined in `wrangler.toml` under `[triggers]`
325
+ - [ ] `scheduled` event handler in server entry point
326
+ - [ ] Dispatch logic based on `event.cron` string
327
+ - [ ] All errors caught and logged (never rethrow)
328
+ - [ ] Business logic in standalone functions (not in handler)
329
+ - [ ] Admin-protected HTTP endpoint for manual testing
330
+ - [ ] Debug mode returns preview without executing
331
+ - [ ] `ctx.waitUntil()` used for async work
332
+
333
+ ---
334
+
335
+ **Status**: Stable - Production-ready scheduled task pattern
336
+ **Recommendation**: Use for all periodic background tasks
337
+ **Last Updated**: 2026-02-28
338
+ **Contributors**: Patrick Michaelsen