@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,461 @@
1
+ # Next.js to TanStack Start Routing Migration
2
+
3
+ **Category**: Migration
4
+ **Applicable To**: Projects migrating from Next.js App Router to TanStack Start + Cloudflare Workers
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ This pattern maps Next.js App Router conventions to their TanStack Start equivalents. It covers page routes, layouts, API routes, dynamic parameters, metadata, server-side data fetching, and middleware. The goal is a systematic migration where each Next.js file has a clear TanStack Start counterpart.
12
+
13
+ Both frameworks use file-based routing, but they differ in naming conventions, data loading strategies, and API route syntax. This guide provides side-by-side mappings with code examples.
14
+
15
+ ---
16
+
17
+ ## When to Use This Pattern
18
+
19
+ ✅ **Use this pattern when:**
20
+ - Migrating an existing Next.js App Router application to TanStack Start
21
+ - Need a reference for mapping Next.js conventions to TanStack equivalents
22
+ - Building new features and want to know the TanStack way of doing something familiar from Next.js
23
+
24
+ ❌ **Don't use this pattern when:**
25
+ - Migrating from Next.js Pages Router (different conventions)
26
+ - Building a new project from scratch (use other patterns directly)
27
+
28
+ ---
29
+
30
+ ## Route File Mapping
31
+
32
+ | Next.js App Router | TanStack Start | Notes |
33
+ |-------------------|----------------|-------|
34
+ | `app/page.tsx` | `routes/index.tsx` | Home page |
35
+ | `app/layout.tsx` | `routes/__root.tsx` | Root layout |
36
+ | `app/about/page.tsx` | `routes/about.tsx` | Static page |
37
+ | `app/blog/[id]/page.tsx` | `routes/blog/$id.tsx` | Dynamic route (`[id]` → `$id`) |
38
+ | `app/blog/[...slug]/page.tsx` | `routes/blog/$.tsx` | Catch-all route |
39
+ | `app/api/posts/route.ts` | `routes/api/posts/index.tsx` | API route |
40
+ | `app/api/posts/[id]/route.ts` | `routes/api/posts/$id.tsx` | Dynamic API route |
41
+ | `app/(group)/page.tsx` | `routes/_group/index.tsx` | Route group (parentheses → underscore) |
42
+ | `app/loading.tsx` | `pendingComponent` on route | Loading state |
43
+ | `app/error.tsx` | `errorComponent` on route | Error boundary |
44
+ | `app/not-found.tsx` | `notFoundComponent` on route | 404 handler |
45
+
46
+ ---
47
+
48
+ ## Page Routes
49
+
50
+ ### Next.js: Server Component with Data Fetching
51
+
52
+ ```typescript
53
+ // app/home/page.tsx (Next.js)
54
+ import { cookies } from 'next/headers'
55
+ import { getServerSession } from '@/lib/auth-server'
56
+ import HomePageClient from './HomePageClient'
57
+
58
+ export const metadata = {
59
+ title: 'Home',
60
+ description: 'Your neighborhood feed',
61
+ }
62
+
63
+ export default async function HomePage() {
64
+ const cookieStore = cookies()
65
+ const session = cookieStore.get('session')
66
+ // Fetch data server-side...
67
+ return <HomePageClient />
68
+ }
69
+ ```
70
+
71
+ ### TanStack Start: Route with beforeLoad
72
+
73
+ ```typescript
74
+ // routes/home.tsx (TanStack Start)
75
+ import { createFileRoute, redirect } from '@tanstack/react-router'
76
+ import { getAuthSession } from '@/lib/auth/server-fn'
77
+
78
+ export const Route = createFileRoute('/home')({
79
+ // Server-side data fetching (replaces async Server Component)
80
+ beforeLoad: async () => {
81
+ const user = await getAuthSession()
82
+ if (!user) {
83
+ throw redirect({ to: '/auth', search: { redirect_url: '/home' } })
84
+ }
85
+
86
+ let posts = []
87
+ try {
88
+ posts = await PostDatabaseService.getFeed(user.uid, 50)
89
+ } catch (error) {
90
+ console.error('Failed to preload feed:', error)
91
+ }
92
+
93
+ return { user, posts }
94
+ },
95
+
96
+ // SEO metadata (replaces export const metadata)
97
+ meta: () => [
98
+ { title: 'Home' },
99
+ { name: 'description', content: 'Your neighborhood feed' },
100
+ ],
101
+
102
+ component: HomePage,
103
+ })
104
+
105
+ function HomePage() {
106
+ const { user, posts } = Route.useRouteContext()
107
+ return <HomePageClient initialPosts={posts} />
108
+ }
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Layouts
114
+
115
+ ### Next.js: layout.tsx
116
+
117
+ ```typescript
118
+ // app/layout.tsx (Next.js)
119
+ import { cookies } from 'next/headers'
120
+ import ReduxProvider from '@/components/ReduxProvider'
121
+
122
+ export default async function RootLayout({ children }) {
123
+ const cookieStore = cookies()
124
+ const sessionCookie = cookieStore.get('session')
125
+
126
+ let initialState
127
+ try {
128
+ initialState = await getStateFromHeaders(sessionCookie?.value)
129
+ } catch { initialState = {} }
130
+
131
+ return (
132
+ <html lang="en">
133
+ <body>
134
+ <ReduxProvider initialState={initialState}>
135
+ <Navbar />
136
+ {children}
137
+ <ModalContainer />
138
+ <ToastContainer />
139
+ </ReduxProvider>
140
+ </body>
141
+ </html>
142
+ )
143
+ }
144
+ ```
145
+
146
+ ### TanStack Start: __root.tsx
147
+
148
+ ```typescript
149
+ // routes/__root.tsx (TanStack Start)
150
+ import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
151
+ import { getAuthSession } from '@/lib/auth/server-fn'
152
+ import ReduxProvider from '@/components/ReduxProvider'
153
+
154
+ export const Route = createRootRouteWithContext()({
155
+ beforeLoad: async () => {
156
+ const user = await getAuthSession()
157
+ return { user }
158
+ },
159
+ component: RootLayout,
160
+ })
161
+
162
+ function RootLayout() {
163
+ const { user } = Route.useRouteContext()
164
+
165
+ return (
166
+ <ReduxProvider initialUser={user}>
167
+ <Navbar />
168
+ <Outlet /> {/* Replaces {children} */}
169
+ <ModalContainer />
170
+ <ToastContainer />
171
+ </ReduxProvider>
172
+ )
173
+ }
174
+ ```
175
+
176
+ **Key differences**:
177
+ - `{children}` → `<Outlet />`
178
+ - `cookies()` from `next/headers` → `getAuthSession()` server function
179
+ - Metadata in `export const metadata` → `meta` function on route
180
+ - `<html>` and `<body>` go in a separate entry file, not in `__root.tsx`
181
+
182
+ ---
183
+
184
+ ## API Routes
185
+
186
+ ### Next.js: Route Handlers
187
+
188
+ ```typescript
189
+ // app/api/posts/create/route.ts (Next.js)
190
+ import { NextRequest, NextResponse } from 'next/server'
191
+ import { getServerSession } from '@/lib/auth-server'
192
+
193
+ export async function POST(request: NextRequest) {
194
+ const session = await getServerSession(request)
195
+ if (!session?.user) {
196
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
197
+ }
198
+
199
+ const body = await request.json()
200
+ const post = await PostDatabaseService.create(session.user.uid, body)
201
+
202
+ return NextResponse.json(post, { status: 201 })
203
+ }
204
+ ```
205
+
206
+ ### TanStack Start: Server Handlers
207
+
208
+ ```typescript
209
+ // routes/api/posts/create.tsx (TanStack Start)
210
+ import { createFileRoute } from '@tanstack/react-router'
211
+ import { getAuthSession } from '@/lib/auth/server-fn'
212
+
213
+ export const Route = createFileRoute('/api/posts/create')({
214
+ server: {
215
+ handlers: {
216
+ POST: async ({ request }) => {
217
+ const user = await getAuthSession()
218
+ if (!user) {
219
+ return new Response(JSON.stringify({ error: 'Unauthorized' }), {
220
+ status: 401,
221
+ headers: { 'Content-Type': 'application/json' },
222
+ })
223
+ }
224
+
225
+ const body = await request.json()
226
+ const post = await PostDatabaseService.create(user.uid, body)
227
+
228
+ return new Response(JSON.stringify(post), {
229
+ status: 201,
230
+ headers: { 'Content-Type': 'application/json' },
231
+ })
232
+ },
233
+ },
234
+ },
235
+ })
236
+ ```
237
+
238
+ **Key differences**:
239
+ - `NextResponse.json()` → `new Response(JSON.stringify())` with headers
240
+ - `export async function POST` → `server.handlers.POST`
241
+ - `NextRequest` type → standard `Request`
242
+ - File extension `.ts` → `.tsx`
243
+
244
+ ---
245
+
246
+ ## Dynamic Routes
247
+
248
+ ### Next.js: [id] Parameter
249
+
250
+ ```typescript
251
+ // app/api/posts/[id]/route.ts (Next.js)
252
+ export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
253
+ const post = await PostDatabaseService.getById(params.id)
254
+ return NextResponse.json(post)
255
+ }
256
+ ```
257
+
258
+ ### TanStack Start: $id Parameter
259
+
260
+ ```typescript
261
+ // routes/api/posts/$id.tsx (TanStack Start)
262
+ export const Route = createFileRoute('/api/posts/$id')({
263
+ server: {
264
+ handlers: {
265
+ GET: async ({ params }) => {
266
+ const post = await PostDatabaseService.getById(params.id)
267
+ return new Response(JSON.stringify(post), {
268
+ status: 200,
269
+ headers: { 'Content-Type': 'application/json' },
270
+ })
271
+ },
272
+ },
273
+ },
274
+ })
275
+ ```
276
+
277
+ **Key difference**: `[id]` → `$id` in file names. Params accessed the same way.
278
+
279
+ ---
280
+
281
+ ## Server-Side Data Fetching
282
+
283
+ | Next.js Pattern | TanStack Start Equivalent |
284
+ |----------------|--------------------------|
285
+ | `async function Page()` (Server Component) | `beforeLoad` on route |
286
+ | `cookies()` from `next/headers` | `getRequest()` from `@tanstack/react-start/server` |
287
+ | `headers()` from `next/headers` | `getRequest().headers` |
288
+ | `fetch()` in Server Component | Database service call in `beforeLoad` |
289
+ | `revalidatePath()` / `revalidateTag()` | Not needed (no ISR — always fresh on Workers) |
290
+ | `generateStaticParams()` | Not applicable (no SSG on Workers) |
291
+
292
+ ---
293
+
294
+ ## Metadata / SEO
295
+
296
+ ### Next.js: Metadata Export
297
+
298
+ ```typescript
299
+ // app/profile/[username]/page.tsx (Next.js)
300
+ export async function generateMetadata({ params }) {
301
+ const profile = await getProfile(params.username)
302
+ return {
303
+ title: profile.displayName,
304
+ openGraph: { title: profile.displayName, images: [profile.avatar] },
305
+ }
306
+ }
307
+ ```
308
+
309
+ ### TanStack Start: meta Function
310
+
311
+ ```typescript
312
+ // routes/profile/$username.tsx (TanStack Start)
313
+ export const Route = createFileRoute('/profile/$username')({
314
+ beforeLoad: async ({ params }) => {
315
+ const profile = await ProfileDatabaseService.getByUsername(params.username)
316
+ return { profile }
317
+ },
318
+ meta: ({ loaderData }) => [
319
+ { title: loaderData.profile?.displayName },
320
+ { property: 'og:title', content: loaderData.profile?.displayName },
321
+ ],
322
+ component: ProfilePage,
323
+ })
324
+ ```
325
+
326
+ ---
327
+
328
+ ## Middleware
329
+
330
+ ### Next.js: middleware.ts
331
+
332
+ ```typescript
333
+ // middleware.ts (Next.js — runs on every request)
334
+ import { NextResponse } from 'next/server'
335
+ export function middleware(request) {
336
+ if (request.nextUrl.pathname.startsWith('/admin')) {
337
+ // Check auth, redirect if needed
338
+ }
339
+ return NextResponse.next()
340
+ }
341
+ export const config = { matcher: ['/admin/:path*'] }
342
+ ```
343
+
344
+ ### TanStack Start: beforeLoad on Parent Route
345
+
346
+ ```typescript
347
+ // routes/admin.tsx (TanStack Start — layout route for /admin/*)
348
+ export const Route = createFileRoute('/admin')({
349
+ beforeLoad: async () => {
350
+ const user = await getAuthSession()
351
+ if (!user?.isAdmin) {
352
+ throw redirect({ to: '/auth' })
353
+ }
354
+ return { user }
355
+ },
356
+ component: AdminLayout,
357
+ })
358
+
359
+ function AdminLayout() {
360
+ return <Outlet /> // Child admin routes render here
361
+ }
362
+ ```
363
+
364
+ **Key difference**: No global middleware file. Use `beforeLoad` on parent routes for path-specific guards.
365
+
366
+ ---
367
+
368
+ ## SSE / Streaming Responses
369
+
370
+ ### Next.js: ReadableStream
371
+
372
+ ```typescript
373
+ // app/api/chat/stream/route.ts (Next.js)
374
+ export async function POST(request: NextRequest) {
375
+ const stream = new ReadableStream({
376
+ async start(controller) {
377
+ const encoder = new TextEncoder()
378
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'chunk' })}\n\n`))
379
+ controller.close()
380
+ }
381
+ })
382
+ return new Response(stream, {
383
+ headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' }
384
+ })
385
+ }
386
+ ```
387
+
388
+ ### TanStack Start: Same Pattern (Web Standards)
389
+
390
+ ```typescript
391
+ // routes/api/chat/stream.tsx (TanStack Start)
392
+ POST: async ({ request }) => {
393
+ const stream = new ReadableStream({
394
+ async start(controller) {
395
+ const encoder = new TextEncoder()
396
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'chunk' })}\n\n`))
397
+ controller.close()
398
+ }
399
+ })
400
+ return new Response(stream, {
401
+ headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' }
402
+ })
403
+ }
404
+ ```
405
+
406
+ SSE streaming is identical — both use Web Standard `ReadableStream`. However, for real-time chat consider migrating to **Durable Objects WebSocket** instead of SSE.
407
+
408
+ ---
409
+
410
+ ## Migration Checklist
411
+
412
+ ### Phase 1: Project Setup
413
+ - [ ] Initialize TanStack Start project with Cloudflare Workers
414
+ - [ ] Configure `wrangler.toml` (see [wrangler-configuration](./tanstack-cloudflare.wrangler-configuration.md))
415
+ - [ ] Set up `vite.config.ts` with TanStack + Cloudflare plugins
416
+ - [ ] Configure path aliases (`@/` → `./src/`)
417
+ - [ ] Move environment variables to Cloudflare secrets
418
+
419
+ ### Phase 2: Route Migration
420
+ - [ ] Convert `app/layout.tsx` → `routes/__root.tsx`
421
+ - [ ] Convert `app/page.tsx` → `routes/index.tsx`
422
+ - [ ] Convert page routes: `app/X/page.tsx` → `routes/X.tsx`
423
+ - [ ] Convert dynamic routes: `[id]` → `$id`
424
+ - [ ] Convert `metadata` exports → `meta` functions
425
+ - [ ] Move `{children}` to `<Outlet />`
426
+
427
+ ### Phase 3: API Route Migration
428
+ - [ ] Convert `app/api/X/route.ts` → `routes/api/X/index.tsx`
429
+ - [ ] Replace `NextRequest`/`NextResponse` with Web Standard `Request`/`Response`
430
+ - [ ] Replace `export async function GET/POST` → `server.handlers.GET/POST`
431
+ - [ ] Replace `cookies()` API with cookie parsing from request headers
432
+
433
+ ### Phase 4: Data Fetching Migration
434
+ - [ ] Replace async Server Components with `beforeLoad`
435
+ - [ ] Replace `cookies()`/`headers()` with `getAuthSession()` server function
436
+ - [ ] Remove `revalidatePath`/`revalidateTag` (not needed on Workers)
437
+ - [ ] Remove `generateStaticParams` (no SSG on Workers)
438
+
439
+ ### Phase 5: Next.js Specific Removal
440
+ - [ ] Remove `next.config.js`
441
+ - [ ] Remove `middleware.ts` (use `beforeLoad` guards)
442
+ - [ ] Remove `next/image` usage (use standard `<img>` or Cloudflare Images)
443
+ - [ ] Remove `next/link` (use TanStack Router `<Link>`)
444
+ - [ ] Remove `next/navigation` (use TanStack Router hooks)
445
+ - [ ] Remove `next/headers` usage
446
+
447
+ ---
448
+
449
+ ## Related Patterns
450
+
451
+ - **[API Route Handlers](./tanstack-cloudflare.api-route-handlers.md)**: Target API route pattern
452
+ - **[SSR Preload](./tanstack-cloudflare.ssr-preload.md)**: Replaces async Server Components
453
+ - **[Auth Session Management](./tanstack-cloudflare.auth-session-management.md)**: Replaces `cookies()` auth pattern
454
+ - **[Wrangler Configuration](./tanstack-cloudflare.wrangler-configuration.md)**: Replaces `vercel.json` + `next.config.js`
455
+
456
+ ---
457
+
458
+ **Status**: Stable - Comprehensive migration reference
459
+ **Recommendation**: Use as a lookup guide during Next.js → TanStack Start migration
460
+ **Last Updated**: 2026-02-28
461
+ **Contributors**: Patrick Michaelsen
@@ -0,0 +1,151 @@
1
+ # Notifications Engine
2
+
3
+ **Category**: Architecture
4
+ **Applicable To**: Real-time push notifications via WebSocket with multi-tab sync, exponential backoff, and FCM fallback for offline users
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ A three-layer notification system: NotificationsEngine (client WebSocket with event subscriptions), NotificationHub (per-user Durable Object broadcasting to all connected tabs), and NotificationTriggers (server-side delivery with WebSocket-first, FCM-fallback strategy). Notifications flow in real-time to all open tabs; when the user is offline, FCM push notifications deliver instead.
12
+
13
+ ---
14
+
15
+ ## Implementation
16
+
17
+ ### NotificationsEngine (Client)
18
+
19
+ **File**: `src/lib/notifications/notifications-engine.ts`
20
+
21
+ ```typescript
22
+ type NotificationEventType = 'notification' | 'notification_read' | 'notification_removed'
23
+ | 'unread_count' | 'connection_change'
24
+
25
+ class NotificationsEngine {
26
+ private ws: WebSocket | null = null
27
+ private handlers: Map<NotificationEventType, Set<EventHandler>> = new Map()
28
+ private reconnectAttempts = 0
29
+ private maxReconnectAttempts = 10
30
+ private reconnectDelay = 1000
31
+ private intentionalClose = false
32
+
33
+ connect() {
34
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
35
+ this.ws = new WebSocket(`${protocol}//${location.host}/api/notifications-ws`)
36
+ this.ws.onopen = () => { this.reconnectAttempts = 0; this.emit('connection_change', { connected: true }) }
37
+ this.ws.onmessage = (e) => { const data = JSON.parse(e.data); this.emit(data.type, data) }
38
+ this.ws.onclose = () => { this.emit('connection_change', { connected: false }); this.attemptReconnect() }
39
+ }
40
+
41
+ on<T extends NotificationEventType>(event: T, handler: EventHandler<T>): () => void {
42
+ if (!this.handlers.has(event)) this.handlers.set(event, new Set())
43
+ this.handlers.get(event)!.add(handler)
44
+ return () => { this.handlers.get(event)?.delete(handler) } // Unsubscribe
45
+ }
46
+
47
+ disconnect() { this.intentionalClose = true; this.ws?.close() }
48
+ }
49
+ ```
50
+
51
+ Backoff: `1000 * 2^(attempt-1)` — 1s, 2s, 4s, ... 512s, then give up after 10 attempts.
52
+
53
+ ### NotificationHub (Server — Durable Object)
54
+
55
+ **File**: `src/durable-objects/NotificationHub.ts`
56
+
57
+ ```typescript
58
+ class NotificationHub extends DurableObject {
59
+ private sessions: Set<WebSocket> = new Set()
60
+
61
+ async fetch(request: Request) {
62
+ if (url.pathname === '/broadcast') {
63
+ const event = await request.json()
64
+ this.broadcast(event) // Send to all connected tabs
65
+ return new Response('ok')
66
+ }
67
+ if (url.pathname === '/connected') {
68
+ return Response.json({ connected: this.sessions.size > 0, count: this.sessions.size })
69
+ }
70
+ // WebSocket upgrade
71
+ const [client, server] = Object.values(new WebSocketPair())
72
+ this.ctx.acceptWebSocket(server)
73
+ this.sessions.add(server)
74
+ return new Response(null, { status: 101, webSocket: client })
75
+ }
76
+
77
+ private broadcast(event: NotificationEvent) {
78
+ for (const ws of this.sessions) {
79
+ try { ws.send(JSON.stringify(event)) }
80
+ catch { this.sessions.delete(ws) } // Clean dead connections
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ One DO instance per user (`idFromName(userId)`). Push-only channel — clients don't send messages.
87
+
88
+ ### NotificationTriggers (Delivery Strategy)
89
+
90
+ **File**: `src/services/notification-triggers.service.ts`
91
+
92
+ ```typescript
93
+ private static async deliver(recipientId, notification, pushData?, env?) {
94
+ if (env) {
95
+ const connected = await NotificationHubService.isUserConnected(env, recipientId)
96
+ if (connected) {
97
+ // In-app: WebSocket only (updates bell badge instantly)
98
+ await NotificationHubService.pushNotification(env, recipientId, notification)
99
+ return
100
+ }
101
+ }
102
+ // Offline: FCM push notification
103
+ await FcmService.sendToUser(recipientId, { title: notification.title, body: notification.message })
104
+ }
105
+ ```
106
+
107
+ ### Multi-Tab Sync
108
+
109
+ When any tab marks a notification as read:
110
+ 1. API updates Firestore
111
+ 2. API broadcasts `notification_read` to NotificationHub
112
+ 3. Hub sends to ALL connected tabs
113
+ 4. Each tab's `engine.on('notification_read')` decrements unread count
114
+
115
+ ### Component Integration
116
+
117
+ ```typescript
118
+ function NotificationBell() {
119
+ const [unreadCount, setUnreadCount] = useState(0)
120
+ const engineRef = useRef<NotificationsEngine | null>(null)
121
+
122
+ useEffect(() => {
123
+ if (!user?.uid) return
124
+ const engine = new NotificationsEngine(user.uid)
125
+
126
+ engine.on('notification', () => setUnreadCount(c => c + 1))
127
+ engine.on('notification_read', () => setUnreadCount(c => Math.max(0, c - 1)))
128
+ engine.on('notification_removed', () => refetchCount())
129
+
130
+ engine.connect()
131
+ engineRef.current = engine
132
+ return () => engine.disconnect()
133
+ }, [user?.uid])
134
+ }
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Checklist
140
+
141
+ - [ ] One NotificationHub DO per user (`idFromName(userId)`)
142
+ - [ ] Engine `on()` returns unsubscribe function — call it on unmount
143
+ - [ ] Delivery checks WebSocket connectivity first, falls back to FCM
144
+ - [ ] API endpoints broadcast changes for multi-tab sync
145
+ - [ ] Push-only WebSocket — clients receive, never send
146
+
147
+ ---
148
+
149
+ **Status**: Stable
150
+ **Last Updated**: 2026-03-14
151
+ **Contributors**: Community
@@ -0,0 +1,90 @@
1
+ # OAuth Token Refresh Queue
2
+
3
+ **Category**: Architecture
4
+ **Applicable To**: Proactive OAuth credential rotation with cron-driven queue processing and per-provider refresh logic
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ A Firestore-backed queue system for proactive OAuth token refresh. After OAuth callback, credentials are enqueued with a `next_refresh_at` timestamp (e.g., 10 minutes before expiry). A cron job queries the queue for expiring entries, performs per-provider token rotation (Google refresh_token exchange, Instagram stateless refresh), updates credentials, and re-enqueues with the next refresh time.
12
+
13
+ ---
14
+
15
+ ## Implementation
16
+
17
+ **File**: `src/services/oauth-refresh.service.ts`
18
+
19
+ ### Queue Entry
20
+
21
+ ```typescript
22
+ interface RefreshQueueEntry {
23
+ user_id: string
24
+ provider: string
25
+ next_refresh_at: string // ISO timestamp
26
+ created_at: string
27
+ updated_at: string
28
+ }
29
+ // Collection: oauth-refresh-queue
30
+ // Document ID: {userId}_{provider}
31
+ ```
32
+
33
+ ### Service Methods
34
+
35
+ ```typescript
36
+ class OAuthRefreshService {
37
+ // Called after OAuth callback — schedule first refresh
38
+ static async enqueueRefresh(userId, provider, nextRefreshAt): Promise<void>
39
+
40
+ // Called on disconnect — remove from queue
41
+ static async dequeueRefresh(userId, provider): Promise<void>
42
+
43
+ // Called by cron — process all expiring credentials
44
+ static async refreshExpiringCredentials(): Promise<RefreshResult[]>
45
+ }
46
+ ```
47
+
48
+ ### Cron Processing Flow
49
+
50
+ ```
51
+ Cron trigger (every minute)
52
+ → Query: oauth-refresh-queue WHERE next_refresh_at <= now (limit 100)
53
+ → For each entry:
54
+ ├─ Load credentials from users/{userId}/credentials/{provider}
55
+ ├─ Per-provider refresh:
56
+ │ ├─ Google/YouTube: POST oauth2.googleapis.com/token with refresh_token
57
+ │ └─ Instagram: GET graph.instagram.com/refresh_access_token
58
+ ├─ Save new credentials (access_token, expires_at)
59
+ ├─ Update integration timestamps (last_refreshed_at, next_refresh_at)
60
+ └─ Re-enqueue with new next_refresh_at
61
+ ```
62
+
63
+ ### Per-Provider Timing
64
+
65
+ | Provider | Refresh Timing | Token Lifetime |
66
+ |---|---|---|
67
+ | Google/YouTube | `expiresIn - 600s` (10 min before) | ~1 hour |
68
+ | Instagram | 50 days | 60 days |
69
+
70
+ ### Error Handling
71
+
72
+ - Expired/revoked tokens: dequeue + return `{ status: 'failed' }`
73
+ - HTTP errors: log + return `{ status: 'failed', error: httpStatus }`
74
+ - Network errors: log + return `{ status: 'failed' }` (retried next cron cycle)
75
+
76
+ ---
77
+
78
+ ## Checklist
79
+
80
+ - [ ] OAuth callback enqueues refresh with provider-specific timing
81
+ - [ ] Disconnect dequeues the entry
82
+ - [ ] Cron processes max 100 entries per cycle
83
+ - [ ] Failed refreshes are dequeued (user must re-authenticate)
84
+ - [ ] Successful refreshes re-enqueue with next timing
85
+
86
+ ---
87
+
88
+ **Status**: Stable
89
+ **Last Updated**: 2026-03-14
90
+ **Contributors**: Community