Skip to content

Bug: Milestone Achievements Use localStorage as Deduplication Guard — Causing Duplicate Activity Feed Spam Across Devices #88

@HunarBhatia

Description

@HunarBhatia

Bug: Milestone Achievements Use localStorage as Deduplication Guard — Causing Duplicate Activity Feed Spam Across Devices

Describe the bug
Milestone achievements (e.g. "🎉 Read 1000 pages!", "🏆 Completed 10 books!") are posted to the Supabase activity feed multiple times if the user logs in from a second device, clears browser storage, or uses incognito mode.

Root Cause
In apps/web/src/pages/Stats.jsx, the functions getAchievedMilestones() and markMilestoneAchieved() (lines 11–27) use localStorage under the key bookcat_achieved_milestones as the sole guard against re-triggering milestones. Since localStorage is browser/device-local, this deduplication does not persist across devices or sessions. Every new device or cleared browser will re-fire all milestones and call logMilestone() again, writing duplicate entries into Supabase.

Steps to Reproduce

  1. Use the app on Device A until a milestone is reached (e.g. complete 5 books) — milestone posts to activity feed
  2. Log into the same account on Device B (or clear localStorage on Device A)
  3. Open the Stats page
  4. The same milestone fires again and a duplicate entry appears in the activity feed

Expected Behavior
Each milestone should only ever be logged once per user, regardless of device or browser. The deduplication guard must live server-side, not in localStorage.

Suggested Fix
Move milestone deduplication to the database using one of these approaches:

Option A — Add a dedicated table:
Create a user_milestones table in Supabase with a unique constraint on (user_id, milestone_key). Before calling logMilestone(), insert into this table. If the insert fails due to the unique constraint, skip logging.

Option B — Query existing activities:
Before calling logMilestone(), query the activities table to check whether an activity with the same milestone_type and value metadata already exists for the user. Only log if no match is found.

localStorage can still be used as a fast client-side cache to avoid redundant DB lookups within a session, but the source of truth must be Supabase.

Files affected
apps/web/src/pages/Stats.jsxgetAchievedMilestones(), markMilestoneAchieved(), isMilestoneAchieved(), checkMilestones() (lines 11–60)

I am a GSSoC 2026 contributor and would love to work on this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions