@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,550 @@
1
+ # Firebase Firestore
2
+
3
+ **Category**: Code
4
+ **Applicable To**: All Firestore CRUD operations, DatabaseService classes, collection path helpers, query patterns, and SSR/API data access
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ This pattern covers how Firestore is used via `@prmichaelsen/firebase-admin-sdk-v8` for all database operations: initialization, CRUD via static DatabaseService classes, centralized collection path helpers, query building (where, orderBy, pagination, chunked `in`), Zod validation on all reads, and the two contexts where Firestore is accessed (API route handlers and SSR `beforeLoad`). There are 37+ DatabaseService classes following this pattern.
12
+
13
+ ---
14
+
15
+ ## When to Use This Pattern
16
+
17
+ **Use this pattern when:**
18
+ - Creating a new DatabaseService for a Firestore entity
19
+ - Adding CRUD operations to an existing service
20
+ - Writing queries with filters, ordering, or pagination
21
+ - Accessing Firestore in API routes or SSR `beforeLoad`
22
+ - Adding new collection paths
23
+
24
+ **Don't use this pattern when:**
25
+ - Building client-side API wrappers (use `{Domain}Service` — see library-services.md)
26
+ - Working with Firebase Storage (see `tanstack-cloudflare.firebase-storage`)
27
+ - Working with external databases (Weaviate, Algolia)
28
+
29
+ ---
30
+
31
+ ## Core Principles
32
+
33
+ 1. **Always Initialize**: Call `initFirebaseAdmin()` before any Firestore operation
34
+ 2. **Always Validate Reads**: Every read uses Zod `safeParse()` — never return raw `any`
35
+ 3. **Reads Return Null, Writes Throw**: Consistent error handling across all services
36
+ 4. **ISO Timestamps**: Always `new Date().toISOString()` — never `Date.now()`
37
+ 5. **Collection Helpers**: Import paths from `@/constant/collections` — never hardcode
38
+ 6. **Static Methods Only**: No instances — all DatabaseService methods are `static`
39
+
40
+ ---
41
+
42
+ ## Implementation
43
+
44
+ ### SDK Functions
45
+
46
+ ```typescript
47
+ import {
48
+ getDocument, // Read single document by ID
49
+ setDocument, // Write/overwrite document (supports merge)
50
+ deleteDocument, // Delete document
51
+ addDocument, // Add with auto-generated or custom ID
52
+ queryDocuments, // Query with where, orderBy, limit, startAfter
53
+ updateDocument, // Atomic field updates (FieldValue operations)
54
+ FieldValue, // arrayRemove, arrayUnion, delete, serverTimestamp
55
+ } from '@prmichaelsen/firebase-admin-sdk-v8'
56
+ import type { QueryOptions } from '@prmichaelsen/firebase-admin-sdk-v8'
57
+ ```
58
+
59
+ ### Collection Path Helpers
60
+
61
+ **File**: `src/constant/collections.ts`
62
+
63
+ **Platform-level collections** (no user scoping):
64
+
65
+ ```typescript
66
+ export const USERS = `${BASE}.users`
67
+ export const PUBLIC_PROFILES = `${BASE}.public-profiles`
68
+ export const RELATIONSHIPS = `${BASE}.relationships`
69
+ export const SCHEDULED_MESSAGES = `${BASE}.scheduled-messages`
70
+ ```
71
+
72
+ **User-scoped subcollections** (with helper functions):
73
+
74
+ ```typescript
75
+ export function getUserProfileCollection(userId: string): string {
76
+ return `${BASE}.users/${userId}/profile`
77
+ }
78
+
79
+ export function getUserConversations(userId: string): string {
80
+ return `${BASE}.users/${userId}/conversations`
81
+ }
82
+
83
+ export function getUserConversationMessages(userId: string, conversationId: string): string {
84
+ return `${BASE}.users/${userId}/conversations/${conversationId}/messages`
85
+ }
86
+
87
+ export function getUserNotificationsCollection(userId: string): string {
88
+ return `${BASE}.users/${userId}/notifications`
89
+ }
90
+ ```
91
+
92
+ **Shared collections** (multi-user, not user-scoped):
93
+
94
+ ```typescript
95
+ export function getSharedConversations(): string {
96
+ return `${BASE}.conversations`
97
+ }
98
+
99
+ export function getSharedConversationMessages(conversationId: string): string {
100
+ return `${BASE}.conversations/${conversationId}/messages`
101
+ }
102
+ ```
103
+
104
+ ### CRUD Operations
105
+
106
+ #### Read — `getDocument()`
107
+
108
+ ```typescript
109
+ static async getProfile(userId: string): Promise<UserProfile | null> {
110
+ try {
111
+ const collection = getUserProfileCollection(userId)
112
+ const doc = await getDocument(collection, PROFILE_DOC_ID)
113
+ if (!doc) return null
114
+
115
+ const result = UserProfileSchema.safeParse(doc)
116
+ if (!result.success) {
117
+ console.error('[ProfileDatabaseService] Invalid data:', result.error)
118
+ return null
119
+ }
120
+ return result.data
121
+ } catch (error) {
122
+ console.error('[ProfileDatabaseService] Failed to get:', error)
123
+ return null
124
+ }
125
+ }
126
+ ```
127
+
128
+ #### Create — `addDocument()`
129
+
130
+ ```typescript
131
+ static async createConversation(userId: string, data: CreateConversationInput): Promise<Conversation> {
132
+ const collection = getUserConversations(userId)
133
+ const now = new Date().toISOString()
134
+
135
+ const conversation = {
136
+ title: data.title,
137
+ type: data.type ?? 'chat',
138
+ created_at: now,
139
+ updated_at: now,
140
+ message_count: 0,
141
+ last_message_preview: '',
142
+ }
143
+
144
+ const docRef = await addDocument(collection, conversation)
145
+ return { id: docRef.id, user_id: userId, ...conversation }
146
+ }
147
+ ```
148
+
149
+ Optional custom document ID:
150
+
151
+ ```typescript
152
+ const docRef = await addDocument(collection, data, customDocumentId)
153
+ ```
154
+
155
+ #### Write — `setDocument()` (Full or Merge)
156
+
157
+ Full overwrite:
158
+
159
+ ```typescript
160
+ await setDocument(collection, docId, data)
161
+ ```
162
+
163
+ Merge (partial update):
164
+
165
+ ```typescript
166
+ await setDocument(collection, conversationId, {
167
+ title,
168
+ updated_at: new Date().toISOString(),
169
+ }, { merge: true })
170
+ ```
171
+
172
+ Selective merge (recommended for nested objects):
173
+
174
+ ```typescript
175
+ await setDocument(collection, conversationId, {
176
+ title,
177
+ updated_at: new Date().toISOString(),
178
+ }, { mergeFields: ['title', 'updated_at'] })
179
+ ```
180
+
181
+ #### Update — `updateDocument()` (FieldValue operations)
182
+
183
+ ```typescript
184
+ import { FieldValue } from '@prmichaelsen/firebase-admin-sdk-v8'
185
+
186
+ // Remove element from array
187
+ await updateDocument(sharedPath, doc.id, {
188
+ participant_user_ids: FieldValue.arrayRemove(userId),
189
+ })
190
+
191
+ // Add element to array (no duplicates)
192
+ await updateDocument(collection, docId, {
193
+ tags: FieldValue.arrayUnion('new-tag'),
194
+ })
195
+ ```
196
+
197
+ #### Delete — `deleteDocument()`
198
+
199
+ ```typescript
200
+ static async deleteProfile(userId: string): Promise<void> {
201
+ const collection = getUserProfileCollection(userId)
202
+ await deleteDocument(collection, PROFILE_DOC_ID)
203
+ }
204
+ ```
205
+
206
+ ### Query Patterns
207
+
208
+ #### Simple WHERE
209
+
210
+ ```typescript
211
+ const docs = await queryDocuments(collection, {
212
+ where: [{ field: 'status', op: '==', value: 'pending' }],
213
+ })
214
+ ```
215
+
216
+ #### WHERE + ORDER + LIMIT
217
+
218
+ ```typescript
219
+ const options: QueryOptions = {
220
+ where: [
221
+ { field: 'status', op: '==', value: 'pending' },
222
+ { field: 'scheduled_at', op: '<=', value: cutoffTime },
223
+ ],
224
+ orderBy: [{ field: 'scheduled_at', direction: 'ASCENDING' }],
225
+ limit: 10,
226
+ }
227
+
228
+ const docs = await queryDocuments(SCHEDULED_MESSAGES, options)
229
+ ```
230
+
231
+ #### Cursor Pagination with `startAfter`
232
+
233
+ ```typescript
234
+ const options: QueryOptions = {
235
+ orderBy: [{ field: 'timestamp', direction: 'DESCENDING' }],
236
+ limit,
237
+ }
238
+ if (startAfter) options.startAfter = [startAfter]
239
+
240
+ const results = await queryDocuments(messagesCollection, options)
241
+ ```
242
+
243
+ #### Chunked `in` Queries (30-item limit)
244
+
245
+ Firestore `in` operator supports max 30 values. Chunk and parallelize:
246
+
247
+ ```typescript
248
+ const chunks: string[][] = []
249
+ for (let i = 0; i < ids.length; i += 30) {
250
+ chunks.push(ids.slice(i, i + 30))
251
+ }
252
+
253
+ const results = await Promise.all(
254
+ chunks.map(async (chunk) => {
255
+ const docs = await queryDocuments(collection, {
256
+ where: [{ field: 'message_id', op: 'in', value: chunk }],
257
+ orderBy: [{ field: 'sequence_number', direction: 'ASCENDING' }],
258
+ })
259
+ return docs.map(doc => ({ id: doc.id, ...doc.data }))
260
+ })
261
+ )
262
+
263
+ const all = results.flat()
264
+ ```
265
+
266
+ #### Simulated OR (Multiple Queries + Merge)
267
+
268
+ Firestore has no OR operator. Run separate queries and merge:
269
+
270
+ ```typescript
271
+ // Query 1: Public messages (visible_to_user_ids is null)
272
+ const publicDocs = await queryDocuments(messagesCollection, {
273
+ orderBy: [{ field: 'timestamp', direction: 'DESCENDING' }],
274
+ limit,
275
+ where: [{ field: 'visible_to_user_ids', op: '==', value: null }],
276
+ })
277
+
278
+ // Query 2: Messages visible to this user
279
+ const privateDocs = await queryDocuments(messagesCollection, {
280
+ orderBy: [{ field: 'timestamp', direction: 'DESCENDING' }],
281
+ limit,
282
+ where: [{ field: 'visible_to_user_ids', op: 'array-contains', value: userId }],
283
+ })
284
+
285
+ // Merge, dedupe, re-sort
286
+ const merged = [...publicDocs, ...privateDocs]
287
+ const deduped = Array.from(new Map(merged.map(d => [d.id, d])).values())
288
+ const sorted = deduped.sort((a, b) =>
289
+ new Date(b.data.timestamp).getTime() - new Date(a.data.timestamp).getTime()
290
+ ).slice(0, limit)
291
+ ```
292
+
293
+ ### Query Result Parsing
294
+
295
+ Always validate query results with Zod and filter invalid documents:
296
+
297
+ ```typescript
298
+ const results = await queryDocuments(collection, options)
299
+
300
+ const valid = results
301
+ .map(doc => {
302
+ const result = EntitySchema.safeParse({ ...doc.data, id: doc.id })
303
+ if (!result.success) {
304
+ console.error('[ServiceName] Invalid data:', result.error)
305
+ return null
306
+ }
307
+ return result.data
308
+ })
309
+ .filter((item): item is Entity => item !== null)
310
+ ```
311
+
312
+ ### Document ID Patterns
313
+
314
+ | Strategy | Example | Use Case |
315
+ |---|---|---|
316
+ | Fixed ID | `'default'`, `'current'` | Singleton documents (profile, subscription, usage) |
317
+ | Provider-keyed | `'instagram'`, `'github'` | One doc per OAuth provider |
318
+ | Composite | `${userId}_${memoryId}` | Cross-entity lookups |
319
+ | Auto-generated | `addDocument(coll, data)` | Most entities (conversations, messages, notifications) |
320
+
321
+ ### Firestore in SSR vs API Routes
322
+
323
+ #### SSR `beforeLoad`
324
+
325
+ ```typescript
326
+ export const Route = createFileRoute('/some-page')({
327
+ beforeLoad: (async () => {
328
+ const user = await getAuthSession()
329
+ if (!user) throw redirect({ to: '/auth' })
330
+
331
+ let preloadData = null
332
+ if (typeof window === 'undefined') {
333
+ initFirebaseAdmin()
334
+ preloadData = await SomeDatabaseService.getData(user.uid)
335
+ }
336
+
337
+ return { initialUser: user, preloadData }
338
+ }) as any,
339
+ })
340
+ ```
341
+
342
+ #### API Routes
343
+
344
+ ```typescript
345
+ GET: async () => {
346
+ initFirebaseAdmin()
347
+
348
+ const user = await getAuthSession()
349
+ if (!user) return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })
350
+
351
+ const data = await SomeDatabaseService.getData(user.uid)
352
+ return new Response(JSON.stringify({ data }), {
353
+ status: 200,
354
+ headers: { 'Content-Type': 'application/json' },
355
+ })
356
+ }
357
+ ```
358
+
359
+ #### Fire-and-Forget (Non-Critical Operations)
360
+
361
+ ```typescript
362
+ // Don't block page load on optional operations
363
+ ProfileViewDatabaseService.trackProfileView(user.uid, targetUserId).catch(() => {})
364
+
365
+ // Dynamic import for optional services
366
+ import('./algolia.service').then(({ AlgoliaService }) =>
367
+ AlgoliaService.syncToIndex(id, data)
368
+ ).catch(() => {})
369
+ ```
370
+
371
+ ### Denormalization Patterns
372
+
373
+ #### Bidirectional Index
374
+
375
+ Write index entries for both users in a relationship:
376
+
377
+ ```typescript
378
+ const index1 = getUserRelationshipIndexCollection(userId1)
379
+ const index2 = getUserRelationshipIndexCollection(userId2)
380
+
381
+ await setDocument(index1, userId2, { related_user_id: userId2, relationship_id: id, flags })
382
+ await setDocument(index2, userId1, { related_user_id: userId1, relationship_id: id, flags })
383
+ ```
384
+
385
+ #### Bulk Delete with Pagination
386
+
387
+ ```typescript
388
+ let hasMore = true
389
+ while (hasMore) {
390
+ const docs = await queryDocuments(collectionPath, { limit: 500 })
391
+ for (const doc of docs) {
392
+ await deleteDocument(collectionPath, doc.id)
393
+ }
394
+ hasMore = docs.length === 500
395
+ }
396
+ ```
397
+
398
+ ### Complex Object Serialization
399
+
400
+ Firestore doesn't support deeply nested arbitrary objects well. Serialize complex inputs:
401
+
402
+ ```typescript
403
+ const toolCallData = {
404
+ tool_name: toolCall.tool_name,
405
+ timestamp: toolCall.timestamp.toISOString(),
406
+ // Serialize to prevent nested object issues
407
+ inputs: typeof toolCall.inputs === 'string'
408
+ ? toolCall.inputs
409
+ : JSON.stringify(toolCall.inputs),
410
+ output: typeof toolCall.output === 'string'
411
+ ? toolCall.output
412
+ : JSON.stringify(toolCall.output),
413
+ }
414
+ ```
415
+
416
+ ---
417
+
418
+ ## Anti-Patterns
419
+
420
+ ### Returning Unvalidated Data
421
+
422
+ ```typescript
423
+ // Bad: Returns raw Firestore data as any
424
+ const doc = await getDocument(collection, id)
425
+ return doc
426
+
427
+ // Good: Always validate with Zod
428
+ const result = EntitySchema.safeParse(doc)
429
+ if (!result.success) return null
430
+ return result.data
431
+ ```
432
+
433
+ ### Hardcoded Collection Paths
434
+
435
+ ```typescript
436
+ // Bad
437
+ const doc = await getDocument('agentbase.users/' + userId + '/profile', 'default')
438
+
439
+ // Good
440
+ const collection = getUserProfileCollection(userId)
441
+ const doc = await getDocument(collection, PROFILE_DOC_ID)
442
+ ```
443
+
444
+ ### Using `merge: true` with Nested Objects
445
+
446
+ ```typescript
447
+ // Bad: Can cause wildcard field issues with nested objects
448
+ await setDocument(coll, id, { nested: { a: 1 } }, { merge: true })
449
+
450
+ // Good: Use mergeFields for explicit control
451
+ await setDocument(coll, id, { nested: { a: 1 } }, { mergeFields: ['nested'] })
452
+ ```
453
+
454
+ ### Unbounded Queries
455
+
456
+ ```typescript
457
+ // Bad: Could return millions of documents
458
+ const all = await queryDocuments(collection, {})
459
+
460
+ // Good: Always limit
461
+ const docs = await queryDocuments(collection, { limit: 500 })
462
+ ```
463
+
464
+ ### Numeric Timestamps
465
+
466
+ ```typescript
467
+ // Bad
468
+ const now = Date.now()
469
+
470
+ // Good
471
+ const now = new Date().toISOString()
472
+ ```
473
+
474
+ ### `in` Queries Over 30 Items
475
+
476
+ ```typescript
477
+ // Bad: Firestore rejects in queries with >30 values
478
+ const docs = await queryDocuments(coll, {
479
+ where: [{ field: 'id', op: 'in', value: hundredIds }],
480
+ })
481
+
482
+ // Good: Chunk into batches of 30
483
+ const chunks = []
484
+ for (let i = 0; i < ids.length; i += 30) chunks.push(ids.slice(i, i + 30))
485
+ const results = await Promise.all(chunks.map(chunk =>
486
+ queryDocuments(coll, { where: [{ field: 'id', op: 'in', value: chunk }] })
487
+ ))
488
+ ```
489
+
490
+ ---
491
+
492
+ ## Key Design Decisions
493
+
494
+ ### Data Model
495
+
496
+ | Decision | Choice | Rationale |
497
+ |---|---|---|
498
+ | User data scoping | User-scoped subcollections | Natural security boundary; collection-group queries still possible |
499
+ | DM/group conversations | Shared collection (not user-scoped) | Multiple participants read/write same messages |
500
+ | Relationship data | Global + per-user index | Global doc is source of truth; index enables fast per-user queries |
501
+ | Document ID strategy | Auto-generated (default), fixed for singletons | Auto-gen prevents conflicts; fixed IDs simplify lookups |
502
+
503
+ ### Query Patterns
504
+
505
+ | Decision | Choice | Rationale |
506
+ |---|---|---|
507
+ | OR queries | Multiple queries + merge | Firestore has no OR operator |
508
+ | Large `in` queries | Chunk at 30, parallelize | Firestore limit; Promise.all for speed |
509
+ | Pagination | Cursor-based (startAfter) | More reliable than offset for real-time data |
510
+ | Sort direction | Specified in QueryOptions | Prevents relying on default (undefined) ordering |
511
+
512
+ ### Error Handling
513
+
514
+ | Decision | Choice | Rationale |
515
+ |---|---|---|
516
+ | Read errors | Return null / empty array | Callers handle gracefully; page still renders |
517
+ | Write errors | Throw | Callers need to know writes failed for user feedback |
518
+ | Validation errors | Log + return null | Invalid data in Firestore is non-fatal; logged for debugging |
519
+
520
+ ---
521
+
522
+ ## Checklist for Implementation
523
+
524
+ - [ ] `initFirebaseAdmin()` called before any Firestore operation
525
+ - [ ] Collection path uses helper from `@/constant/collections`
526
+ - [ ] All reads validated with Zod `safeParse()` — no raw `doc as Type`
527
+ - [ ] Reads return `null` on error; writes throw on error
528
+ - [ ] Timestamps use `new Date().toISOString()`
529
+ - [ ] Logging uses `[ClassName]` prefix
530
+ - [ ] Queries include `limit` to prevent unbounded reads
531
+ - [ ] `in` queries chunked at 30 items
532
+ - [ ] `mergeFields` used instead of `merge: true` for nested objects
533
+ - [ ] New collection paths added to `src/constant/collections.ts`
534
+
535
+ ---
536
+
537
+ ## Related Patterns
538
+
539
+ - **[Database Service Conventions](./database-service-conventions.md)**: Naming, structure, and testing conventions for DatabaseService classes
540
+ - **[Zod Schema Conventions](./zod-schema-conventions.md)**: Schema definitions consumed by DatabaseServices for `safeParse`
541
+ - **[Firebase Auth](./tanstack-cloudflare.firebase-auth.md)**: Auth-verified userId flows into all database calls
542
+ - **[SSR Preload](./ssr-preload.md)**: SSR `beforeLoad` calls DatabaseServices with `typeof window === 'undefined'` guard
543
+ - **[Entity Blueprint](./entity-blueprint.md)**: End-to-end recipe for creating a new entity with schema, service, API, and UI
544
+
545
+ ---
546
+
547
+ **Status**: Stable
548
+ **Recommendation**: Follow this pattern for all Firestore operations. Always use DatabaseService classes — never call SDK functions directly from routes or components.
549
+ **Last Updated**: 2026-03-14
550
+ **Contributors**: Community