@prmichaelsen/acp-visualizer 0.1.0 → 0.1.2

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 (159) hide show
  1. package/package.json +8 -10
  2. package/src/components/ExtraFieldsBadge.tsx +1 -1
  3. package/src/components/FilterBar.tsx +1 -1
  4. package/src/components/Header.tsx +1 -1
  5. package/src/components/MilestoneTable.tsx +1 -1
  6. package/src/components/MilestoneTree.tsx +2 -2
  7. package/src/components/StatusBadge.tsx +1 -1
  8. package/src/components/StatusDot.tsx +1 -1
  9. package/src/components/TaskList.tsx +1 -1
  10. package/src/routes/__root.tsx +5 -5
  11. package/src/routes/api/watch.ts +1 -1
  12. package/src/routes/index.tsx +2 -2
  13. package/src/routes/milestones.tsx +7 -7
  14. package/src/routes/search.tsx +4 -4
  15. package/src/routes/tasks.tsx +3 -3
  16. package/src/services/progress-database.service.ts +3 -3
  17. package/agent/commands/acp.clarification-address.md +0 -417
  18. package/agent/commands/acp.clarification-capture.md +0 -386
  19. package/agent/commands/acp.clarification-create.md +0 -437
  20. package/agent/commands/acp.clarifications-research.md +0 -326
  21. package/agent/commands/acp.command-create.md +0 -432
  22. package/agent/commands/acp.design-create.md +0 -286
  23. package/agent/commands/acp.design-reference.md +0 -355
  24. package/agent/commands/acp.handoff.md +0 -270
  25. package/agent/commands/acp.index.md +0 -423
  26. package/agent/commands/acp.init.md +0 -546
  27. package/agent/commands/acp.package-create.md +0 -895
  28. package/agent/commands/acp.package-info.md +0 -212
  29. package/agent/commands/acp.package-install.md +0 -539
  30. package/agent/commands/acp.package-list.md +0 -280
  31. package/agent/commands/acp.package-publish.md +0 -541
  32. package/agent/commands/acp.package-remove.md +0 -293
  33. package/agent/commands/acp.package-search.md +0 -307
  34. package/agent/commands/acp.package-update.md +0 -361
  35. package/agent/commands/acp.package-validate.md +0 -540
  36. package/agent/commands/acp.pattern-create.md +0 -386
  37. package/agent/commands/acp.plan.md +0 -587
  38. package/agent/commands/acp.proceed.md +0 -882
  39. package/agent/commands/acp.project-create.md +0 -675
  40. package/agent/commands/acp.project-info.md +0 -312
  41. package/agent/commands/acp.project-list.md +0 -226
  42. package/agent/commands/acp.project-remove.md +0 -379
  43. package/agent/commands/acp.project-set.md +0 -227
  44. package/agent/commands/acp.project-update.md +0 -307
  45. package/agent/commands/acp.projects-restore.md +0 -228
  46. package/agent/commands/acp.projects-sync.md +0 -347
  47. package/agent/commands/acp.report.md +0 -407
  48. package/agent/commands/acp.resume.md +0 -239
  49. package/agent/commands/acp.sessions.md +0 -301
  50. package/agent/commands/acp.status.md +0 -293
  51. package/agent/commands/acp.sync.md +0 -364
  52. package/agent/commands/acp.task-create.md +0 -500
  53. package/agent/commands/acp.update.md +0 -302
  54. package/agent/commands/acp.validate.md +0 -466
  55. package/agent/commands/acp.version-check-for-updates.md +0 -276
  56. package/agent/commands/acp.version-check.md +0 -191
  57. package/agent/commands/acp.version-update.md +0 -289
  58. package/agent/commands/command.template.md +0 -339
  59. package/agent/commands/git.commit.md +0 -526
  60. package/agent/commands/git.init.md +0 -514
  61. package/agent/commands/tanstack-cloudflare.deploy.md +0 -272
  62. package/agent/commands/tanstack-cloudflare.tail.md +0 -275
  63. package/agent/design/.gitkeep +0 -0
  64. package/agent/design/design.template.md +0 -154
  65. package/agent/design/local.dashboard-layout-routing.md +0 -288
  66. package/agent/design/local.data-model-yaml-parsing.md +0 -310
  67. package/agent/design/local.search-filtering.md +0 -331
  68. package/agent/design/local.server-api-auto-refresh.md +0 -235
  69. package/agent/design/local.table-tree-views.md +0 -299
  70. package/agent/design/local.visualizer-requirements.md +0 -349
  71. package/agent/design/requirements.template.md +0 -387
  72. package/agent/index/.gitkeep +0 -0
  73. package/agent/index/acp.core.yaml +0 -137
  74. package/agent/index/local.main.template.yaml +0 -37
  75. package/agent/manifest.template.yaml +0 -13
  76. package/agent/manifest.yaml +0 -302
  77. package/agent/milestones/.gitkeep +0 -0
  78. package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +0 -67
  79. package/agent/milestones/milestone-1-{title}.template.md +0 -206
  80. package/agent/milestones/milestone-2-dashboard-views-interaction.md +0 -79
  81. package/agent/package.template.yaml +0 -86
  82. package/agent/patterns/.gitkeep +0 -0
  83. package/agent/patterns/bootstrap.template.md +0 -1237
  84. package/agent/patterns/pattern.template.md +0 -382
  85. package/agent/patterns/tanstack-cloudflare.acl-permissions.md +0 -332
  86. package/agent/patterns/tanstack-cloudflare.action-bar-item.md +0 -416
  87. package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +0 -401
  88. package/agent/patterns/tanstack-cloudflare.auth-session-management.md +0 -387
  89. package/agent/patterns/tanstack-cloudflare.card-and-list.md +0 -271
  90. package/agent/patterns/tanstack-cloudflare.chat-engine.md +0 -353
  91. package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +0 -346
  92. package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +0 -516
  93. package/agent/patterns/tanstack-cloudflare.email-service.md +0 -431
  94. package/agent/patterns/tanstack-cloudflare.expander.md +0 -98
  95. package/agent/patterns/tanstack-cloudflare.fcm-push.md +0 -115
  96. package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +0 -441
  97. package/agent/patterns/tanstack-cloudflare.firebase-auth.md +0 -348
  98. package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +0 -550
  99. package/agent/patterns/tanstack-cloudflare.firebase-storage.md +0 -369
  100. package/agent/patterns/tanstack-cloudflare.form-controls.md +0 -145
  101. package/agent/patterns/tanstack-cloudflare.global-search-context.md +0 -93
  102. package/agent/patterns/tanstack-cloudflare.image-carousel.md +0 -126
  103. package/agent/patterns/tanstack-cloudflare.library-services.md +0 -553
  104. package/agent/patterns/tanstack-cloudflare.lightbox.md +0 -169
  105. package/agent/patterns/tanstack-cloudflare.markdown-content.md +0 -115
  106. package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +0 -98
  107. package/agent/patterns/tanstack-cloudflare.modal.md +0 -156
  108. package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +0 -461
  109. package/agent/patterns/tanstack-cloudflare.notifications-engine.md +0 -151
  110. package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +0 -90
  111. package/agent/patterns/tanstack-cloudflare.og-metadata.md +0 -296
  112. package/agent/patterns/tanstack-cloudflare.pagination.md +0 -442
  113. package/agent/patterns/tanstack-cloudflare.pill-input.md +0 -220
  114. package/agent/patterns/tanstack-cloudflare.provider-adapter.md +0 -401
  115. package/agent/patterns/tanstack-cloudflare.rate-limiting.md +0 -323
  116. package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +0 -338
  117. package/agent/patterns/tanstack-cloudflare.searchable-settings.md +0 -375
  118. package/agent/patterns/tanstack-cloudflare.slide-over.md +0 -129
  119. package/agent/patterns/tanstack-cloudflare.ssr-preload.md +0 -571
  120. package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +0 -508
  121. package/agent/patterns/tanstack-cloudflare.toast-system.md +0 -142
  122. package/agent/patterns/tanstack-cloudflare.unified-header.md +0 -280
  123. package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +0 -628
  124. package/agent/patterns/tanstack-cloudflare.websocket-manager.md +0 -237
  125. package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +0 -358
  126. package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +0 -336
  127. package/agent/progress.template.yaml +0 -161
  128. package/agent/progress.yaml +0 -145
  129. package/agent/schemas/package.schema.yaml +0 -276
  130. package/agent/scripts/acp.common.sh +0 -1781
  131. package/agent/scripts/acp.install.sh +0 -333
  132. package/agent/scripts/acp.package-create.sh +0 -924
  133. package/agent/scripts/acp.package-info.sh +0 -288
  134. package/agent/scripts/acp.package-install.sh +0 -893
  135. package/agent/scripts/acp.package-list.sh +0 -311
  136. package/agent/scripts/acp.package-publish.sh +0 -420
  137. package/agent/scripts/acp.package-remove.sh +0 -348
  138. package/agent/scripts/acp.package-search.sh +0 -156
  139. package/agent/scripts/acp.package-update.sh +0 -517
  140. package/agent/scripts/acp.package-validate.sh +0 -1018
  141. package/agent/scripts/acp.uninstall.sh +0 -85
  142. package/agent/scripts/acp.version-check-for-updates.sh +0 -98
  143. package/agent/scripts/acp.version-check.sh +0 -47
  144. package/agent/scripts/acp.version-update.sh +0 -176
  145. package/agent/scripts/acp.yaml-parser.sh +0 -985
  146. package/agent/scripts/acp.yaml-validate.sh +0 -205
  147. package/agent/tasks/.gitkeep +0 -0
  148. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +0 -210
  149. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +0 -294
  150. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +0 -193
  151. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +0 -262
  152. package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +0 -156
  153. package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +0 -178
  154. package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +0 -141
  155. package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +0 -153
  156. package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +0 -174
  157. package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +0 -233
  158. package/agent/tasks/task-1-{title}.template.md +0 -244
  159. package/vitest.config.ts +0 -27
@@ -1,441 +0,0 @@
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