@prmichaelsen/acp-visualizer 0.1.0 → 0.1.2

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 (159) hide show
  1. package/package.json +8 -10
  2. package/src/components/ExtraFieldsBadge.tsx +1 -1
  3. package/src/components/FilterBar.tsx +1 -1
  4. package/src/components/Header.tsx +1 -1
  5. package/src/components/MilestoneTable.tsx +1 -1
  6. package/src/components/MilestoneTree.tsx +2 -2
  7. package/src/components/StatusBadge.tsx +1 -1
  8. package/src/components/StatusDot.tsx +1 -1
  9. package/src/components/TaskList.tsx +1 -1
  10. package/src/routes/__root.tsx +5 -5
  11. package/src/routes/api/watch.ts +1 -1
  12. package/src/routes/index.tsx +2 -2
  13. package/src/routes/milestones.tsx +7 -7
  14. package/src/routes/search.tsx +4 -4
  15. package/src/routes/tasks.tsx +3 -3
  16. package/src/services/progress-database.service.ts +3 -3
  17. package/agent/commands/acp.clarification-address.md +0 -417
  18. package/agent/commands/acp.clarification-capture.md +0 -386
  19. package/agent/commands/acp.clarification-create.md +0 -437
  20. package/agent/commands/acp.clarifications-research.md +0 -326
  21. package/agent/commands/acp.command-create.md +0 -432
  22. package/agent/commands/acp.design-create.md +0 -286
  23. package/agent/commands/acp.design-reference.md +0 -355
  24. package/agent/commands/acp.handoff.md +0 -270
  25. package/agent/commands/acp.index.md +0 -423
  26. package/agent/commands/acp.init.md +0 -546
  27. package/agent/commands/acp.package-create.md +0 -895
  28. package/agent/commands/acp.package-info.md +0 -212
  29. package/agent/commands/acp.package-install.md +0 -539
  30. package/agent/commands/acp.package-list.md +0 -280
  31. package/agent/commands/acp.package-publish.md +0 -541
  32. package/agent/commands/acp.package-remove.md +0 -293
  33. package/agent/commands/acp.package-search.md +0 -307
  34. package/agent/commands/acp.package-update.md +0 -361
  35. package/agent/commands/acp.package-validate.md +0 -540
  36. package/agent/commands/acp.pattern-create.md +0 -386
  37. package/agent/commands/acp.plan.md +0 -587
  38. package/agent/commands/acp.proceed.md +0 -882
  39. package/agent/commands/acp.project-create.md +0 -675
  40. package/agent/commands/acp.project-info.md +0 -312
  41. package/agent/commands/acp.project-list.md +0 -226
  42. package/agent/commands/acp.project-remove.md +0 -379
  43. package/agent/commands/acp.project-set.md +0 -227
  44. package/agent/commands/acp.project-update.md +0 -307
  45. package/agent/commands/acp.projects-restore.md +0 -228
  46. package/agent/commands/acp.projects-sync.md +0 -347
  47. package/agent/commands/acp.report.md +0 -407
  48. package/agent/commands/acp.resume.md +0 -239
  49. package/agent/commands/acp.sessions.md +0 -301
  50. package/agent/commands/acp.status.md +0 -293
  51. package/agent/commands/acp.sync.md +0 -364
  52. package/agent/commands/acp.task-create.md +0 -500
  53. package/agent/commands/acp.update.md +0 -302
  54. package/agent/commands/acp.validate.md +0 -466
  55. package/agent/commands/acp.version-check-for-updates.md +0 -276
  56. package/agent/commands/acp.version-check.md +0 -191
  57. package/agent/commands/acp.version-update.md +0 -289
  58. package/agent/commands/command.template.md +0 -339
  59. package/agent/commands/git.commit.md +0 -526
  60. package/agent/commands/git.init.md +0 -514
  61. package/agent/commands/tanstack-cloudflare.deploy.md +0 -272
  62. package/agent/commands/tanstack-cloudflare.tail.md +0 -275
  63. package/agent/design/.gitkeep +0 -0
  64. package/agent/design/design.template.md +0 -154
  65. package/agent/design/local.dashboard-layout-routing.md +0 -288
  66. package/agent/design/local.data-model-yaml-parsing.md +0 -310
  67. package/agent/design/local.search-filtering.md +0 -331
  68. package/agent/design/local.server-api-auto-refresh.md +0 -235
  69. package/agent/design/local.table-tree-views.md +0 -299
  70. package/agent/design/local.visualizer-requirements.md +0 -349
  71. package/agent/design/requirements.template.md +0 -387
  72. package/agent/index/.gitkeep +0 -0
  73. package/agent/index/acp.core.yaml +0 -137
  74. package/agent/index/local.main.template.yaml +0 -37
  75. package/agent/manifest.template.yaml +0 -13
  76. package/agent/manifest.yaml +0 -302
  77. package/agent/milestones/.gitkeep +0 -0
  78. package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +0 -67
  79. package/agent/milestones/milestone-1-{title}.template.md +0 -206
  80. package/agent/milestones/milestone-2-dashboard-views-interaction.md +0 -79
  81. package/agent/package.template.yaml +0 -86
  82. package/agent/patterns/.gitkeep +0 -0
  83. package/agent/patterns/bootstrap.template.md +0 -1237
  84. package/agent/patterns/pattern.template.md +0 -382
  85. package/agent/patterns/tanstack-cloudflare.acl-permissions.md +0 -332
  86. package/agent/patterns/tanstack-cloudflare.action-bar-item.md +0 -416
  87. package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +0 -401
  88. package/agent/patterns/tanstack-cloudflare.auth-session-management.md +0 -387
  89. package/agent/patterns/tanstack-cloudflare.card-and-list.md +0 -271
  90. package/agent/patterns/tanstack-cloudflare.chat-engine.md +0 -353
  91. package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +0 -346
  92. package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +0 -516
  93. package/agent/patterns/tanstack-cloudflare.email-service.md +0 -431
  94. package/agent/patterns/tanstack-cloudflare.expander.md +0 -98
  95. package/agent/patterns/tanstack-cloudflare.fcm-push.md +0 -115
  96. package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +0 -441
  97. package/agent/patterns/tanstack-cloudflare.firebase-auth.md +0 -348
  98. package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +0 -550
  99. package/agent/patterns/tanstack-cloudflare.firebase-storage.md +0 -369
  100. package/agent/patterns/tanstack-cloudflare.form-controls.md +0 -145
  101. package/agent/patterns/tanstack-cloudflare.global-search-context.md +0 -93
  102. package/agent/patterns/tanstack-cloudflare.image-carousel.md +0 -126
  103. package/agent/patterns/tanstack-cloudflare.library-services.md +0 -553
  104. package/agent/patterns/tanstack-cloudflare.lightbox.md +0 -169
  105. package/agent/patterns/tanstack-cloudflare.markdown-content.md +0 -115
  106. package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +0 -98
  107. package/agent/patterns/tanstack-cloudflare.modal.md +0 -156
  108. package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +0 -461
  109. package/agent/patterns/tanstack-cloudflare.notifications-engine.md +0 -151
  110. package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +0 -90
  111. package/agent/patterns/tanstack-cloudflare.og-metadata.md +0 -296
  112. package/agent/patterns/tanstack-cloudflare.pagination.md +0 -442
  113. package/agent/patterns/tanstack-cloudflare.pill-input.md +0 -220
  114. package/agent/patterns/tanstack-cloudflare.provider-adapter.md +0 -401
  115. package/agent/patterns/tanstack-cloudflare.rate-limiting.md +0 -323
  116. package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +0 -338
  117. package/agent/patterns/tanstack-cloudflare.searchable-settings.md +0 -375
  118. package/agent/patterns/tanstack-cloudflare.slide-over.md +0 -129
  119. package/agent/patterns/tanstack-cloudflare.ssr-preload.md +0 -571
  120. package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +0 -508
  121. package/agent/patterns/tanstack-cloudflare.toast-system.md +0 -142
  122. package/agent/patterns/tanstack-cloudflare.unified-header.md +0 -280
  123. package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +0 -628
  124. package/agent/patterns/tanstack-cloudflare.websocket-manager.md +0 -237
  125. package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +0 -358
  126. package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +0 -336
  127. package/agent/progress.template.yaml +0 -161
  128. package/agent/progress.yaml +0 -145
  129. package/agent/schemas/package.schema.yaml +0 -276
  130. package/agent/scripts/acp.common.sh +0 -1781
  131. package/agent/scripts/acp.install.sh +0 -333
  132. package/agent/scripts/acp.package-create.sh +0 -924
  133. package/agent/scripts/acp.package-info.sh +0 -288
  134. package/agent/scripts/acp.package-install.sh +0 -893
  135. package/agent/scripts/acp.package-list.sh +0 -311
  136. package/agent/scripts/acp.package-publish.sh +0 -420
  137. package/agent/scripts/acp.package-remove.sh +0 -348
  138. package/agent/scripts/acp.package-search.sh +0 -156
  139. package/agent/scripts/acp.package-update.sh +0 -517
  140. package/agent/scripts/acp.package-validate.sh +0 -1018
  141. package/agent/scripts/acp.uninstall.sh +0 -85
  142. package/agent/scripts/acp.version-check-for-updates.sh +0 -98
  143. package/agent/scripts/acp.version-check.sh +0 -47
  144. package/agent/scripts/acp.version-update.sh +0 -176
  145. package/agent/scripts/acp.yaml-parser.sh +0 -985
  146. package/agent/scripts/acp.yaml-validate.sh +0 -205
  147. package/agent/tasks/.gitkeep +0 -0
  148. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +0 -210
  149. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +0 -294
  150. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +0 -193
  151. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +0 -262
  152. package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +0 -156
  153. package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +0 -178
  154. package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +0 -141
  155. package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +0 -153
  156. package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +0 -174
  157. package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +0 -233
  158. package/agent/tasks/task-1-{title}.template.md +0 -244
  159. package/vitest.config.ts +0 -27
@@ -1,323 +0,0 @@
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
@@ -1,338 +0,0 @@
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