@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,628 @@
1
+ # User-Scoped Collections Pattern
2
+
3
+ **Category**: Architecture
4
+ **Applicable To**: Firestore database design in TanStack Start + Cloudflare Workers applications
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ User-scoped collections store data as subcollections under individual user documents. This pattern provides natural data isolation, simplifies security rules, and makes per-user queries efficient. By embedding the user ID in the document path rather than as a field, you eliminate the need for filtering queries and make security rules trivial to implement.
12
+
13
+ This pattern is essential for multi-tenant applications where each user's data must be completely isolated from other users' data, with the isolation enforced at the database structure level rather than through application logic.
14
+
15
+ ---
16
+
17
+ ## When to Use This Pattern
18
+
19
+ ✅ **Use this pattern when:**
20
+ - Building multi-tenant applications where users have isolated data
21
+ - Working with Firestore in TanStack Start + Cloudflare Workers
22
+ - Need to enforce data isolation at the database level
23
+ - Want simplified security rules (path-based access control)
24
+ - Per-user queries are the primary access pattern
25
+ - Data naturally belongs to a specific user (conversations, credentials, preferences)
26
+
27
+ ❌ **Don't use this pattern when:**
28
+ - Data is shared across multiple users (public content, shared documents)
29
+ - Need to query across all users frequently (analytics, admin dashboards)
30
+ - Working with relational databases (use user_id foreign keys instead)
31
+ - Data doesn't have a clear user ownership
32
+
33
+ ---
34
+
35
+ ## Core Principles
36
+
37
+ 1. **Path-Based User Scoping**: User ID is embedded in the document path, not stored as a field in the document
38
+ 2. **No user_id Field**: Documents don't need a `user_id` field since the path provides the scope
39
+ 3. **Provider-Based Organization**: OAuth credentials and integrations organized by provider (instagram, eventbrite, etc.)
40
+ 4. **Nested Subcollections**: Related data (like messages in conversations) stored as subcollections
41
+ 5. **Simplified Security Rules**: Firestore security rules can easily enforce user isolation using path variables
42
+ 6. **Efficient Queries**: Queries are naturally scoped to a user without requiring filters
43
+
44
+ ---
45
+
46
+ ## Implementation
47
+
48
+ ### Structure
49
+
50
+ ```
51
+ {BASE}.users/{userId}/
52
+ ├── conversations/{conversationId}
53
+ │ └── messages/{messageId}
54
+ ├── credentials/{provider}
55
+ │ └── current
56
+ ├── oauth-integrations/{provider}
57
+ │ └── current
58
+ ├── preferences/
59
+ │ └── settings
60
+ └── activity/{activityId}
61
+ ```
62
+
63
+ ### Code Example
64
+
65
+ #### Step 1: Define Collection Helpers
66
+
67
+ ```typescript
68
+ // src/constant/collections.ts
69
+ export const BASE = getBasePrefix(); // e.g., 'e0.agentbase' or 'agentbase'
70
+
71
+ /**
72
+ * Get the conversations collection path for a specific user
73
+ */
74
+ export function getUserConversations(userId: string): string {
75
+ return `${BASE}.users/${userId}/conversations`;
76
+ }
77
+
78
+ /**
79
+ * Get the messages collection path for a specific conversation
80
+ */
81
+ export function getUserConversationMessages(userId: string, conversationId: string): string {
82
+ return `${BASE}.users/${userId}/conversations/${conversationId}/messages`;
83
+ }
84
+
85
+ /**
86
+ * Get the credentials path for a specific user and provider
87
+ * Pattern: users/{userId}/credentials/{provider}
88
+ */
89
+ export function getUserCredentials(userId: string, provider: string): string {
90
+ return `${BASE}.users/${userId}/credentials/${provider}`;
91
+ }
92
+
93
+ /**
94
+ * Get OAuth integration path for a user and provider
95
+ * Pattern: users/{userId}/oauth-integrations/{provider}
96
+ */
97
+ export function getUserOAuthIntegration(userId: string, provider: string): string {
98
+ return `${BASE}.users/${userId}/oauth-integrations/${provider}`;
99
+ }
100
+ ```
101
+
102
+ #### Step 2: Create Zod Schemas (No user_id field)
103
+
104
+ ```typescript
105
+ // src/schemas/credentials.ts
106
+ import { z } from 'zod';
107
+
108
+ /**
109
+ * User-Scoped Credentials Schema
110
+ *
111
+ * Stored at: users/{userId}/credentials/{provider}/current
112
+ * The userId and provider are implicit in the path.
113
+ */
114
+ export const InstagramCredentialsSchema = z.object({
115
+ access_token: z.string(),
116
+ instagram_user_id: z.string(),
117
+ instagram_username: z.string().optional(),
118
+ expires_at: z.string().datetime(),
119
+ created_at: z.string().datetime(),
120
+ updated_at: z.string().datetime(),
121
+ // Note: No user_id field - it's in the path!
122
+ });
123
+
124
+ export type InstagramCredentials = z.infer<typeof InstagramCredentialsSchema>;
125
+ ```
126
+
127
+ ```typescript
128
+ // src/schemas/oauth-integration.ts
129
+ import { z } from 'zod';
130
+
131
+ /**
132
+ * OAuth Integration Schema (User-Scoped)
133
+ *
134
+ * Stored at: users/{userId}/oauth-integrations/{provider}/current
135
+ * Tracks OAuth connections that provide access tokens.
136
+ */
137
+ export const OAuthIntegrationSchema = z.object({
138
+ connected: z.boolean(),
139
+ connected_at: z.string().datetime(),
140
+ disconnected_at: z.string().datetime().optional(),
141
+
142
+ // OAuth-specific
143
+ requires_refresh: z.boolean(),
144
+ refresh_interval_days: z.number().optional(),
145
+ last_refreshed_at: z.string().datetime().optional(),
146
+ next_refresh_at: z.string().datetime().optional(),
147
+
148
+ // Metadata
149
+ provider_user_id: z.string().optional(),
150
+ provider_username: z.string().optional(),
151
+ scopes: z.array(z.string()).optional(),
152
+
153
+ created_at: z.string().datetime(),
154
+ updated_at: z.string().datetime(),
155
+ // Note: No user_id field - it's in the path!
156
+ });
157
+
158
+ export type OAuthIntegration = z.infer<typeof OAuthIntegrationSchema>;
159
+ ```
160
+
161
+ #### Step 3: Create Service Layer
162
+
163
+ ```typescript
164
+ // src/services/conversation-database.service.ts
165
+ import { getDocument, setDocument, queryDocuments } from '@prmichaelsen/firebase-admin-sdk-v8'
166
+ import { getUserConversations, getUserConversationMessages } from '@/constant/collections'
167
+ import { ConversationSchema, MessageSchema } from '@/schemas/chat'
168
+
169
+ export class ConversationDatabaseService {
170
+ /**
171
+ * Get messages for a specific conversation
172
+ */
173
+ static async getMessages(
174
+ userId: string,
175
+ conversationId: string,
176
+ limit = 50,
177
+ startAfter?: string
178
+ ): Promise<Message[]> {
179
+ const collectionPath = getUserConversationMessages(userId, conversationId)
180
+
181
+ const results = await queryDocuments(collectionPath, {
182
+ orderBy: [{ field: 'created_at', direction: 'DESCENDING' }],
183
+ limit,
184
+ startAfter: startAfter ? [{ field: 'created_at', value: startAfter }] : undefined,
185
+ })
186
+
187
+ return results.map(doc => MessageSchema.parse(doc.data))
188
+ }
189
+
190
+ /**
191
+ * Add a message to a conversation
192
+ */
193
+ static async addMessage(
194
+ userId: string,
195
+ conversationId: string,
196
+ messageData: Omit<Message, 'id' | 'created_at' | 'updated_at'>
197
+ ): Promise<Message> {
198
+ const collectionPath = getUserConversationMessages(userId, conversationId)
199
+ const messageId = crypto.randomUUID()
200
+
201
+ const message: Message = {
202
+ ...messageData,
203
+ id: messageId,
204
+ created_at: new Date().toISOString(),
205
+ updated_at: new Date().toISOString(),
206
+ // Note: No user_id field needed!
207
+ }
208
+
209
+ await setDocument(collectionPath, messageId, message)
210
+ return message
211
+ }
212
+ }
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Examples
218
+
219
+ ### Example 1: OAuth Callback Saving Credentials
220
+
221
+ ```typescript
222
+ // src/routes/api/auth/instagram/callback.ts
223
+ import { getServerSession } from '@/lib/auth/session'
224
+ import { setDocument } from '@prmichaelsen/firebase-admin-sdk-v8'
225
+ import { getUserCredentials, getUserOAuthIntegration } from '@/constant/collections'
226
+
227
+ export const APIRoute = createAPIFileRoute('/api/auth/instagram/callback')({
228
+ GET: async ({ request }) => {
229
+ const session = await getServerSession(request)
230
+ if (!session?.user) {
231
+ return Response.json({ error: 'Unauthorized' }, { status: 401 })
232
+ }
233
+
234
+ // Exchange code for token...
235
+ const { accessToken, expiresIn, instagramUserId } = await exchangeCodeForToken(code)
236
+
237
+ // Save credentials (user-scoped)
238
+ const credentialsPath = getUserCredentials(session.user.uid, 'instagram')
239
+ await setDocument(credentialsPath, 'current', {
240
+ access_token: accessToken,
241
+ instagram_user_id: instagramUserId.toString(),
242
+ expires_at: new Date(Date.now() + expiresIn * 1000).toISOString(),
243
+ created_at: new Date().toISOString(),
244
+ updated_at: new Date().toISOString(),
245
+ // No user_id field!
246
+ })
247
+
248
+ // Save OAuth integration status (user-scoped)
249
+ const integrationPath = getUserOAuthIntegration(session.user.uid, 'instagram')
250
+ await setDocument(integrationPath, 'current', {
251
+ connected: true,
252
+ connected_at: new Date().toISOString(),
253
+ requires_refresh: true,
254
+ refresh_interval_days: 50,
255
+ provider_user_id: instagramUserId.toString(),
256
+ created_at: new Date().toISOString(),
257
+ updated_at: new Date().toISOString(),
258
+ // No user_id field!
259
+ })
260
+
261
+ return Response.redirect('/integrations')
262
+ }
263
+ })
264
+ ```
265
+
266
+ ### Example 2: Querying User's Conversations
267
+
268
+ ```typescript
269
+ // src/services/conversation-database.service.ts
270
+ export class ConversationDatabaseService {
271
+ static async getUserConversations(userId: string): Promise<Conversation[]> {
272
+ // Query is naturally scoped to user - no filtering needed!
273
+ const collectionPath = getUserConversations(userId)
274
+
275
+ const results = await queryDocuments(collectionPath, {
276
+ orderBy: [{ field: 'updated_at', direction: 'DESCENDING' }],
277
+ limit: 50,
278
+ })
279
+
280
+ return results.map(doc => ConversationSchema.parse(doc.data))
281
+ }
282
+ }
283
+ ```
284
+
285
+ ### Example 3: Firestore Security Rules
286
+
287
+ ```javascript
288
+ // firestore.rules
289
+ rules_version = '2';
290
+ service cloud.firestore {
291
+ match /databases/{database}/documents {
292
+ // User-scoped conversations
293
+ match /{environment}.users/{userId}/conversations/{conversationId} {
294
+ allow read, write: if request.auth.uid == userId;
295
+ }
296
+
297
+ // User-scoped messages
298
+ match /{environment}.users/{userId}/conversations/{conversationId}/messages/{messageId} {
299
+ allow read, write: if request.auth.uid == userId;
300
+ }
301
+
302
+ // User-scoped credentials
303
+ match /{environment}.users/{userId}/credentials/{provider}/{document=**} {
304
+ allow read, write: if request.auth.uid == userId;
305
+ }
306
+
307
+ // User-scoped OAuth integrations
308
+ match /{environment}.users/{userId}/oauth-integrations/{provider}/{document=**} {
309
+ allow read, write: if request.auth.uid == userId;
310
+ }
311
+ }
312
+ }
313
+ ```
314
+
315
+ ---
316
+
317
+ ## Benefits
318
+
319
+ ### 1. Natural Data Isolation
320
+
321
+ All data is naturally scoped to a user with no risk of cross-user data leakage:
322
+
323
+ ```typescript
324
+ // All data naturally scoped to user
325
+ const messages = await ConversationDatabaseService.getMessages(userId, conversationId)
326
+ // Impossible to accidentally access another user's data
327
+ ```
328
+
329
+ ### 2. Simplified Security Rules
330
+
331
+ Security rules are trivial - just check if `request.auth.uid == userId`:
332
+
333
+ ```javascript
334
+ // Simple, clear security rule
335
+ match /{environment}.users/{userId}/conversations/{conversationId} {
336
+ allow read, write: if request.auth.uid == userId;
337
+ }
338
+ ```
339
+
340
+ ### 3. Efficient Queries
341
+
342
+ Queries are naturally scoped to a user without requiring filters:
343
+
344
+ ```typescript
345
+ // No filtering needed - path provides scope
346
+ const conversations = await queryDocuments(
347
+ getUserConversations(userId),
348
+ { orderBy: [{ field: 'updated_at', direction: 'DESCENDING' }] }
349
+ )
350
+ ```
351
+
352
+ ### 4. Clean Data Model
353
+
354
+ Documents don't need redundant `user_id` fields:
355
+
356
+ ```typescript
357
+ // Clean schema - no user_id field
358
+ interface Message {
359
+ id: string
360
+ content: string
361
+ role: 'user' | 'assistant'
362
+ created_at: string
363
+ // No user_id field!
364
+ }
365
+ ```
366
+
367
+ ### 5. Scalable Architecture
368
+
369
+ Firestore can efficiently index and query within user subcollections, making this pattern scale well.
370
+
371
+ ---
372
+
373
+ ## Trade-offs
374
+
375
+ ### 1. Cross-User Queries Are Difficult
376
+
377
+ **Downside**: Querying across all users (e.g., for analytics or admin dashboards) requires collection group queries or denormalization.
378
+
379
+ **Mitigation**:
380
+ - Use collection group queries for cross-user analytics
381
+ - Denormalize data into global collections for admin views
382
+ - Use separate analytics database for cross-user reporting
383
+
384
+ ### 2. Data Migration Complexity
385
+
386
+ **Downside**: Migrating from global collections to user-scoped requires rewriting paths for all documents.
387
+
388
+ **Mitigation**:
389
+ - Plan data structure carefully upfront
390
+ - Use migration scripts to automate path changes
391
+ - Consider dual-write during migration period
392
+
393
+ ---
394
+
395
+ ## Anti-Patterns
396
+
397
+ ### ❌ Anti-Pattern 1: Storing user_id in User-Scoped Documents
398
+
399
+ **Description**: Adding a redundant `user_id` field to documents that are already user-scoped by path.
400
+
401
+ **Why it's bad**: Redundant data that can become inconsistent, wastes storage, violates DRY principle.
402
+
403
+ **Instead, do this**: Omit the `user_id` field - the path provides the scope.
404
+
405
+ ```typescript
406
+ // ❌ Bad - redundant user_id field
407
+ await setDocument(getUserCredentials(userId, 'instagram'), 'current', {
408
+ user_id: userId, // Redundant! Already in path
409
+ access_token: token,
410
+ })
411
+
412
+ // ✅ Good - no user_id field
413
+ await setDocument(getUserCredentials(userId, 'instagram'), 'current', {
414
+ access_token: token,
415
+ // No user_id field needed
416
+ })
417
+ ```
418
+
419
+ ### ❌ Anti-Pattern 2: Using Global Collections for User Data
420
+
421
+ **Description**: Storing user-specific data in global collections with `user_id` filters.
422
+
423
+ **Why it's bad**: Requires filtering every query, complex security rules, risk of data leakage.
424
+
425
+ **Instead, do this**: Use user-scoped subcollections.
426
+
427
+ ```typescript
428
+ // ❌ Bad - global collection with filtering
429
+ const credentials = await queryDocuments('credentials', {
430
+ where: [
431
+ { field: 'user_id', op: 'EQUAL', value: userId },
432
+ { field: 'provider', op: 'EQUAL', value: 'instagram' }
433
+ ]
434
+ })
435
+
436
+ // ✅ Good - user-scoped collection
437
+ const credentialsPath = getUserCredentials(userId, 'instagram')
438
+ const credentials = await getDocument(credentialsPath, 'current')
439
+ ```
440
+
441
+ ### ❌ Anti-Pattern 3: Mixing Scoping Patterns
442
+
443
+ **Description**: Using user-scoped collections for some data and global collections for other user data.
444
+
445
+ **Why it's bad**: Inconsistent patterns make codebase harder to understand and maintain.
446
+
447
+ **Instead, do this**: Be consistent - use user-scoped collections for all user data.
448
+
449
+ ```typescript
450
+ // ❌ Bad - inconsistent patterns
451
+ const conversations = getUserConversations(userId) // User-scoped ✓
452
+ const credentials = 'credentials' // Global ✗
453
+
454
+ // ✅ Good - consistent patterns
455
+ const conversations = getUserConversations(userId) // User-scoped ✓
456
+ const credentials = getUserCredentials(userId, provider) // User-scoped ✓
457
+ ```
458
+
459
+ ---
460
+
461
+ ## Testing Strategy
462
+
463
+ ### Unit Tests
464
+
465
+ ```typescript
466
+ describe('User-Scoped Collections', () => {
467
+ it('should isolate data by user', async () => {
468
+ const user1Messages = await ConversationDatabaseService.getMessages('user1', 'conv1')
469
+ const user2Messages = await ConversationDatabaseService.getMessages('user2', 'conv1')
470
+
471
+ // Same conversation ID, different users = different data
472
+ expect(user1Messages).not.toEqual(user2Messages)
473
+ })
474
+
475
+ it('should not require user_id in document', async () => {
476
+ const message = await ConversationDatabaseService.addMessage('user1', 'conv1', {
477
+ content: 'Hello',
478
+ role: 'user',
479
+ })
480
+
481
+ expect(message).not.toHaveProperty('user_id')
482
+ })
483
+ })
484
+ ```
485
+
486
+ ### Integration Tests
487
+
488
+ ```typescript
489
+ describe('Firestore Security Rules', () => {
490
+ it('should prevent cross-user access', async () => {
491
+ // User1 tries to access User2's data
492
+ await expect(
493
+ getDocument(getUserConversations('user2'), 'conv1', { auth: user1Auth })
494
+ ).rejects.toThrow('Permission denied')
495
+ })
496
+
497
+ it('should allow user to access own data', async () => {
498
+ const doc = await getDocument(
499
+ getUserConversations('user1'),
500
+ 'conv1',
501
+ { auth: user1Auth }
502
+ )
503
+
504
+ expect(doc).toBeDefined()
505
+ })
506
+ })
507
+ ```
508
+
509
+ ---
510
+
511
+ ## Related Patterns
512
+
513
+ - **[Library Services Pattern](./tanstack-cloudflare.library-services.md)**: Database services use user-scoped collection paths
514
+ - **[SSR Preload Pattern](./tanstack-cloudflare.ssr-preload.md)**: Server-side data fetching with user-scoped collections
515
+
516
+ ---
517
+
518
+ ## Migration Guide
519
+
520
+ ### Step 1: Identify Global Collections
521
+
522
+ Find collections that store user-specific data with `user_id` fields:
523
+
524
+ ```typescript
525
+ // Old pattern - global collection
526
+ interface Conversation {
527
+ id: string
528
+ user_id: string // Field that indicates user ownership
529
+ title: string
530
+ created_at: string
531
+ }
532
+ ```
533
+
534
+ ### Step 2: Create Collection Helper Functions
535
+
536
+ ```typescript
537
+ // src/constant/collections.ts
538
+ export function getUserConversations(userId: string): string {
539
+ return `${BASE}.users/${userId}/conversations`;
540
+ }
541
+ ```
542
+
543
+ ### Step 3: Update Schemas (Remove user_id)
544
+
545
+ ```typescript
546
+ // New pattern - user-scoped
547
+ interface Conversation {
548
+ id: string
549
+ // No user_id field!
550
+ title: string
551
+ created_at: string
552
+ }
553
+ ```
554
+
555
+ ### Step 4: Migrate Data
556
+
557
+ ```typescript
558
+ // Migration script
559
+ async function migrateConversations() {
560
+ const oldConversations = await queryDocuments('conversations', {})
561
+
562
+ for (const doc of oldConversations) {
563
+ const { id, user_id, ...data } = doc.data
564
+ const newPath = getUserConversations(user_id)
565
+ await setDocument(newPath, id, data)
566
+ }
567
+
568
+ console.log(`Migrated ${oldConversations.length} conversations`)
569
+ }
570
+ ```
571
+
572
+ ### Step 5: Update Service Layer
573
+
574
+ ```typescript
575
+ // Update services to use new paths
576
+ export class ConversationDatabaseService {
577
+ static async getUserConversations(userId: string): Promise<Conversation[]> {
578
+ // Old: queryDocuments('conversations', { where: [{ field: 'user_id', ... }] })
579
+ // New: queryDocuments(getUserConversations(userId), {})
580
+ const collectionPath = getUserConversations(userId)
581
+ return await queryDocuments(collectionPath, {})
582
+ }
583
+ }
584
+ ```
585
+
586
+ ### Step 6: Update Security Rules
587
+
588
+ ```javascript
589
+ // Old rules - complex filtering
590
+ match /conversations/{conversationId} {
591
+ allow read, write: if resource.data.user_id == request.auth.uid;
592
+ }
593
+
594
+ // New rules - simple path-based
595
+ match /{environment}.users/{userId}/conversations/{conversationId} {
596
+ allow read, write: if request.auth.uid == userId;
597
+ }
598
+ ```
599
+
600
+ ---
601
+
602
+ ## References
603
+
604
+ - [Firestore Data Model Best Practices](https://firebase.google.com/docs/firestore/data-model)
605
+ - [Firestore Security Rules](https://firebase.google.com/docs/firestore/security/get-started)
606
+ - [firebase-admin-sdk-v8 Documentation](https://github.com/prmichaelsen/firebase-admin-sdk-v8)
607
+ - [Hierarchical Data in Firestore](https://firebase.google.com/docs/firestore/data-model#hierarchical-data)
608
+
609
+ ---
610
+
611
+ ## Checklist for Implementation
612
+
613
+ - [ ] Collection helper functions created for all user-scoped collections
614
+ - [ ] Zod schemas don't include `user_id` field
615
+ - [ ] Service layer uses collection helpers
616
+ - [ ] Firestore security rules use path-based access control
617
+ - [ ] No direct path strings in service methods
618
+ - [ ] All user data uses user-scoped collections
619
+ - [ ] Migration script created (if migrating from global collections)
620
+ - [ ] Tests verify data isolation between users
621
+ - [ ] Tests verify security rules work correctly
622
+
623
+ ---
624
+
625
+ **Status**: Stable - Proven pattern for Firestore in TanStack Start applications
626
+ **Recommendation**: Use for all user-specific data in Firestore
627
+ **Last Updated**: 2026-02-21
628
+ **Contributors**: Patrick Michaelsen