feat: real-time pool group chat per pool (#90)#200
Conversation
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
left a comment
There was a problem hiding this comment.
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:
-
In-memory rate limiter is per-server-instance —
lastMessageAtis a module-scopedMap. 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., queryMAX(created_at)frompool_messagesfor the sender) or Redis instead. -
isMemberdepends onpool.pool_membersbeing in the API response — InGroupClient.tsx,isMembercheckspool.pool_members?.some(...). Verify that/api/poolsactually returnspool_membersin the response. If it doesn't,isMemberwill always befalseand the chat will always show the non-member wall.
Improvements:
-
Add DB-level constraints — The 500-char limit is enforced at the API only. Add
CHECK (char_length(message) <= 500)to the migration. Similarly, addCHECK (sender_address = lower(sender_address))to prevent mixed-case addresses from direct Supabase client writes. -
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.
-
loadOlderMessageshasmessagesin 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 frommessages[0]. -
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
left a comment
There was a problem hiding this comment.
Greate fix @Clement-coder - Approving
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— newpool_messagestable (pool_id, sender_address, message, created_at) with:pool_members) can read or write(pool_id, created_at DESC)for efficient paginationsupabase_realtimepublication for live deliveryAPI
frontend/app/api/pools/messages/route.tsHook
frontend/hooks/usePoolChat.ts—usePoolChatpostgres_changes INSERT) — zero pollingComponent
frontend/components/group/pool-chat.tsx—PoolChatEnterto send,Shift+Enterfor newline — fully mobile-responsiveIntegration
frontend/app/dashboard/group/[id]/GroupClient.tsx—PoolChatmounted belowGroupActivity;isMemberderived from already-fetchedpool_members(no extra network call)frontend/lib/supabase.ts— addedpool_messagesDatabase typefrontend/lib/constants.ts— addedCHAT_MESSAGE_MAX_LENGTH(500) andCHAT_RATE_LIMIT_MS(3000)Acceptance criteria checklist