@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,441 @@
1
+ # Firebase Anonymous Sessions & Account Upgrade
2
+
3
+ **Category**: Code
4
+ **Applicable To**: Anonymous user auto-creation, feature gating by auth tier, anonymous-to-real account upgrade, and data preservation across upgrade
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ This pattern covers the anonymous session lifecycle: auto-creation on first visit, feature gating based on anonymous vs real user, the upgrade flow that preserves the user's UID and all associated data, and the message-limit-to-signup funnel. Every visitor gets a Firebase UID immediately — anonymous users can chat in The Void with a message limit, and upgrading to a real account links credentials to the same UID so no data is lost.
12
+
13
+ ---
14
+
15
+ ## When to Use This Pattern
16
+
17
+ **Use this pattern when:**
18
+ - Adding a new feature that should be restricted to real (non-anonymous) users
19
+ - Building a new public-facing page that anonymous users can access
20
+ - Adding a new capability to The Void or other anonymous-accessible areas
21
+ - Implementing a new signup prompt or conversion funnel
22
+ - Checking whether a user should see premium/gated features
23
+
24
+ **Don't use this pattern when:**
25
+ - Working on server-only code that already receives a verified userId
26
+ - Building admin-only features (use `requireAdmin` guard instead)
27
+ - Implementing MCP server auth (separate JWT system)
28
+
29
+ ---
30
+
31
+ ## Core Principles
32
+
33
+ 1. **Every Visitor Gets a UID**: Anonymous sign-in happens automatically on first visit — no user action needed
34
+ 2. **Same UID After Upgrade**: `linkWithCredential()` preserves the anonymous UID, so all prior data (conversations, memories, relationships) stays intact
35
+ 3. **Gate Features, Not Access**: Anonymous users can browse and chat — restrict actions (publish, friend, rate) not pages
36
+ 4. **Null-Safe Helpers**: Use `isRealUser(user)` — handles null, undefined, and anonymous in one check
37
+ 5. **One Attempt Per Session**: Anonymous sign-in is guarded by a ref to prevent duplicate calls
38
+
39
+ ---
40
+
41
+ ## Implementation
42
+
43
+ ### Anonymous Session Lifecycle
44
+
45
+ ```
46
+ First Visit Chat in Void Sign Up
47
+ │ │ │
48
+ ├─ AuthProvider mounts │ │
49
+ ├─ onAuthChange(null) │ │
50
+ ├─ signInAnonymously() │ │
51
+ ├─ POST /api/auth/login ──► │ │
52
+ │ (creates session cookie) │ │
53
+ │ │ │
54
+ │ user.isAnonymous = true │ │
55
+ │ ├─ 10 message limit │
56
+ │ ├─ SignupCta shown │
57
+ │ │ │
58
+ │ │ ├─ upgradeAnonymousAccount()
59
+ │ │ ├─ linkWithCredential()
60
+ │ │ ├─ POST /api/auth/login
61
+ │ │ │ (new session cookie)
62
+ │ │ │
63
+ │ │ │ SAME UID ✓
64
+ │ │ │ All data preserved ✓
65
+ │ │ │ user.isAnonymous = false
66
+ ```
67
+
68
+ ### Auto-Creation in AuthProvider
69
+
70
+ **File**: `src/components/auth/AuthContext.tsx`
71
+
72
+ ```typescript
73
+ export function AuthProvider({ children, initialUser }: AuthProviderProps) {
74
+ const [user, setUser] = useState<User | null>(initialUser as User | null)
75
+ const anonSignInAttempted = useRef(false)
76
+
77
+ useEffect(() => {
78
+ const unsubscribe = onAuthChange((firebaseUser) => {
79
+ if (firebaseUser) {
80
+ setAnalyticsUserId(firebaseUser.uid)
81
+ }
82
+
83
+ // Auto sign-in anonymously if no user exists (once per session)
84
+ if (!firebaseUser && !anonSignInAttempted.current) {
85
+ anonSignInAttempted.current = true
86
+ signInAnonymously()
87
+ .then(async (cred) => {
88
+ const idToken = await cred.user.getIdToken()
89
+ await fetch('/api/auth/login', {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json' },
92
+ body: JSON.stringify({ idToken }),
93
+ })
94
+ })
95
+ .catch((err) => {
96
+ console.error('[AuthProvider] Anonymous sign-in failed:', err)
97
+ })
98
+ return // onAuthChange will fire again with the anonymous user
99
+ }
100
+
101
+ setUser(firebaseUser)
102
+ })
103
+
104
+ return unsubscribe
105
+ }, [])
106
+
107
+ return <AuthContext.Provider value={{ user, loading: false }}>{children}</AuthContext.Provider>
108
+ }
109
+ ```
110
+
111
+ Key details:
112
+ - `anonSignInAttempted` ref prevents duplicate sign-in attempts
113
+ - After `signInAnonymously()` succeeds, `onAuthChange` fires again with the new user
114
+ - Session cookie created immediately so server-side auth works
115
+
116
+ ### Firebase Client Functions
117
+
118
+ **File**: `src/lib/firebase-client.ts`
119
+
120
+ ```typescript
121
+ // Create anonymous account
122
+ export async function signInAnonymously(): Promise<UserCredential> {
123
+ const auth = getFirebaseAuth()
124
+ return firebaseSignInAnonymously(auth)
125
+ }
126
+
127
+ // Upgrade anonymous to email/password
128
+ export async function upgradeAnonymousAccount(
129
+ email: string,
130
+ password: string
131
+ ): Promise<UserCredential> {
132
+ const auth = getFirebaseAuth()
133
+ if (!auth.currentUser?.isAnonymous) {
134
+ throw new Error('Current user is not anonymous')
135
+ }
136
+ const credential = EmailAuthProvider.credential(email, password)
137
+ return linkWithCredential(auth.currentUser, credential)
138
+ }
139
+
140
+ // Upgrade anonymous via OAuth popup
141
+ export async function upgradeAnonymousWithPopup(
142
+ provider: AuthProvider
143
+ ): Promise<UserCredential> {
144
+ const auth = getFirebaseAuth()
145
+ if (!auth.currentUser?.isAnonymous) {
146
+ throw new Error('Current user is not anonymous')
147
+ }
148
+ return linkWithPopup(auth.currentUser, provider)
149
+ }
150
+ ```
151
+
152
+ ### Detection Helpers
153
+
154
+ **File**: `src/lib/auth/helpers.ts`
155
+
156
+ ```typescript
157
+ /** Client-side: true if authenticated with a real (non-anonymous) account */
158
+ export function isRealUser(user: User | null | undefined): boolean {
159
+ return !!user && !user.isAnonymous
160
+ }
161
+
162
+ /** Server-side: true if authenticated with a real (non-anonymous) account */
163
+ export function isRealUserServer(user: ServerUser | null | undefined): boolean {
164
+ return !!user && !user.isAnonymous
165
+ }
166
+ ```
167
+
168
+ **Server-side detection** (`src/lib/auth/session.ts`):
169
+
170
+ ```typescript
171
+ const isAnonymous =
172
+ decodedToken.firebase?.sign_in_provider === 'anonymous' || !decodedToken.email
173
+ ```
174
+
175
+ ### Signup Flow with Upgrade Detection
176
+
177
+ **File**: `src/components/auth/AuthForm.tsx`
178
+
179
+ ```typescript
180
+ // Signup handler
181
+ const auth = getFirebaseAuth()
182
+ let userCredential
183
+
184
+ if (auth.currentUser?.isAnonymous) {
185
+ // Upgrade: links email/password to existing anonymous UID
186
+ userCredential = await upgradeAnonymousAccount(email, password)
187
+ } else {
188
+ // Fresh signup: creates new account
189
+ userCredential = await signUp(email, password)
190
+ }
191
+
192
+ // Both paths: exchange ID token for session cookie
193
+ const idToken = await userCredential.user.getIdToken()
194
+ const response = await fetch('/api/auth/login', {
195
+ method: 'POST',
196
+ headers: { 'Content-Type': 'application/json' },
197
+ body: JSON.stringify({ idToken, turnstileToken }),
198
+ })
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Examples
204
+
205
+ ### Example 1: Feature Gating in a Component
206
+
207
+ ```typescript
208
+ // Hide friend actions for anonymous users
209
+ function ProfileActionBar({ profileUserId }: Props) {
210
+ const { user } = useAuth()
211
+
212
+ // Anonymous users and own profile — hide actions
213
+ if (!isRealUser(user) || user.uid === profileUserId) return null
214
+
215
+ return (
216
+ <div>
217
+ <AddFriendButton userId={profileUserId} />
218
+ <SendMessageButton userId={profileUserId} />
219
+ </div>
220
+ )
221
+ }
222
+ ```
223
+
224
+ ### Example 2: Feature Gating in Header
225
+
226
+ ```typescript
227
+ // Only show notification bell for real users
228
+ function UnifiedHeader() {
229
+ const { user } = useAuth()
230
+
231
+ const bell = isRealUser(user) ? <NotificationBell userId={user.uid} /> : null
232
+
233
+ return (
234
+ <header>
235
+ {bell}
236
+ {/* other header content */}
237
+ </header>
238
+ )
239
+ }
240
+ ```
241
+
242
+ ### Example 3: Skipping Server Operations for Anonymous Users
243
+
244
+ ```typescript
245
+ // Don't track preferences or consent for anonymous users
246
+ function UIPreferencesProvider({ children }: Props) {
247
+ const { user } = useAuth()
248
+
249
+ useEffect(() => {
250
+ if (!isRealUser(user)) return // Skip for anonymous
251
+ PreferencesService.loadPreferences(user.uid).then(setPrefs)
252
+ }, [user])
253
+
254
+ const updatePref = useCallback((key, value) => {
255
+ if (!isRealUser(user)) return // Skip for anonymous
256
+ PreferencesService.updatePreference(user.uid, key, value)
257
+ }, [user])
258
+
259
+ return <UIPreferencesContext.Provider value={{ prefs, updatePref }}>{children}</UIPreferencesContext.Provider>
260
+ }
261
+ ```
262
+
263
+ ### Example 4: Anonymous Chat with Message Limit
264
+
265
+ ```typescript
266
+ // Chat route — anonymous users access main conversation only
267
+ if (conversationId === 'main' && (!user || user.isAnonymous)) {
268
+ return new Response(JSON.stringify({
269
+ conversation: { id: 'main', title: 'Void Ghost' }
270
+ }), { status: 200 })
271
+ }
272
+
273
+ // ChatInterface — enforce message limit for anonymous
274
+ <ChatInterface
275
+ conversationId={conversationId}
276
+ anonMessageLimit={10}
277
+ disabledMessage={
278
+ <SignupCta message="You've hit your limit. Sign up to keep chatting!" />
279
+ }
280
+ />
281
+ ```
282
+
283
+ ### Example 5: API Route Rejecting Anonymous Users
284
+
285
+ ```typescript
286
+ GET: async () => {
287
+ initFirebaseAdmin()
288
+
289
+ const user = await getAuthSession()
290
+ if (!user || user.isAnonymous) {
291
+ return new Response(JSON.stringify({ error: 'Unauthorized' }), {
292
+ status: 401,
293
+ headers: { 'Content-Type': 'application/json' },
294
+ })
295
+ }
296
+
297
+ // Only real users reach here
298
+ const data = await SomeDatabaseService.getData(user.uid)
299
+ return new Response(JSON.stringify({ data }), { status: 200 })
300
+ }
301
+ ```
302
+
303
+ ### Example 6: SSR beforeLoad Auth Redirect
304
+
305
+ ```typescript
306
+ // Settings page — redirect anonymous users to auth
307
+ beforeLoad: (async ({ context }: any) => {
308
+ const user = context.initialUser
309
+ if (!user || user.isAnonymous) {
310
+ throw redirect({
311
+ to: '/auth',
312
+ search: { redirect_url: '/settings' },
313
+ })
314
+ }
315
+ return { initialUser: user }
316
+ }) as any
317
+ ```
318
+
319
+ ---
320
+
321
+ ## Anti-Patterns
322
+
323
+ ### Checking `user === null` Instead of `isRealUser()`
324
+
325
+ ```typescript
326
+ // Bad: Misses anonymous users — they have a user object but shouldn't access features
327
+ if (!user) return <LoginPrompt />
328
+ return <ProtectedFeature /> // Anonymous users get through!
329
+
330
+ // Good: Handles null, undefined, and anonymous in one check
331
+ if (!isRealUser(user)) return <LoginPrompt />
332
+ return <ProtectedFeature />
333
+ ```
334
+
335
+ ### Creating a New Account Instead of Upgrading
336
+
337
+ ```typescript
338
+ // Bad: Creates a new UID — all anonymous data is orphaned
339
+ const userCredential = await signUp(email, password) // New UID!
340
+
341
+ // Good: Check if anonymous first, upgrade to preserve UID
342
+ const auth = getFirebaseAuth()
343
+ if (auth.currentUser?.isAnonymous) {
344
+ userCredential = await upgradeAnonymousAccount(email, password) // Same UID
345
+ } else {
346
+ userCredential = await signUp(email, password)
347
+ }
348
+ ```
349
+
350
+ ### Multiple Anonymous Sign-In Attempts
351
+
352
+ ```typescript
353
+ // Bad: No guard — creates multiple anonymous accounts
354
+ useEffect(() => {
355
+ if (!user) signInAnonymously() // Fires on every render!
356
+ }, [user])
357
+
358
+ // Good: Ref guard ensures single attempt
359
+ const attempted = useRef(false)
360
+ useEffect(() => {
361
+ if (!user && !attempted.current) {
362
+ attempted.current = true
363
+ signInAnonymously()
364
+ }
365
+ }, [user])
366
+ ```
367
+
368
+ ### Gating Pages Instead of Actions
369
+
370
+ ```typescript
371
+ // Bad: Blocks anonymous users from browsing public content
372
+ if (!isRealUser(user)) throw redirect({ to: '/auth' })
373
+ return <PublicProfilePage /> // Anonymous should be able to browse!
374
+
375
+ // Good: Let anonymous browse, gate specific actions
376
+ return (
377
+ <PublicProfilePage>
378
+ {isRealUser(user) && <AddFriendButton />}
379
+ {isRealUser(user) && <SendMessageButton />}
380
+ </PublicProfilePage>
381
+ )
382
+ ```
383
+
384
+ ---
385
+
386
+ ## Key Design Decisions
387
+
388
+ ### Anonymous Sessions
389
+
390
+ | Decision | Choice | Rationale |
391
+ |---|---|---|
392
+ | Auto-creation | On first visit, no user action | Every visitor gets a UID for chat and tracking |
393
+ | Session cookie | Same 14-day cookie as real users | Uniform server-side auth — no special anonymous path |
394
+ | Message limit | 10 messages in The Void | Conversion funnel — enough to experience chat, then prompt signup |
395
+ | Sign-in guard | `useRef` one-attempt flag | Prevents duplicate anonymous accounts from React re-renders |
396
+
397
+ ### Account Upgrade
398
+
399
+ | Decision | Choice | Rationale |
400
+ |---|---|---|
401
+ | Upgrade method | `linkWithCredential()` | Firebase preserves UID — all data stays under same account |
402
+ | Detection | `auth.currentUser?.isAnonymous` | Simple check before signup; transparent to user |
403
+ | Data migration | None needed | Same UID means same Firestore paths — zero data migration |
404
+ | OAuth upgrade | `linkWithPopup()` available | Supports Google/GitHub upgrade alongside email/password |
405
+
406
+ ### Feature Gating
407
+
408
+ | Decision | Choice | Rationale |
409
+ |---|---|---|
410
+ | Gating strategy | Gate actions, not pages | Anonymous users can browse profiles, feeds, spaces — just can't interact |
411
+ | Helper function | `isRealUser()` | Single null-safe check; avoids repeated `!user \|\| user.isAnonymous` |
412
+ | Server gating | Check `user.isAnonymous` in API routes | Return 401 for features requiring real account |
413
+ | Consent/preferences | Skip for anonymous | No point collecting consent or persisting preferences for transient users |
414
+
415
+ ---
416
+
417
+ ## Checklist for Implementation
418
+
419
+ - [ ] Use `isRealUser(user)` for feature gating — never check `user === null` alone
420
+ - [ ] Gate actions (publish, friend, rate) not pages (browse, search, view)
421
+ - [ ] Check `auth.currentUser?.isAnonymous` before signup to trigger upgrade path
422
+ - [ ] After upgrade, create new session cookie via `/api/auth/login`
423
+ - [ ] API routes checking anonymous: `if (!user || user.isAnonymous)` → 401
424
+ - [ ] SSR routes for protected pages: redirect to `/auth?redirect_url=...`
425
+ - [ ] Public pages: allow anonymous access, hide action buttons with `isRealUser()`
426
+ - [ ] Never call `signInAnonymously()` without a one-attempt ref guard
427
+
428
+ ---
429
+
430
+ ## Related Patterns
431
+
432
+ - **[Firebase Auth](./tanstack-cloudflare.firebase-auth.md)**: Session cookie management, `getAuthSession()`, route guards
433
+ - **[Firebase Firestore](./tanstack-cloudflare.firebase-firestore.md)**: All user data keyed by UID — preserved across upgrade
434
+ - **[SSR Preload](./ssr-preload.md)**: `beforeLoad` uses `context.initialUser` which may be anonymous
435
+
436
+ ---
437
+
438
+ **Status**: Stable
439
+ **Recommendation**: Always use `isRealUser()` for feature gating. Always check `isAnonymous` before signup to trigger the upgrade path and preserve user data.
440
+ **Last Updated**: 2026-03-14
441
+ **Contributors**: Community