@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,387 @@
1
+ # Auth Session Management Pattern
2
+
3
+ **Category**: Architecture
4
+ **Applicable To**: TanStack Start + Cloudflare Workers applications with authentication
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ This pattern provides cookie-based session management for TanStack Start applications running on Cloudflare Workers. It uses Firebase Admin SDK for token verification, HTTP-only session cookies for security, and a server function (`getAuthSession`) that can be called from any component, route, or API handler to get the current authenticated user.
12
+
13
+ The pattern enforces server-side-only authentication — all auth checks happen on the server, never on the client. This prevents token exposure and ensures that authentication state is always authoritative.
14
+
15
+ ---
16
+
17
+ ## When to Use This Pattern
18
+
19
+ ✅ **Use this pattern when:**
20
+ - Building authenticated TanStack Start applications
21
+ - Need cookie-based session management
22
+ - Using Firebase Authentication as the identity provider
23
+ - Need a universal `getAuthSession()` function callable from any context
24
+ - Want server-side auth enforcement (never client-side token verification)
25
+
26
+ ❌ **Don't use this pattern when:**
27
+ - Building public-only applications with no authentication
28
+ - Using a different auth provider that handles sessions differently (Auth0, Clerk)
29
+ - Building static sites
30
+
31
+ ---
32
+
33
+ ## Core Principles
34
+
35
+ 1. **Server-Side Only**: All authentication verification happens on the server — never verify tokens client-side
36
+ 2. **Cookie-Based Sessions**: Use HTTP-only cookies to store session tokens (not localStorage)
37
+ 3. **Universal Server Function**: `getAuthSession()` is a `createServerFn` callable from any context
38
+ 4. **Graceful Fallback**: Auth failures return `null`, never throw — callers decide how to handle
39
+ 5. **Session Cookie Exchange**: Exchange short-lived ID tokens for long-lived session cookies (14 days)
40
+ 6. **Request-Based Verification**: `getServerSession(request)` extracts and verifies the cookie from the request
41
+
42
+ ---
43
+
44
+ ## Implementation
45
+
46
+ ### Structure
47
+
48
+ ```
49
+ src/
50
+ ├── lib/
51
+ │ └── auth/
52
+ │ ├── session.ts # Server-side session verification
53
+ │ └── server-fn.ts # createServerFn wrapper for components
54
+ ├── types/
55
+ │ └── auth.ts # User and ServerSession types
56
+ ├── components/
57
+ │ └── auth/
58
+ │ ├── AuthContext.tsx # React context for auth state
59
+ │ └── AuthForm.tsx # Login/register UI
60
+ └── routes/
61
+ ├── __root.tsx # Root layout with auth initialization
62
+ ├── auth.tsx # Auth page
63
+ └── api/
64
+ └── auth/
65
+ ├── session.tsx # POST: Create session cookie
66
+ └── logout.tsx # POST: Destroy session
67
+ ```
68
+
69
+ ### Code Example
70
+
71
+ #### Step 1: Define Auth Types
72
+
73
+ ```typescript
74
+ // src/types/auth.ts
75
+ export interface User {
76
+ uid: string
77
+ email: string | null
78
+ displayName: string | null
79
+ photoURL: string | null
80
+ emailVerified: boolean
81
+ }
82
+
83
+ export interface ServerSession {
84
+ user: User
85
+ }
86
+ ```
87
+
88
+ #### Step 2: Server-Side Session Verification
89
+
90
+ ```typescript
91
+ // src/lib/auth/session.ts
92
+ import { verifyIdToken, verifySessionCookie, createSessionCookie as createFirebaseSessionCookie }
93
+ from '@prmichaelsen/firebase-admin-sdk-v8'
94
+ import type { User, ServerSession } from '@/types/auth'
95
+
96
+ /**
97
+ * Extract session cookie from request headers
98
+ */
99
+ function getSessionCookie(request: Request): string | undefined {
100
+ const cookieHeader = request.headers.get('cookie')
101
+ if (!cookieHeader) return undefined
102
+
103
+ const cookies = cookieHeader.split(';').reduce((acc, cookie) => {
104
+ const [name, value] = cookie.trim().split('=')
105
+ acc[name] = value
106
+ return acc
107
+ }, {} as Record<string, string>)
108
+
109
+ return cookies.session
110
+ }
111
+
112
+ /**
113
+ * Get authenticated user from request
114
+ * Returns null if not authenticated — never throws
115
+ */
116
+ export async function getServerSession(request: Request): Promise<ServerSession | null> {
117
+ try {
118
+ const sessionCookie = getSessionCookie(request)
119
+ if (!sessionCookie) return null
120
+
121
+ // Verify session cookie (try session cookie first, fallback to ID token)
122
+ let decodedToken
123
+ try {
124
+ decodedToken = await verifySessionCookie(sessionCookie)
125
+ } catch {
126
+ decodedToken = await verifyIdToken(sessionCookie)
127
+ }
128
+
129
+ const user: User = {
130
+ uid: decodedToken.sub,
131
+ email: decodedToken.email || null,
132
+ displayName: decodedToken.name || null,
133
+ photoURL: decodedToken.picture || null,
134
+ emailVerified: decodedToken.email_verified || false,
135
+ }
136
+
137
+ return { user }
138
+ } catch (error) {
139
+ console.error('Failed to get server session', error)
140
+ return null
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Create a long-lived session cookie from a Firebase ID token
146
+ */
147
+ export async function createSessionCookie(idToken: string): Promise<string> {
148
+ return createFirebaseSessionCookie(idToken, {
149
+ expiresIn: 60 * 60 * 24 * 14 * 1000 // 14 days
150
+ })
151
+ }
152
+ ```
153
+
154
+ #### Step 3: Universal Server Function
155
+
156
+ ```typescript
157
+ // src/lib/auth/server-fn.ts
158
+ import { createServerFn } from '@tanstack/react-start'
159
+ import { getRequest } from '@tanstack/react-start/server'
160
+ import { getServerSession } from '@/lib/auth/session'
161
+ import { initFirebaseAdmin } from '@/lib/firebase-admin'
162
+
163
+ /**
164
+ * Server function to get current auth session.
165
+ * Callable from any component, route, or server context.
166
+ */
167
+ export const getAuthSession = createServerFn({ method: 'GET' }).handler(async () => {
168
+ try {
169
+ initFirebaseAdmin()
170
+ const session = await getServerSession(getRequest())
171
+ return session?.user || null
172
+ } catch (error) {
173
+ console.error('[getAuthSession] Error:', error)
174
+ return null
175
+ }
176
+ })
177
+ ```
178
+
179
+ #### Step 4: Root Layout with Auth Initialization
180
+
181
+ ```typescript
182
+ // src/routes/__root.tsx
183
+ import { createRootRouteWithContext } from '@tanstack/react-router'
184
+ import { getAuthSession } from '@/lib/auth/server-fn'
185
+ import { AuthProvider } from '@/components/auth/AuthContext'
186
+
187
+ export const Route = createRootRouteWithContext()({
188
+ beforeLoad: async () => {
189
+ // Fetch auth session server-side (SSR)
190
+ const user = await getAuthSession()
191
+ return { user }
192
+ },
193
+ component: RootLayout,
194
+ })
195
+
196
+ function RootLayout() {
197
+ const { user } = Route.useRouteContext()
198
+
199
+ return (
200
+ <AuthProvider initialUser={user}>
201
+ <Outlet />
202
+ </AuthProvider>
203
+ )
204
+ }
205
+ ```
206
+
207
+ #### Step 5: Auth Context Provider
208
+
209
+ ```typescript
210
+ // src/components/auth/AuthContext.tsx
211
+ import { createContext, useContext, useState } from 'react'
212
+ import type { User } from '@/types/auth'
213
+
214
+ interface AuthContextType {
215
+ user: User | null
216
+ setUser: (user: User | null) => void
217
+ }
218
+
219
+ const AuthContext = createContext<AuthContextType | null>(null)
220
+
221
+ export function AuthProvider({ initialUser, children }: {
222
+ initialUser: User | null
223
+ children: React.ReactNode
224
+ }) {
225
+ const [user, setUser] = useState<User | null>(initialUser)
226
+
227
+ return (
228
+ <AuthContext.Provider value={{ user, setUser }}>
229
+ {children}
230
+ </AuthContext.Provider>
231
+ )
232
+ }
233
+
234
+ export function useAuth(): AuthContextType {
235
+ const ctx = useContext(AuthContext)
236
+ if (!ctx) throw new Error('useAuth must be used within AuthProvider')
237
+ return ctx
238
+ }
239
+ ```
240
+
241
+ #### Step 6: Protected Route with Redirect
242
+
243
+ ```typescript
244
+ // src/routes/profile.tsx
245
+ import { createFileRoute, redirect } from '@tanstack/react-router'
246
+ import { getAuthSession } from '@/lib/auth/server-fn'
247
+
248
+ export const Route = createFileRoute('/profile')({
249
+ beforeLoad: async () => {
250
+ const user = await getAuthSession()
251
+
252
+ if (!user) {
253
+ throw redirect({
254
+ to: '/auth',
255
+ search: { redirect_url: '/profile' },
256
+ })
257
+ }
258
+
259
+ return { user }
260
+ },
261
+ component: ProfilePage,
262
+ })
263
+ ```
264
+
265
+ #### Step 7: Session Creation API Route
266
+
267
+ ```typescript
268
+ // src/routes/api/auth/session.tsx
269
+ import { createFileRoute } from '@tanstack/react-router'
270
+ import { createSessionCookie } from '@/lib/auth/session'
271
+
272
+ export const Route = createFileRoute('/api/auth/session')({
273
+ server: {
274
+ handlers: {
275
+ POST: async ({ request }) => {
276
+ try {
277
+ const { idToken } = await request.json()
278
+ const sessionCookie = await createSessionCookie(idToken)
279
+
280
+ return new Response(JSON.stringify({ success: true }), {
281
+ status: 200,
282
+ headers: {
283
+ 'Content-Type': 'application/json',
284
+ 'Set-Cookie': `session=${sessionCookie}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${60 * 60 * 24 * 14}`,
285
+ },
286
+ })
287
+ } catch (error) {
288
+ return new Response(JSON.stringify({ error: 'Failed to create session' }), {
289
+ status: 401,
290
+ headers: { 'Content-Type': 'application/json' },
291
+ })
292
+ }
293
+ },
294
+ },
295
+ },
296
+ })
297
+ ```
298
+
299
+ ---
300
+
301
+ ## Benefits
302
+
303
+ ### 1. Universal Auth Check
304
+ `getAuthSession()` works in components, `beforeLoad`, API routes, and server functions — one function everywhere.
305
+
306
+ ### 2. Secure by Default
307
+ HTTP-only cookies prevent XSS attacks. Server-side verification prevents token tampering.
308
+
309
+ ### 3. SSR Compatible
310
+ Auth state is available during server-side rendering via `beforeLoad`, enabling instant authenticated content.
311
+
312
+ ### 4. Graceful Degradation
313
+ Auth failures return `null` instead of throwing, preventing cascading errors.
314
+
315
+ ---
316
+
317
+ ## Trade-offs
318
+
319
+ ### 1. Firebase Dependency
320
+ **Downside**: Tightly coupled to Firebase Admin SDK for token verification.
321
+ **Mitigation**: Wrap in an interface if you anticipate switching auth providers.
322
+
323
+ ### 2. Cookie Size Limits
324
+ **Downside**: Session cookies have size limits (~4KB).
325
+ **Mitigation**: Store minimal data in the cookie (just the session token), not user profile data.
326
+
327
+ ---
328
+
329
+ ## Anti-Patterns
330
+
331
+ ### ❌ Anti-Pattern 1: Client-Side Token Verification
332
+
333
+ ```typescript
334
+ // ❌ BAD: Verifying tokens on the client
335
+ function MyComponent() {
336
+ const token = localStorage.getItem('token')
337
+ const user = jwt.decode(token) // Client-side decode — not verified!
338
+ }
339
+
340
+ // ✅ GOOD: Always verify on server
341
+ const user = await getAuthSession()
342
+ ```
343
+
344
+ ### ❌ Anti-Pattern 2: Throwing on Auth Failure
345
+
346
+ ```typescript
347
+ // ❌ BAD: Throwing prevents page from loading
348
+ export async function getServerSession(request) {
349
+ const cookie = getSessionCookie(request)
350
+ if (!cookie) throw new Error('Not authenticated') // Crashes!
351
+ }
352
+
353
+ // ✅ GOOD: Return null, let callers decide
354
+ export async function getServerSession(request) {
355
+ const cookie = getSessionCookie(request)
356
+ if (!cookie) return null // Graceful
357
+ }
358
+ ```
359
+
360
+ ---
361
+
362
+ ## Related Patterns
363
+
364
+ - **[API Route Handlers](./tanstack-cloudflare.api-route-handlers.md)**: API routes use `getAuthSession()` for auth
365
+ - **[SSR Preload Pattern](./tanstack-cloudflare.ssr-preload.md)**: `beforeLoad` uses auth for user-specific data
366
+ - **[Rate Limiting](./tanstack-cloudflare.rate-limiting.md)**: Rate limit auth endpoints
367
+
368
+ ---
369
+
370
+ ## Checklist for Implementation
371
+
372
+ - [ ] `getServerSession(request)` returns `ServerSession | null`
373
+ - [ ] `getAuthSession` is a `createServerFn` wrapper
374
+ - [ ] Session cookie is HTTP-only, Secure, SameSite=Lax
375
+ - [ ] Root layout fetches auth in `beforeLoad`
376
+ - [ ] AuthProvider wraps app with `initialUser` from SSR
377
+ - [ ] Protected routes use `redirect` to `/auth` when not authenticated
378
+ - [ ] API routes check `getAuthSession()` before processing
379
+ - [ ] Auth failures never throw — always return null
380
+ - [ ] Firebase Admin SDK initialized before verification
381
+
382
+ ---
383
+
384
+ **Status**: Stable - Proven pattern for TanStack Start authentication
385
+ **Recommendation**: Use for all authenticated TanStack Start applications
386
+ **Last Updated**: 2026-02-28
387
+ **Contributors**: Patrick Michaelsen
@@ -0,0 +1,271 @@
1
+ # Card, NotificationCard & CardList
2
+
3
+ **Category**: Design
4
+ **Applicable To**: All card-based data display, notification items with swipe-to-dismiss, and generic feed/list rendering
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ Cards are the primary data display unit across the app. This pattern covers the standard card styling, NotificationCard with swipe-to-dismiss gesture, and CardList (FeedList) — a generic list component that handles loading skeletons, empty states, and error banners for any card type.
12
+
13
+ ---
14
+
15
+ ## Implementation
16
+
17
+ ### Card (Standard Styling)
18
+
19
+ All cards in the app follow consistent styling:
20
+
21
+ ```typescript
22
+ // Standard card container
23
+ <div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-4">
24
+ {/* Card content */}
25
+ </div>
26
+
27
+ // With hover effect
28
+ <div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-4
29
+ hover:border-blue-500/50 transition-colors cursor-pointer">
30
+ {/* Clickable card */}
31
+ </div>
32
+
33
+ // Highlighted/active
34
+ <div className="bg-purple-900/20 border border-purple-400/60 rounded-xl p-4 shadow-purple-500/10">
35
+ {/* Active card */}
36
+ </div>
37
+ ```
38
+
39
+ **Text Hierarchy**:
40
+ - Title: `text-white font-semibold`
41
+ - Subtitle/secondary: `text-gray-400 text-sm`
42
+ - Meta/timestamp: `text-gray-500 text-xs`
43
+
44
+ **Action Buttons** (icon buttons within cards):
45
+ - Base: `p-2 text-gray-400 hover:text-white hover:bg-gray-700/50 rounded transition-colors`
46
+ - Danger: `text-red-400 hover:text-red-300`
47
+ - Disabled: `opacity-50 cursor-not-allowed`
48
+
49
+ **State Variants**:
50
+ - Deleted/faded: `opacity-20 transition-opacity duration-200`
51
+ - Loading: `animate-pulse bg-gray-800`
52
+
53
+ ---
54
+
55
+ ### NotificationCard
56
+
57
+ **File**: `src/components/notifications/NotificationCard.tsx`
58
+
59
+ ```typescript
60
+ interface NotificationCardProps {
61
+ notification: Notification
62
+ onMarkAsRead: (id: string) => void
63
+ onOpenFriendRequest?: (notification: Notification) => void
64
+ }
65
+ ```
66
+
67
+ **Features**:
68
+ - Unread indicator: blue dot (w-2 h-2) on left side
69
+ - Avatar circle (w-10 h-10) or type-specific icon with colored background
70
+ - Content: title (truncated), message (line-clamp-2), relative timestamp
71
+ - Message count badge (top-right, blue pill)
72
+ - **Swipe-to-dismiss**: horizontal swipe >80px threshold
73
+ - Reveal background: `bg-blue-600/30`
74
+ - Smooth dismiss animation (300ms translateX)
75
+ - Calls `onMarkAsRead` on dismiss
76
+ - Type-specific icons: `friend_request`, `friend_accepted`, `group_invite`, `new_message`, `system`, `memory_published`, `memory_comment`, `organize_nudge`
77
+
78
+ ---
79
+
80
+ ### CardList (Generic Feed Primitive)
81
+
82
+ **File**: `src/components/feed/FeedList.tsx`
83
+
84
+ ```typescript
85
+ interface CardListProps<T> {
86
+ items: T[]
87
+ loading: boolean
88
+ error: string | null
89
+ renderItem: (item: T, index: number) => ReactNode
90
+ emptyIcon: ReactNode
91
+ emptyMessage: string
92
+ skeletonCount?: number // default: 4
93
+ }
94
+
95
+ export function FeedList<T>({ items, loading, error, renderItem, emptyIcon, emptyMessage, skeletonCount = 4 }: CardListProps<T>) {
96
+ // Error state: red banner
97
+ if (error) return <div className="text-red-400 p-4 ...">{error}</div>
98
+
99
+ // Loading state: skeleton cards
100
+ if (loading) return (
101
+ <div className="space-y-2">
102
+ {Array.from({ length: skeletonCount }).map((_, i) => (
103
+ <div key={i} className="bg-gray-800 rounded-xl h-32 animate-pulse" />
104
+ ))}
105
+ </div>
106
+ )
107
+
108
+ // Empty state: centered icon + message
109
+ if (items.length === 0) return (
110
+ <div className="flex flex-col items-center justify-center py-12 text-gray-500">
111
+ {emptyIcon}
112
+ <p className="mt-2">{emptyMessage}</p>
113
+ </div>
114
+ )
115
+
116
+ // Items
117
+ return <div className="space-y-2">{items.map(renderItem)}</div>
118
+ }
119
+ ```
120
+
121
+ **Usage**:
122
+
123
+ ```typescript
124
+ <FeedList
125
+ items={conversations}
126
+ loading={loading}
127
+ error={error}
128
+ renderItem={(conv, i) => <ConversationCard key={conv.id} conversation={conv} />}
129
+ emptyIcon={<MessageSquare className="w-12 h-12 text-gray-600" />}
130
+ emptyMessage="No conversations yet"
131
+ skeletonCount={6}
132
+ />
133
+ ```
134
+
135
+ ---
136
+
137
+ ### Virtualized Lists (react-virtuoso)
138
+
139
+ For feeds with many items, use `react-virtuoso` instead of mapping all items to DOM. Two usage patterns exist:
140
+
141
+ #### Pattern A: Window-Scroll Feed (Memories, Spaces, Groups, Profiles)
142
+
143
+ Used for page-level feeds where the entire page scrolls. `useWindowScroll` delegates scrolling to the browser window.
144
+
145
+ ```typescript
146
+ import { Virtuoso } from 'react-virtuoso'
147
+
148
+ <Virtuoso
149
+ useWindowScroll
150
+ data={memories}
151
+ endReached={() => {
152
+ if (hasMore && !loading && !loadingMore) {
153
+ loadFeed(false) // Append next page
154
+ }
155
+ }}
156
+ itemContent={(index, memory) => (
157
+ <div className="pb-2">
158
+ <MemoryCard memory={memory} source={source} />
159
+ </div>
160
+ )}
161
+ components={{
162
+ Footer: () =>
163
+ loadingMore ? (
164
+ <div className="flex justify-center py-4">
165
+ <Loader2 className="w-5 h-5 animate-spin text-gray-500" />
166
+ </div>
167
+ ) : null,
168
+ }}
169
+ />
170
+ ```
171
+
172
+ **Key props**:
173
+ - `useWindowScroll`: Scroll container is the browser window, not Virtuoso's own div
174
+ - `data`: The array of items to render
175
+ - `endReached`: Callback when user scrolls to the bottom — trigger load-more
176
+ - `itemContent`: Render function per item (wraps card in `pb-2` for gap)
177
+ - `components.Footer`: Loading spinner while fetching next page
178
+
179
+ **Used in**: `/memories`, `SpacesFeed`, `ProfileMemoriesFeed`, `GroupMemories`
180
+
181
+ #### Pattern B: Container-Scroll Chat (MessageList)
182
+
183
+ Used for chat where messages prepend from the top and the container has a fixed height.
184
+
185
+ ```typescript
186
+ import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
187
+
188
+ const virtuosoRef = useRef<VirtuosoHandle>(null)
189
+
190
+ <Virtuoso
191
+ ref={virtuosoRef}
192
+ className="flex-grow h-0"
193
+ firstItemIndex={firstItemIndex}
194
+ initialTopMostItemIndex={items.length - 1}
195
+ data={items}
196
+ startReached={() => {
197
+ if (!isLoadingMore && hasMore && onLoadMore) {
198
+ setIsLoadingMore(true)
199
+ onLoadMore()
200
+ }
201
+ }}
202
+ itemContent={(index, item) => <Message ... />}
203
+ />
204
+
205
+ // Programmatic scroll to bottom
206
+ virtuosoRef.current?.scrollToIndex({ index: 'LAST', behavior: 'smooth' })
207
+ ```
208
+
209
+ **Key props**:
210
+ - `firstItemIndex`: Enables stable prepend — set to a large number minus item count, decrement as older messages load
211
+ - `initialTopMostItemIndex`: Start at bottom (`items.length - 1`)
212
+ - `startReached`: Callback when user scrolls to the top — load older messages
213
+ - `ref` (`VirtuosoHandle`): Exposes `scrollToIndex` for programmatic scroll (new message arrival, search result navigation)
214
+
215
+ **Used in**: `MessageList`
216
+
217
+ #### When to Use Each
218
+
219
+ | Pattern | When | Scroll Container |
220
+ |---|---|---|
221
+ | FeedList (non-virtualized) | < ~50 items, simple lists | Parent div |
222
+ | Virtuoso `useWindowScroll` | Feed pages with infinite scroll | Browser window |
223
+ | Virtuoso container-scroll | Chat with prepend, fixed height | Virtuoso div |
224
+
225
+ ---
226
+
227
+ ## Anti-Patterns
228
+
229
+ ### Inconsistent Card Styling
230
+
231
+ ```typescript
232
+ // Bad: Custom card styling that doesn't match the system
233
+ <div className="bg-white rounded-md p-2 shadow">{content}</div>
234
+
235
+ // Good: Use standard card classes
236
+ <div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-4">
237
+ {content}
238
+ </div>
239
+ ```
240
+
241
+ ### Reimplementing Loading/Empty/Error States
242
+
243
+ ```typescript
244
+ // Bad: Custom loading/empty per page
245
+ {loading ? <Spinner /> : items.length === 0 ? <p>Empty</p> : items.map(...)}
246
+
247
+ // Good: Use CardList/FeedList
248
+ <FeedList items={items} loading={loading} error={error}
249
+ renderItem={(item) => <MyCard item={item} />}
250
+ emptyIcon={<Icon />} emptyMessage="Nothing here" />
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Checklist
256
+
257
+ - [ ] Cards use `bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-4`
258
+ - [ ] Text hierarchy follows: white (title), gray-400 (subtitle), gray-500 (meta)
259
+ - [ ] Use `FeedList` for small static lists with loading/empty/error states
260
+ - [ ] Use `Virtuoso` with `useWindowScroll` for feed pages with infinite scroll
261
+ - [ ] Use `Virtuoso` container-scroll with `firstItemIndex` for chat-style prepend lists
262
+ - [ ] Wrap each Virtuoso item in `<div className="pb-2">` for consistent card gap
263
+ - [ ] Provide `components.Footer` with loading spinner for load-more feedback
264
+ - [ ] Swipe-to-dismiss uses 80px threshold with reveal background
265
+ - [ ] Deleted/faded items use `opacity-20` transition
266
+
267
+ ---
268
+
269
+ **Status**: Stable
270
+ **Last Updated**: 2026-03-14
271
+ **Contributors**: Community