@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.
- package/README.md +68 -0
- package/agent/commands/acp.clarification-address.md +417 -0
- package/agent/commands/acp.clarification-capture.md +386 -0
- package/agent/commands/acp.clarification-create.md +437 -0
- package/agent/commands/acp.clarifications-research.md +326 -0
- package/agent/commands/acp.command-create.md +432 -0
- package/agent/commands/acp.design-create.md +286 -0
- package/agent/commands/acp.design-reference.md +355 -0
- package/agent/commands/acp.handoff.md +270 -0
- package/agent/commands/acp.index.md +423 -0
- package/agent/commands/acp.init.md +546 -0
- package/agent/commands/acp.package-create.md +895 -0
- package/agent/commands/acp.package-info.md +212 -0
- package/agent/commands/acp.package-install.md +539 -0
- package/agent/commands/acp.package-list.md +280 -0
- package/agent/commands/acp.package-publish.md +541 -0
- package/agent/commands/acp.package-remove.md +293 -0
- package/agent/commands/acp.package-search.md +307 -0
- package/agent/commands/acp.package-update.md +361 -0
- package/agent/commands/acp.package-validate.md +540 -0
- package/agent/commands/acp.pattern-create.md +386 -0
- package/agent/commands/acp.plan.md +587 -0
- package/agent/commands/acp.proceed.md +882 -0
- package/agent/commands/acp.project-create.md +675 -0
- package/agent/commands/acp.project-info.md +312 -0
- package/agent/commands/acp.project-list.md +226 -0
- package/agent/commands/acp.project-remove.md +379 -0
- package/agent/commands/acp.project-set.md +227 -0
- package/agent/commands/acp.project-update.md +307 -0
- package/agent/commands/acp.projects-restore.md +228 -0
- package/agent/commands/acp.projects-sync.md +347 -0
- package/agent/commands/acp.report.md +407 -0
- package/agent/commands/acp.resume.md +239 -0
- package/agent/commands/acp.sessions.md +301 -0
- package/agent/commands/acp.status.md +293 -0
- package/agent/commands/acp.sync.md +364 -0
- package/agent/commands/acp.task-create.md +500 -0
- package/agent/commands/acp.update.md +302 -0
- package/agent/commands/acp.validate.md +466 -0
- package/agent/commands/acp.version-check-for-updates.md +276 -0
- package/agent/commands/acp.version-check.md +191 -0
- package/agent/commands/acp.version-update.md +289 -0
- package/agent/commands/command.template.md +339 -0
- package/agent/commands/git.commit.md +526 -0
- package/agent/commands/git.init.md +514 -0
- package/agent/commands/tanstack-cloudflare.deploy.md +272 -0
- package/agent/commands/tanstack-cloudflare.tail.md +275 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/design.template.md +154 -0
- package/agent/design/local.dashboard-layout-routing.md +288 -0
- package/agent/design/local.data-model-yaml-parsing.md +310 -0
- package/agent/design/local.search-filtering.md +331 -0
- package/agent/design/local.server-api-auto-refresh.md +235 -0
- package/agent/design/local.table-tree-views.md +299 -0
- package/agent/design/local.visualizer-requirements.md +349 -0
- package/agent/design/requirements.template.md +387 -0
- package/agent/index/.gitkeep +0 -0
- package/agent/index/acp.core.yaml +137 -0
- package/agent/index/local.main.template.yaml +37 -0
- package/agent/manifest.template.yaml +13 -0
- package/agent/manifest.yaml +302 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +67 -0
- package/agent/milestones/milestone-1-{title}.template.md +206 -0
- package/agent/milestones/milestone-2-dashboard-views-interaction.md +79 -0
- package/agent/package.template.yaml +86 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.template.md +1237 -0
- package/agent/patterns/pattern.template.md +382 -0
- package/agent/patterns/tanstack-cloudflare.acl-permissions.md +332 -0
- package/agent/patterns/tanstack-cloudflare.action-bar-item.md +416 -0
- package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +401 -0
- package/agent/patterns/tanstack-cloudflare.auth-session-management.md +387 -0
- package/agent/patterns/tanstack-cloudflare.card-and-list.md +271 -0
- package/agent/patterns/tanstack-cloudflare.chat-engine.md +353 -0
- package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +346 -0
- package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +516 -0
- package/agent/patterns/tanstack-cloudflare.email-service.md +431 -0
- package/agent/patterns/tanstack-cloudflare.expander.md +98 -0
- package/agent/patterns/tanstack-cloudflare.fcm-push.md +115 -0
- package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +441 -0
- package/agent/patterns/tanstack-cloudflare.firebase-auth.md +348 -0
- package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +550 -0
- package/agent/patterns/tanstack-cloudflare.firebase-storage.md +369 -0
- package/agent/patterns/tanstack-cloudflare.form-controls.md +145 -0
- package/agent/patterns/tanstack-cloudflare.global-search-context.md +93 -0
- package/agent/patterns/tanstack-cloudflare.image-carousel.md +126 -0
- package/agent/patterns/tanstack-cloudflare.library-services.md +553 -0
- package/agent/patterns/tanstack-cloudflare.lightbox.md +169 -0
- package/agent/patterns/tanstack-cloudflare.markdown-content.md +115 -0
- package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +98 -0
- package/agent/patterns/tanstack-cloudflare.modal.md +156 -0
- package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +461 -0
- package/agent/patterns/tanstack-cloudflare.notifications-engine.md +151 -0
- package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +90 -0
- package/agent/patterns/tanstack-cloudflare.og-metadata.md +296 -0
- package/agent/patterns/tanstack-cloudflare.pagination.md +442 -0
- package/agent/patterns/tanstack-cloudflare.pill-input.md +220 -0
- package/agent/patterns/tanstack-cloudflare.provider-adapter.md +401 -0
- package/agent/patterns/tanstack-cloudflare.rate-limiting.md +323 -0
- package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +338 -0
- package/agent/patterns/tanstack-cloudflare.searchable-settings.md +375 -0
- package/agent/patterns/tanstack-cloudflare.slide-over.md +129 -0
- package/agent/patterns/tanstack-cloudflare.ssr-preload.md +571 -0
- package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +508 -0
- package/agent/patterns/tanstack-cloudflare.toast-system.md +142 -0
- package/agent/patterns/tanstack-cloudflare.unified-header.md +280 -0
- package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +628 -0
- package/agent/patterns/tanstack-cloudflare.websocket-manager.md +237 -0
- package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +358 -0
- package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +336 -0
- package/agent/progress.template.yaml +161 -0
- package/agent/progress.yaml +145 -0
- package/agent/schemas/package.schema.yaml +276 -0
- package/agent/scripts/acp.common.sh +1781 -0
- package/agent/scripts/acp.install.sh +333 -0
- package/agent/scripts/acp.package-create.sh +924 -0
- package/agent/scripts/acp.package-info.sh +288 -0
- package/agent/scripts/acp.package-install.sh +893 -0
- package/agent/scripts/acp.package-list.sh +311 -0
- package/agent/scripts/acp.package-publish.sh +420 -0
- package/agent/scripts/acp.package-remove.sh +348 -0
- package/agent/scripts/acp.package-search.sh +156 -0
- package/agent/scripts/acp.package-update.sh +517 -0
- package/agent/scripts/acp.package-validate.sh +1018 -0
- package/agent/scripts/acp.uninstall.sh +85 -0
- package/agent/scripts/acp.version-check-for-updates.sh +98 -0
- package/agent/scripts/acp.version-check.sh +47 -0
- package/agent/scripts/acp.version-update.sh +176 -0
- package/agent/scripts/acp.yaml-parser.sh +985 -0
- package/agent/scripts/acp.yaml-validate.sh +205 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +210 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +294 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +193 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +262 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +156 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +178 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +141 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +153 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +174 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +233 -0
- package/agent/tasks/task-1-{title}.template.md +244 -0
- package/bin/visualize.mjs +84 -0
- package/package.json +48 -0
- package/src/components/ExtraFieldsBadge.tsx +15 -0
- package/src/components/FilterBar.tsx +33 -0
- package/src/components/Header.tsx +23 -0
- package/src/components/MilestoneTable.tsx +167 -0
- package/src/components/MilestoneTree.tsx +84 -0
- package/src/components/ProgressBar.tsx +20 -0
- package/src/components/SearchInput.tsx +22 -0
- package/src/components/Sidebar.tsx +54 -0
- package/src/components/StatusBadge.tsx +23 -0
- package/src/components/StatusDot.tsx +12 -0
- package/src/components/TaskList.tsx +36 -0
- package/src/components/ViewToggle.tsx +31 -0
- package/src/lib/config.ts +8 -0
- package/src/lib/file-watcher.ts +43 -0
- package/src/lib/search.ts +48 -0
- package/src/lib/types.ts +73 -0
- package/src/lib/useAutoRefresh.ts +31 -0
- package/src/lib/useCollapse.ts +31 -0
- package/src/lib/useFilteredData.ts +55 -0
- package/src/lib/yaml-loader-real.spec.ts +47 -0
- package/src/lib/yaml-loader.spec.ts +201 -0
- package/src/lib/yaml-loader.ts +265 -0
- package/src/routeTree.gen.ts +140 -0
- package/src/router.tsx +10 -0
- package/src/routes/__root.tsx +75 -0
- package/src/routes/api/watch.ts +29 -0
- package/src/routes/index.tsx +115 -0
- package/src/routes/milestones.tsx +50 -0
- package/src/routes/search.tsx +84 -0
- package/src/routes/tasks.tsx +63 -0
- package/src/services/progress-database.service.ts +46 -0
- package/src/styles.css +25 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +16 -0
- 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
|