Community Service
Port :3002. Everything community-scoped — communities themselves,
issues, petitions, representatives, comments, and the notification hub.
Issues live here, alongside petitions and representatives. The MVP colocates them so cross-entity flows (upvote → notification, petition signature → milestone → notification) stay in-process. Extract if scale demands it.
Responsibilities
- Communities — list, filter by state / LGA, create (admin roles only).
- Issues — CRUD, upvote toggle, status changes, comment threads.
- Petitions — CRUD, sign, comment threads, milestone notifications.
- Representatives — CRUD, follow / unfollow, comment threads, official-response notifications to followers.
- Notifications — persisted list plus realtime SSE push via an in-process hub.
- Search — global search across issues, petitions, representatives.
- Discover feed — personalized feed tiered by geographic proximity
(
COMMUNITY→LGA→STATE→COUNTRY). - Uploads — image upload endpoint (5 MB max, JPG/PNG/GIF/WEBP) plus public serve.
Package layout
services/community-service/
├── cmd/server/main.go
├── internal/
│ ├── audit/ # Writes to audit_logs (schema owned by identity)
│ ├── communities/ # Communities CRUD
│ ├── discover/ # Discover feed with tier logic
│ ├── domain/models.go
│ ├── issues/ # Issues + comments + upvotes
│ ├── middleware/ # JWTAuth, RequireVerified, RequirePrimaryCommunityMatch, RequireMembershipInCommunity
│ ├── notifications/ # Hub + service + handler (including SSE stream)
│ ├── petitions/ # Petitions + signatures + comments + milestones
│ ├── representatives/ # Reps + follows + comments
│ ├── search/ # Global ILIKE search
│ └── uploads/ # Multipart upload + static serve
├── migrations/
└── pkg/…
Key concepts
Content flags → hidden placeholders
Comments, issues, and petitions can be flagged. When a moderator
resolves a flag as HIDDEN, the read path in each repository replaces
the content and author name with placeholders — [Removed by moderator]
— without deleting the row. Conversation flow survives; the audit
trail is preserved; the offending payload is gone.
The isHidden field on the DTOs is computed at query time — never
stored. If the flag is dismissed later, the row is unaffected.
Notifications + SSE
- Every notification is persisted to the
notificationstable. - In parallel, the
notifications.Hubbroadcasts to any connected SSE subscribers for that user. - The SSE handler holds a goroutine per open browser tab; a 25-second keep-alive comment prevents intermediaries from closing idle connections.
- The frontend uses
EventSourceto subscribe and re-hydrates unread state on reconnect.
The hub is a map[userID]chan *Notification behind a sync.RWMutex.
It's the simplest thing that works. Replace with Redis pub/sub when the
service is horizontally scaled.
Community scoping middleware
Two middlewares gate community-scoped actions:
RequirePrimaryCommunityMatch— used on creation endpoints (issue, petition, representative). Confirms the caller'sprimaryCommunityIdmatches the community they're creating in.RequireMembershipInCommunity— used on interaction endpoints (comment, upvote, sign). Confirms the caller has a membership row for the target's community.
Both live in internal/middleware/.
Petition milestones
petitions.Handler.sign fires an extra notification when the new count
crosses 25 %, 50 %, or 100 % of the goal. The crossing is
determined by (newCount - 1 < threshold) && (newCount >= threshold) —
each milestone is emitted exactly once per petition per threshold.
Discover feed tiers
discover.Service.Feed labels each item with a tier based on the
caller's active community:
COMMUNITY— same community as callerLGA— same LGA, different communitySTATE— same state, different LGACOUNTRY— same country, different state
Passing tier= filters to one tier. Passing kind=issue|petition
filters the discriminator. Under the hood, it scans up to 1 000 items
from the DB and filters in-memory — fine at MVP scale, replace with
community_id IN (…) when the dataset grows.
Environment
DATABASE_URLJWT_SECRET(must match gateway + identity)PORT/COMMUNITY_SERVICE_PORT(default3002)