Skip to content

[Tracking] Stuck-Online users and the presence review follow-ups #496

Open
7 of 9 issues completed
Open

Description

@HMarzban

Summary

Two production users show ONLINE for good in the admin dashboard, though they left months ago. Their users.online_at is NULL. The cron job update-user-status (16-cron-jobs.sql:12-14) filters on online_at < now() - interval '2 minutes'. NULL < x is never true, so the job never sets them OFFLINE.

How the rows got there: from c825e32be (2026-02-12) to 28f90e0b3 (2026-05-15), Settings save sent the whole profile with status ONLINE and online_at NULL. The old trigger stamped online_at only when status changed, so these rows were never stamped.

The harm: the chat bell skips a member whose raw flag reads ONLINE (10-func-notifications.sql:383). Push and email fire only from a bell row. So the stuck users get no bell, push or email for plain chat messages. Admin "Online Now" also counts them.

Root cause

Postgres answers "is this user online" with two rules. The bell creator and admin read the raw flag users.status. Push reads the lease internal.is_user_online: status = 'ONLINE' and online_at within 2 minutes. #489 clears the stuck rows. #490 makes the lease the one rule for the bell. The other children are smaller follow-ups.

Decisions

The recommended answer comes first. Owner: the maintainer. The ruling is posted as a comment here.

  1. Leave apps/webapp/src/types/supabase.ts out of Stamp online_at only on ONLINE writes and clear users stuck ONLINE #489, Make the chat bell use the online lease instead of the raw status flag #490 and Make presence heartbeats HOT updates and drop the duplicate username index #491? Ruled 2026-10-15: yes. This is an exception to packages/supabase/CLAUDE.md:15, so it needs this ruling.
    • None of the three adds a table, a column, an enum value or a function signature. Function bodies and indexes do not reach the generated types.
    • The pinned CLI output already differs from the committed file on an unchanged local DB: 2,809 diff lines raw, 1,120 after Prettier (measured locally). A commit would put that drift into a bug fix.
    • So these children do not run types. A child that changes a type stops and asks.
    • Alternative: run types in each child and commit the drift with it.
    • The drift fix is Make the generated Supabase types reproducible again #498.

Child issues, in order

#494 (always-create bell rows) was closed as not planned. See Rejected by the review.

Shared rules

Children point here instead of repeating these.

The next db push

#489, #490 and #491 reach production by hand paste, so production has their objects but no history row. No child runs db push or migration repair for itself. The maintainer's next planned push runs the three files again. Each re-run is harmless: create or replace, an update that matches 0 rows, and if not exists guards. None of them calls cron.schedule.

At that push, the maintainer runs every bunx supabase command from packages/supabase:

  1. Check that packages/supabase/.temp/project-ref holds the production ref. If not, run bunx supabase link --project-ref <production ref>. Never paste the ref into an issue.
  2. Set [db.migrations] enabled = true in packages/supabase/config.toml.
  3. Run bunx supabase migration list. Write down every version with an empty Remote column.
  4. Run bunx supabase db push --dry-run. If it lists a version other than the Stamp online_at only on ONLINE writes and clear users stuck ONLINE #489, Make the chat bell use the online lease instead of the raw status flag #490 and Make presence heartbeats HOT updates and drop the duplicate username index #491 migrations, check it against packages/supabase/CLAUDE.md:23. If you are not sure, stop.
  5. Run bunx supabase db push. If it refuses because a local version sorts before the newest remote row, rerun it with --include-all. Do this only after step 4 passes, because it pushes every listed version.
  6. Set [db.migrations] enabled = false again. Never commit it as true.

What only the maintainer can do

Acceptance criteria

Close this tracker when all three hold.

Already tracked

These issues overlap the set. Each one stays as it is.

Rejected by the review

  • Delete the sweeper cron and drop idx_users_online_status. It saves at most 1.25 s of sweep time a day. The cron keeps the raw flag correct at no extra cost.
  • An internal.live_users view plus admin_online_count(). Its gain appears only at 1,000-member channels among 100k synthetic users. It also adds a second name for the rule.
  • A localStorage cross-tab heartbeat. It saves about a third of a write rate below 1 per second at 1x. It also adds an untested cross-tab protocol.
  • A heartbeat-only Status writer (no tab writes OFFLINE). It ends the tab race, but each leave then takes 60 to 120 s. In that window a plain message makes no bell, push or email. Fix the Status writer's tab race, reload gap and silent errors #492 keeps the hide write.
  • Push reads Redis occupancy in the worker. It adds a Redis call per push to save about 2 µs of SQL. Also, a mention in pad B would push while the user reads pad A.
  • The server writes Status from pad-socket edges. It overturns "Presence writes stay on Supabase" (apps/hocuspocus.server/CLAUDE.md:88). It also needs an atomic per-user count across replicas.
  • A presence-source interface or a per-user Redis key. Only one server-readable source exists, so the seam is hypothetical, and Postgres triggers cannot read Redis.
  • Always create a plain-message bell row (always-create). Make the chat bell use the online lease instead of the raw status flag #490 fixes the stuck members. Always-create alone emails content the member already read. Decide whether plain chat messages always create a bell row #494 (closed) holds the details.
  • Applying a presenceSync as one store write. At 50 faces it costs microseconds.
  • Industry-scale presence (a presence process, CRDT replication, Teams-style state precedence). Those designs serve millions of sessions; docs.plus has hundreds of users.

Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    ChatRelated to chat features
    on Oct 14, 2026
  2. HMarzban commented on Oct 15, 2026

    @HMarzban
    CollaboratorAuthor

    Maintainer ruling, 2026-10-15

    Decision 1: yes. #489, #490 and #491 leave apps/webapp/src/types/supabase.ts out. This is a recorded exception to packages/supabase/CLAUDE.md:15, because none of them changes a generated type. A child that does change a type stops and asks.

    The types drift is #498.

    Also ruled today:

  3. added a commit that references this issue on Oct 16, 2026
  4. added this to the v2.2.0 milestone on Oct 18, 2026
  5. HMarzban commented on Oct 19, 2026

    @HMarzban
    CollaboratorAuthor

    Decision 1 exception is no longer needed (#498), 2026-10-19

    #498 made the generated types reproducible, so the rule in packages/supabase/CLAUDE.md applies again. Any SQL change includes the regenerated apps/webapp/src/types/supabase.ts. That includes #491.

    Checked today on the local database:

    • Two bun run --filter @docs.plus/supabase_back types runs in a row give the same file.
    • After the runs, git status shows no change to supabase.ts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ChatRelated to chat featuresbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions