Skip to content

feat: real-time pool group chat per pool (#90)#200

Merged
Sendi0011 merged 5 commits into
JointSave-org:mainfrom
Clement-coder:feat/pool-group-chat
Jul 25, 2026
Merged

feat: real-time pool group chat per pool (#90)#200
Sendi0011 merged 5 commits into
JointSave-org:mainfrom
Clement-coder:feat/pool-group-chat

Conversation

@Clement-coder

Copy link
Copy Markdown
Contributor

Summary

Closes #90

Adds a Discussion section to each pool's group page so members can coordinate without leaving the platform.

Changes

Database

  • supabase/migrations/20260725000000_pool_messages.sql — new pool_messages table (pool_id, sender_address, message, created_at) with:
    • RLS policies: only verified pool members (checked against pool_members) can read or write
    • Index on (pool_id, created_at DESC) for efficient pagination
    • Added to supabase_realtime publication for live delivery

API

  • frontend/app/api/pools/messages/route.ts
    • GET — paginated history (cursor-based, 50 per page), member-verified before any data is returned (mirrors RLS)
    • POST — insert message; enforces 500-char length cap and a 3-second per-sender server-side rate limit (separate map, doesn't eat global quota)

Hook

  • frontend/hooks/usePoolChat.tsusePoolChat
    • Initial history load + cursor pagination for infinite scroll
    • Supabase Realtime subscription (postgres_changes INSERT) — zero polling
    • Optimistic send with rollback on failure
    • Client-side 3-second rate-limit countdown (deduped against server limit)

Component

  • frontend/components/group/pool-chat.tsxPoolChat
    • Own/other message bubbles, skeleton loading, empty state
    • Load-earlier infinite scroll with scroll-position preservation
    • Auto-scroll to bottom on load/new message (respects scroll position)
    • Character counter, rate-limit countdown badge, non-member wall
    • Enter to send, Shift+Enter for newline — fully mobile-responsive

Integration

  • frontend/app/dashboard/group/[id]/GroupClient.tsxPoolChat mounted below GroupActivity; isMember derived from already-fetched pool_members (no extra network call)
  • frontend/lib/supabase.ts — added pool_messages Database type
  • frontend/lib/constants.ts — added CHAT_MESSAGE_MAX_LENGTH (500) and CHAT_RATE_LIMIT_MS (3000)

Acceptance criteria checklist

  • Only pool members can read or send messages (enforced via RLS and server-side API check)
  • Messages appear in real time without a page refresh (Supabase Realtime)
  • Rate limiting prevents spam (1 message per 3 s, server-side + client-side guard)
  • Chat history persists and loads when revisiting the page
  • Mobile-responsive

Add a Discussion section to each pool's group page with live
message delivery, persistent history, and spam prevention.

Changes:
- supabase/migrations/20260725000000_pool_messages.sql
  New pool_messages table with RLS policies (read + write
  restricted to verified pool members), index on
  (pool_id, created_at DESC), and realtime publication.

- frontend/app/api/pools/messages/route.ts
  GET  — paginated history (cursor-based, 50 per page),
         member-verified before any data is returned.
  POST — insert message; enforces 500-char length cap and a
         3-second per-sender rate limit server-side.

- frontend/hooks/usePoolChat.ts
  Manages history load, Supabase Realtime subscription
  (zero-polling live delivery), cursor pagination, optimistic
  send with rollback, and client-side rate-limit countdown.

- frontend/components/group/pool-chat.tsx
  Chat UI: own/other message bubbles, skeleton loading,
  empty state, load-earlier infinite scroll, auto-scroll,
  character counter, rate-limit countdown, non-member wall.

- frontend/app/dashboard/group/[id]/GroupClient.tsx
  Mount PoolChat below GroupActivity; derive isMember from
  already-fetched pool_members (no extra network call).

- frontend/lib/supabase.ts  — add pool_messages Database type
- frontend/lib/constants.ts — add CHAT_MESSAGE_MAX_LENGTH (500)
                              and CHAT_RATE_LIMIT_MS (3000)
@Sendi0011
Sendi0011 self-requested a review July 25, 2026 17:30

@Sendi0011 Sendi0011 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the well-structured PR! The architecture (migration → API → hook → component) is clean, and the Supabase Realtime integration with optimistic send is solid. A few issues to address:

Critical:

  1. In-memory rate limiter is per-server-instancelastMessageAt is a module-scoped Map. On Vercel serverless or multi-instance deploys, each instance has its own map. A user could send multiple messages simultaneously by hitting different instances. Use a database-backed check (e.g., query MAX(created_at) from pool_messages for the sender) or Redis instead.

  2. isMember depends on pool.pool_members being in the API response — In GroupClient.tsx, isMember checks pool.pool_members?.some(...). Verify that /api/pools actually returns pool_members in the response. If it doesn't, isMember will always be false and the chat will always show the non-member wall.

Improvements:

  1. Add DB-level constraints — The 500-char limit is enforced at the API only. Add CHECK (char_length(message) <= 500) to the migration. Similarly, add CHECK (sender_address = lower(sender_address)) to prevent mixed-case addresses from direct Supabase client writes.

  2. Realtime has no reconnection handling — If the Supabase Realtime connection drops, the user sees stale messages with no indication. Consider adding a connection status indicator or at minimum log and auto-reconnect.

  3. loadOlderMessages has messages in its dependency array — This recreates the callback on every message change, triggering unnecessary re-renders. Use a ref for the oldest message timestamp instead of reading from messages[0].

  4. No message edit/delete — This is fine for a chat system, but worth noting in the PR description that messages are permanent once sent, so reviewers and future contributors are aware of the design decision.

- API: replace module-scoped in-memory rate limit Map with a DB-backed
  query (MAX created_at per sender per pool). Safe across all serverless
  instances — no shared state between Vercel function invocations.

- Migration: add DB-level constraints to pool_messages:
    CHECK (char_length(message) <= 500)
    CHECK (sender_address = lower(sender_address))
  Also add idx_pool_messages_sender_recent index to support the new
  rate-limit query efficiently.

- Hook (usePoolChat): fix loadOlderMessages dependency array — cursor is
  now read from a ref (oldestCreatedAtRef) kept in sync via a useEffect,
  so the callback is not recreated on every message arrival.

- Hook + Component: add realtimeStatus ('connecting' | 'connected' |
  'disconnected') derived from Supabase channel subscribe() callback.
  PoolChat header badge now reflects live connection state with colour
  coding (green / yellow / red) and a descriptive tooltip.
@Sendi0011
Sendi0011 self-requested a review July 25, 2026 17:48

@Sendi0011 Sendi0011 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Greate fix @Clement-coder - Approving

@Sendi0011
Sendi0011 merged commit 793063b into JointSave-org:main Jul 25, 2026
4 checks passed
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.

[Feature] Build a real-time collaborative pool chat/comments feature per group

2 participants