Civiqapp architecture

Community — The Town Hall

District-scoped discussion boards. Membership isn't chosen — it's derived from the user's ZIP code, which is why the ZIP locks for 30 days once set.

Flowchart of the community forum: sign-in and ZIP gates, hub loading, feed pagination, posting with anchors, likes, comments, and reports
Two gates, then hub-scoped feeds. Anchors tie posts back to bills and representatives — and loop into the contact flow.

Hubs: geography as membership

A hub is a constituency: the user's congressional district (cd:CA-52), state house and senate districts, state, and city. GET /community/hubs returns the set the signed-in user belongs to, derived server-side from their profile ZIP. That derivation is the reason for the two gates in front of the forum — signed out, or signed in without a ZIP, each shows an explanatory gate that routes to the account page — and for the 30-day ZIP lock described on the Accounts page: letting users hop ZIPs freely would let them hop into arbitrary district discussions.

Feeds

GET /community/posts?hub=…&cursor=… pages each hub's feed with an opaque cursor. CommunityService keeps a per-hub in-memory cache (feed list plus next cursor) with single-flight loading per hub, so switching hub chips back and forth doesn't refetch, while pull-to-refresh replaces the first page. Infinite scroll appends via a sentinel row that schedules the next page load outside the build phase — loadPosts notifies listeners, which must never run during build. All community state is memory-only; nothing is persisted, so a restart starts fresh.

Posting and anchors

Posts carry a title, body, up to five tags, and optionally one anchor: a bill or a representative the discussion is about. Anchors are chosen from local caches only (the reps already loaded on the home page, and each rep's cached bill highlights) — a deliberate "no new network surface" decision. A representative anchor is validated client-side for hub containment: a rep must actually represent the target hub's constituency for the post to link them, computed from seat kind and the user's own districts (senators reach every hub in their state; a house member reaches their district and its city; a governor reaches statewide). The server stores anchors as opaque display metadata, so this containment check is a UX guarantee, not a security boundary.

In the feed, a bill anchor renders as an informational chip, while a representative anchor is tappable: it resolves the rep against the local cache and opens their full card in a sheet — with every action wired, including Email. That sheet is how forum discussion loops back into the contact flow.

Interaction mechanics

Likes (PUT /community/likes) are optimistic: the cached post flips immediately, and a failed request rolls it back before rethrowing. Comments (GET/POST /community/posts/{id}/comments) come back as a flat list ordered oldest-first with a depth field; the UI indents by depth rather than reconstructing a tree. Posting a comment bumps the cached post's comment count without refetching the feed.

Moderation surfaces are minimal by design. A removed comment keeps its place in the thread — author renders as "[removed]", the body as a placeholder — so replies stay coherent, and its action menu disappears entirely. Users can report posts and comments (POST /community/reports) and delete their own; deletion of a post is confirmed with an explicit "removes the post and its comments for everyone" dialog. Reporting currently sends an empty reason string; there is no reason picker yet.

Where enforcement lives

Every community endpoint requires both device attestation and a bearer token, and the server enforces hub membership (not_hub_member), authorship (not_author), and input validity on every write. The client-side gates and containment checks exist to produce good error messages and prevent doomed requests, not to be the security boundary.

AspectDecision
Hub kindscd, sh, ss, state, city
PaginationOpaque server cursor; no page-size parameter
Feed cacheIn-memory per hub; single-flight loads; no persistence
Post limitsTitle ≤ 200 chars; ≤ 5 tags; body optional
Anchor id formsBill: bill number; rep: 7-char short UUID
LikesOptimistic with rollback; comment likes parsed but not yet rendered
Removed commentsRedacted placeholder, position preserved

Dormant surfaces: CommunityService.reset() is documented "call on sign-out" but nothing calls it yet, and setCommentLike has no UI caller — comment like counts are parsed and ready for a future release.