Skip to content

feat(auth): anonymous sessions#851

Open
ital0 wants to merge 52 commits into
mainfrom
italomenezes/thu-383-add-anonymous-session-support-with-turnstile-bot-protection
Open

feat(auth): anonymous sessions#851
ital0 wants to merge 52 commits into
mainfrom
italomenezes/thu-383-add-anonymous-session-support-with-turnstile-bot-protection

Conversation

@ital0
Copy link
Copy Markdown
Collaborator

@ital0 ital0 commented May 9, 2026

Note

Medium Risk
Touches authentication/session creation and PowerSync access control, plus a DB schema migration; mistakes could block logins or incorrectly allow/deny sync for some users. Changes are covered by new backend and frontend tests but still span critical identity flows.

Overview
Introduces anonymous user support end-to-end by adding user.is_anonymous (migration + Drizzle schema), enabling Better Auth’s anonymous plugin, and exposing isAnonymous via Better Auth additionalFields.

Adds guards and UX behavior around anonymous sessions: a new AnonymousSessionGuard auto-creates an anonymous session when entering the app without one, while UI elements (e.g. SidebarFooter, PowerSyncStatus, Preferences sync toggle/account deletion) treat anonymous users as effectively logged out and prompt sign-in.

Tightens sync security/capabilities by rejecting PowerSync GET /powersync/token (both session and bearer refresh paths) and PUT /powersync/upload for anonymous users, and updates DAL getUserById to return isAnonymous for these checks.

Adds promotion analytics + SSO bridge to alias anonymous IDs to real user IDs on successful OTP/SSO sign-in, plus cross-tab token-change handling via onAuthTokenChangedInOtherTab; includes extensive new tests for anonymous auth wiring, PowerSync rejection, guard behavior, analytics, and improved event-listener testing reliability.

Reviewed by Cursor Bugbot for commit 8c921b6. Bugbot is set up for automated code reviews on this repo. Configure here.

ital0 added 30 commits May 8, 2026 17:24
The 20-LOC information_schema columns query at the top of createAuth
forced the function async, cascading awaits through 11 files (elysia
plugin, index.ts, swagger.test.ts, and 8 auth/api test files).

The schema-drift test in CI plus Postgres' "column does not exist"
error on first anonymous sign-in already cover the deployment hazard
the health check was guarding against (M3 spec external-4). Removing
it lets createAuth stay sync, drops 11 mechanical await/Awaited<>
edits, and lets swagger.test.ts go back to being DB-less.
…load

Better Auth's M3 registration adds isAnonymous to additionalFields,
so the session.user object already carries it. The Path 1 + PUT /upload
re-fetches via getUserById were left over from when M4 ran in parallel
against an M3 that hadn't yet exposed the field.

Read user.isAnonymous directly off the session — eliminates two
redundant DB round-trips per token refresh / upload. Path 2
(Bearer-only refresh, no session derive) still needs getUserById.
- Replace per-file mock.module of @/contexts with createMockAuthClient
  + createTestProvider so tests exercise real provider wiring.
- Mock only single-export leaf modals (SignInModal, SyncSetupModal) and
  useIsMobile, per docs/development/testing.md.
- Add isAnonymous to MockAuthClientOptions session shape.
- remove mock.module() calls for app hooks, contexts, and modals
- rely on real implementations via createTestProvider + providers
- aligns with docs/development/testing.md (no mocking shared modules)
- Collapse phase state machine into a single hasAttempted flag
- Compute loading directly from observable state to avoid a render gap
  between mount and the post-commit effect firing
- Add effect cleanup to ignore stale completions on unmount
Comment thread backend/src/dal/anonymous.ts Outdated
Comment thread src/components/powersync-status.tsx Outdated
Comment thread src/hooks/use-powersync-credentials-invalid-listener.ts Outdated
Comment thread src/settings/preferences.tsx
Copy link
Copy Markdown
Collaborator

@raivieiraadriano92 raivieiraadriano92 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left a few question to confirm the correct behavior

…Account

- Drop the anonymous DAL (migration, row-cap, transient-error retry)
- Reduce onLinkAccount to a single delete of the anonymous user row
- Trim the auth integration test to cover the delete + fixation guard
ital0 added 3 commits May 12, 2026 13:50
Anonymous sessions have no real account to delete, so showing the
Delete My Account action is misleading and the underlying flow would
fail. Gate the section on !isAnonymous alongside isAuthenticated.
…tatus

- replace hide-entirely branch with reusing the logged-out popover path
- anonymous users now see the same sync affordance and Sign-In CTA as
  fully logged-out users, keeping the two states visually consistent
- relocate storage-event handler from use-powersync-credentials-invalid-
  listener.ts to AuthProvider so auth-token concerns live next to the
  rest of the auth lifecycle
- add coverage for both branches (token rotated → reload; token cleared
  → dispatch powersync_credentials_invalid)
- use the shared powersyncCredentialsInvalid event-name constant instead
  of repeating the string literal
Comment thread src/contexts/auth-context.test.ts Dismissed
Comment thread src/settings/preferences.tsx Outdated
- Sign In button already surfaces this affordance elsewhere
- update tests to assert on the button instead of hint text
Comment thread src/waitlist/use-waitlist-state.ts
…add-anonymous-session-support-with-turnstile-bot-protection

# Conflicts:
#	src/lib/auth-token.test.ts
#	src/lib/auth-token.ts
Comment thread src/lib/auth-token.test.ts Dismissed
Comment thread src/lib/auth-token.test.ts Dismissed
Comment thread src/components/sign-in/use-sign-in-form-state.ts
…ion tests

- inject trackEvent/alias deps into createAnonymousPromotionAnalytics so
  unit tests pass fakes instead of spyOn-ing the posthog ESM namespace
- dispatch real oauthRetryEvent via window in integration-completion
  tests instead of overwriting window.addEventListener (which leaked to
  sibling tests under --randomize --rerun-each)
@ital0 ital0 deployed to preview May 14, 2026 20:20 — with GitHub Actions Active
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8c921b6. Configure here.

if (user) {
if (user.isAnonymous) {
throw new ForbiddenError('Anonymous users cannot sync')
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anonymous user 403 causes PowerSync credential retry loop

Medium Severity

When the backend throws ForbiddenError for anonymous users hitting PowerSync endpoints, the safeErrorHandler returns { success: false, data: null, error: 'Forbidden' } with status 403 but no code field. The client-side getCredentialsInvalidReason in connector.ts only recognizes 403 with code: 'DEVICE_DISCONNECTED', so this 403 is treated as an unrecognized transient error — the connector returns null and PowerSync retries indefinitely. If a user previously had sync enabled and later gets an anonymous session (e.g., after signing out in bypass-waitlist mode), the connector will poll the token endpoint in a retry loop, generating repeated console errors and unnecessary network traffic.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8c921b6. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants