@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.
- package/README.md +68 -0
- package/agent/commands/acp.clarification-address.md +417 -0
- package/agent/commands/acp.clarification-capture.md +386 -0
- package/agent/commands/acp.clarification-create.md +437 -0
- package/agent/commands/acp.clarifications-research.md +326 -0
- package/agent/commands/acp.command-create.md +432 -0
- package/agent/commands/acp.design-create.md +286 -0
- package/agent/commands/acp.design-reference.md +355 -0
- package/agent/commands/acp.handoff.md +270 -0
- package/agent/commands/acp.index.md +423 -0
- package/agent/commands/acp.init.md +546 -0
- package/agent/commands/acp.package-create.md +895 -0
- package/agent/commands/acp.package-info.md +212 -0
- package/agent/commands/acp.package-install.md +539 -0
- package/agent/commands/acp.package-list.md +280 -0
- package/agent/commands/acp.package-publish.md +541 -0
- package/agent/commands/acp.package-remove.md +293 -0
- package/agent/commands/acp.package-search.md +307 -0
- package/agent/commands/acp.package-update.md +361 -0
- package/agent/commands/acp.package-validate.md +540 -0
- package/agent/commands/acp.pattern-create.md +386 -0
- package/agent/commands/acp.plan.md +587 -0
- package/agent/commands/acp.proceed.md +882 -0
- package/agent/commands/acp.project-create.md +675 -0
- package/agent/commands/acp.project-info.md +312 -0
- package/agent/commands/acp.project-list.md +226 -0
- package/agent/commands/acp.project-remove.md +379 -0
- package/agent/commands/acp.project-set.md +227 -0
- package/agent/commands/acp.project-update.md +307 -0
- package/agent/commands/acp.projects-restore.md +228 -0
- package/agent/commands/acp.projects-sync.md +347 -0
- package/agent/commands/acp.report.md +407 -0
- package/agent/commands/acp.resume.md +239 -0
- package/agent/commands/acp.sessions.md +301 -0
- package/agent/commands/acp.status.md +293 -0
- package/agent/commands/acp.sync.md +364 -0
- package/agent/commands/acp.task-create.md +500 -0
- package/agent/commands/acp.update.md +302 -0
- package/agent/commands/acp.validate.md +466 -0
- package/agent/commands/acp.version-check-for-updates.md +276 -0
- package/agent/commands/acp.version-check.md +191 -0
- package/agent/commands/acp.version-update.md +289 -0
- package/agent/commands/command.template.md +339 -0
- package/agent/commands/git.commit.md +526 -0
- package/agent/commands/git.init.md +514 -0
- package/agent/commands/tanstack-cloudflare.deploy.md +272 -0
- package/agent/commands/tanstack-cloudflare.tail.md +275 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/design.template.md +154 -0
- package/agent/design/local.dashboard-layout-routing.md +288 -0
- package/agent/design/local.data-model-yaml-parsing.md +310 -0
- package/agent/design/local.search-filtering.md +331 -0
- package/agent/design/local.server-api-auto-refresh.md +235 -0
- package/agent/design/local.table-tree-views.md +299 -0
- package/agent/design/local.visualizer-requirements.md +349 -0
- package/agent/design/requirements.template.md +387 -0
- package/agent/index/.gitkeep +0 -0
- package/agent/index/acp.core.yaml +137 -0
- package/agent/index/local.main.template.yaml +37 -0
- package/agent/manifest.template.yaml +13 -0
- package/agent/manifest.yaml +302 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +67 -0
- package/agent/milestones/milestone-1-{title}.template.md +206 -0
- package/agent/milestones/milestone-2-dashboard-views-interaction.md +79 -0
- package/agent/package.template.yaml +86 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.template.md +1237 -0
- package/agent/patterns/pattern.template.md +382 -0
- package/agent/patterns/tanstack-cloudflare.acl-permissions.md +332 -0
- package/agent/patterns/tanstack-cloudflare.action-bar-item.md +416 -0
- package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +401 -0
- package/agent/patterns/tanstack-cloudflare.auth-session-management.md +387 -0
- package/agent/patterns/tanstack-cloudflare.card-and-list.md +271 -0
- package/agent/patterns/tanstack-cloudflare.chat-engine.md +353 -0
- package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +346 -0
- package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +516 -0
- package/agent/patterns/tanstack-cloudflare.email-service.md +431 -0
- package/agent/patterns/tanstack-cloudflare.expander.md +98 -0
- package/agent/patterns/tanstack-cloudflare.fcm-push.md +115 -0
- package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +441 -0
- package/agent/patterns/tanstack-cloudflare.firebase-auth.md +348 -0
- package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +550 -0
- package/agent/patterns/tanstack-cloudflare.firebase-storage.md +369 -0
- package/agent/patterns/tanstack-cloudflare.form-controls.md +145 -0
- package/agent/patterns/tanstack-cloudflare.global-search-context.md +93 -0
- package/agent/patterns/tanstack-cloudflare.image-carousel.md +126 -0
- package/agent/patterns/tanstack-cloudflare.library-services.md +553 -0
- package/agent/patterns/tanstack-cloudflare.lightbox.md +169 -0
- package/agent/patterns/tanstack-cloudflare.markdown-content.md +115 -0
- package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +98 -0
- package/agent/patterns/tanstack-cloudflare.modal.md +156 -0
- package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +461 -0
- package/agent/patterns/tanstack-cloudflare.notifications-engine.md +151 -0
- package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +90 -0
- package/agent/patterns/tanstack-cloudflare.og-metadata.md +296 -0
- package/agent/patterns/tanstack-cloudflare.pagination.md +442 -0
- package/agent/patterns/tanstack-cloudflare.pill-input.md +220 -0
- package/agent/patterns/tanstack-cloudflare.provider-adapter.md +401 -0
- package/agent/patterns/tanstack-cloudflare.rate-limiting.md +323 -0
- package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +338 -0
- package/agent/patterns/tanstack-cloudflare.searchable-settings.md +375 -0
- package/agent/patterns/tanstack-cloudflare.slide-over.md +129 -0
- package/agent/patterns/tanstack-cloudflare.ssr-preload.md +571 -0
- package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +508 -0
- package/agent/patterns/tanstack-cloudflare.toast-system.md +142 -0
- package/agent/patterns/tanstack-cloudflare.unified-header.md +280 -0
- package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +628 -0
- package/agent/patterns/tanstack-cloudflare.websocket-manager.md +237 -0
- package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +358 -0
- package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +336 -0
- package/agent/progress.template.yaml +161 -0
- package/agent/progress.yaml +145 -0
- package/agent/schemas/package.schema.yaml +276 -0
- package/agent/scripts/acp.common.sh +1781 -0
- package/agent/scripts/acp.install.sh +333 -0
- package/agent/scripts/acp.package-create.sh +924 -0
- package/agent/scripts/acp.package-info.sh +288 -0
- package/agent/scripts/acp.package-install.sh +893 -0
- package/agent/scripts/acp.package-list.sh +311 -0
- package/agent/scripts/acp.package-publish.sh +420 -0
- package/agent/scripts/acp.package-remove.sh +348 -0
- package/agent/scripts/acp.package-search.sh +156 -0
- package/agent/scripts/acp.package-update.sh +517 -0
- package/agent/scripts/acp.package-validate.sh +1018 -0
- package/agent/scripts/acp.uninstall.sh +85 -0
- package/agent/scripts/acp.version-check-for-updates.sh +98 -0
- package/agent/scripts/acp.version-check.sh +47 -0
- package/agent/scripts/acp.version-update.sh +176 -0
- package/agent/scripts/acp.yaml-parser.sh +985 -0
- package/agent/scripts/acp.yaml-validate.sh +205 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +210 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +294 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +193 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +262 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +156 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +178 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +141 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +153 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +174 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +233 -0
- package/agent/tasks/task-1-{title}.template.md +244 -0
- package/bin/visualize.mjs +84 -0
- package/package.json +48 -0
- package/src/components/ExtraFieldsBadge.tsx +15 -0
- package/src/components/FilterBar.tsx +33 -0
- package/src/components/Header.tsx +23 -0
- package/src/components/MilestoneTable.tsx +167 -0
- package/src/components/MilestoneTree.tsx +84 -0
- package/src/components/ProgressBar.tsx +20 -0
- package/src/components/SearchInput.tsx +22 -0
- package/src/components/Sidebar.tsx +54 -0
- package/src/components/StatusBadge.tsx +23 -0
- package/src/components/StatusDot.tsx +12 -0
- package/src/components/TaskList.tsx +36 -0
- package/src/components/ViewToggle.tsx +31 -0
- package/src/lib/config.ts +8 -0
- package/src/lib/file-watcher.ts +43 -0
- package/src/lib/search.ts +48 -0
- package/src/lib/types.ts +73 -0
- package/src/lib/useAutoRefresh.ts +31 -0
- package/src/lib/useCollapse.ts +31 -0
- package/src/lib/useFilteredData.ts +55 -0
- package/src/lib/yaml-loader-real.spec.ts +47 -0
- package/src/lib/yaml-loader.spec.ts +201 -0
- package/src/lib/yaml-loader.ts +265 -0
- package/src/routeTree.gen.ts +140 -0
- package/src/router.tsx +10 -0
- package/src/routes/__root.tsx +75 -0
- package/src/routes/api/watch.ts +29 -0
- package/src/routes/index.tsx +115 -0
- package/src/routes/milestones.tsx +50 -0
- package/src/routes/search.tsx +84 -0
- package/src/routes/tasks.tsx +63 -0
- package/src/services/progress-database.service.ts +46 -0
- package/src/styles.css +25 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +16 -0
- 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
|