Architecture
CivicPulse is a full-stack civic issue tracking platform built for India. This page describes how the major components fit together, their responsibilities, and the key design decisions behind each layer.
System Overviewβ
Issue Lifecycleβ
Frontendβ
Technologyβ
| Item | Detail |
|---|---|
| Framework | React 19 |
| Build tool | Vite 7 |
| Entry point | src/civic-pulse.jsx |
| Styling | CSS custom properties (design tokens), no CSS-in-JS library |
| Font system | Plus Jakarta Sans (display) + Inter (body) |
| State management | React built-ins: useState, useEffect, useCallback |
| Real-time client | Socket.IO client |
Component Architectureβ
The application UI is built as a modular React component tree organised by feature domain. Data (categories, cities, mock content) is extracted into src/data/*.json files for CMS-readiness β any JSON import can be swapped for a useCMS() hook call when a headless CMS is connected.
The main entry point is src/civic-pulse.jsx (~310 lines), which contains the root App component with global state, handlers, and routing. All UI components are extracted into src/components/ organised by domain:
src/
βββ civic-pulse.jsx App root β state, handlers, routing (~310 lines)
βββ data/ JSON content (categories, cities, flagsβ¦)
βββ styles/civic-pulse.css Design tokens + all component styles
βββ hooks/ useFlags, useUserLocation, usePushNotifications, useDonors
βββ utils/helpers.js uid, fmtDate, haversine, scoring utils
βββ components/
βββ shared/ (6) FlagsCtx, Ic, ImageMetaPanel, FlagModal, SignInModal, ShareModal
βββ media/ (4) Lightbox, UploadZone, MediaGallery, MediaStrip
βββ maps/ (2) IssueDetailMap, IssueClusterMap
βββ issues/ (9) ICard, Detail, ResolveModal, ReopenRequestInline, NewModal,
β ChatPanel, AssignmentPanel, WardSubmitModal, AccountabilityMini
βββ browse/ (4) CategoryBrowser, IssuesList, BrowseIssues, Cities
βββ social/ (4) ConvertModal, XFeed, WAFeed, SocialWall
βββ rankings/ (5) ContributorCard, ContributorDetail, ContributorLeaderboard,
β Leaderboard, CityRankings
βββ events/ (4) EventCard, CreateEventModal, EventDetail, Events
βββ dashboard/ (1) Dashboard
βββ support/ (4) FeatureRequestModal, JoinTeamModal, SupporterForm, SupportPage
βββ admin/ (2) AdminPanel, AccountabilityAdmin
βββ auth/ (1) UserProfile
βββ landing/ (1) Landing
Total: 47 component files + 4 hooks + 1 App root.
CSS Design Tokensβ
All visual values are defined as CSS custom properties on :root. Dark/light mode is toggled by swapping a data-theme attribute on <html>.
:root {
--color-bg-primary: #0f172a;
--color-bg-surface: #1e293b;
--color-accent: #6366f1;
--color-text-primary: #f1f5f9;
--radius-md: 8px;
--font-display: 'Plus Jakarta Sans', sans-serif;
--font-body: 'Inter', sans-serif;
}
[data-theme="light"] {
--color-bg-primary: #f8fafc;
--color-text-primary: #0f172a;
}
Backendβ
Technologyβ
| Item | Detail |
|---|---|
| Runtime | Node.js 18+ |
| Framework | Express 4 |
| Process model | cluster module β 1 worker per os.cpus().length |
| Real-time | Socket.IO 4, shares the HTTP server |
| Auth | JWT (HS256), access + refresh token pattern |
| Validation | express-validator chains on every route |
Cluster Modeβ
The master process forks one worker per CPU core. If a worker crashes, the master restarts it automatically. All workers share the same port via the OS load balancer.
Master PID 1234
βββ Worker 1 (CPU 0)
βββ Worker 2 (CPU 1)
βββ Worker 3 (CPU 2)
βββ Worker 4 (CPU 3)
Socket.IO sessions are sticky-session compatible via the cluster-adapter package, ensuring WebSocket connections remain on the same worker.
Middleware Pipelineβ
Every request passes through this ordered middleware chain before reaching route handlers:
helmet()β sets 14 security-related HTTP headerscors()β allowlist-based, supports credentialsexpress-rate-limitβ 100 req/15 min general, 20 req/15 min for/api/auth/*hpp()β HTTP Parameter Pollution protectionexpress-mongo-sanitizeβ strips$and.from user inputexpress.json({ limit: '10kb' })β body parser with size cap- Custom XSS sanitizer β strips HTML tags from string fields
- Route-level
express-validatorchains - JWT
authenticateTokenmiddleware (applied per-route, not globally)
Database β DynamoDBβ
Table Designβ
CivicPulse uses a single DynamoDB table with the following key schema:
| Attribute | Type | Role |
|---|---|---|
PK | String (partition key) | Entity ID β e.g. ISSUE#<uuid> |
SK | String (sort key) | Entity type or sub-item β e.g. METADATA |
entityType | String | ISSUE, MESSAGE, SCORE, ASSIGNMENT, JURISDICTION, OFFICIAL, DEPARTMENT, POLICE_STATION |
Global Secondary Indexesβ
| Index Name | Partition Key | Sort Key | Query Pattern |
|---|---|---|---|
city-index | city | createdAt | Issues by city, newest first |
category-index | category | createdAt | Issues by category |
status-index | status | priority | Issues by status + priority filter |
Paginationβ
DynamoDB pagination uses LastEvaluatedKey. The API encodes this as nextKey (base64 URL-safe string) in the response envelope. Pass it back as ?lastKey=<value> in subsequent requests.
Storage β S3β
Media files (images and videos) attached to issues are stored in a dedicated S3 bucket. The upload flow uses presigned URLs to avoid routing large binaries through the application server:
- Client calls
POST /api/issues/:id/mediawith{ type: "upload", mimeType: "image/jpeg" } - Backend generates a presigned PUT URL (15-minute TTL) and returns it with a
mediaId - Client uploads the file directly to S3 using the presigned URL
- Client notifies the backend that upload is complete (optional confirm step)
- Backend stores the S3 object key against the
mediaIdon the issue record
Real-Time β Socket.IOβ
Socket.IO runs on the same HTTP server as the REST API. Authentication is enforced at the handshake level β connections without a valid JWT are rejected before any events are processed.
Chat rooms are keyed by issue ID: room:issue:<issueId>. Clients join/leave rooms as they navigate between issue detail views.
See Real-Time Architecture for the full event reference.
Accountability Chainβ
The Accountability Chain is a hierarchical official assignment system that maps every civic issue to its chain of responsible officials. Given an issue's location, the system resolves the full chain from Ward Officer up through Corporator, MLA, and MP.
Entity Modelβ
Four entity types are stored in the same DynamoDB single-table schema (PK/SK/entityType):
| Entity Type | PK Pattern | Description |
|---|---|---|
jurisdiction | JURISDICTION#<id> | A ward or area with pincode ranges, constituency codes, and references to its officials |
official | OFFICIAL#<id> | An elected or appointed official (Ward Officer, Corporator, MLA, MP) with contact info |
department | DEPARTMENT#<id> | A municipal department responsible for a category of issues (roads, water, etc.) |
police_station | POLICE_STATION#<id> | A police station with jurisdiction over specific pincodes |
Each jurisdiction record contains:
pincodesβ array of 6-digit pincodes covered by this wardconstituency_assembly/constituency_parliamentaryβ codes linking to MLA and MPward_officer_id,corporator_idβ direct foreign keys toofficialrecords
Resolution Flowβ
When an issue is created with a location (lat/lng or pincode), the system resolves the full accountability chain:
Seed Dataβ
The system ships with seed data for three cities:
| City | Wards | Officials | Departments | Police Stations | Total Records |
|---|---|---|---|---|---|
| Mumbai | 8 | ~24 | ~6 | ~4 | ~42 |
| Thane | 3 | ~9 | ~3 | ~2 | ~17 |
| Navi Mumbai | 3 | ~9 | ~3 | ~1 | ~16 |
| Total | 14 | ~42 | ~12 | ~7 | 75 |
DynamoDB Migration Pathβ
The seed data is stored in a single JSON file (backend/data/accountability-chain.json) using the DynamoDB single-table pattern:
{
"PK": "JURISDICTION#mum-ward-a",
"SK": "METADATA",
"entityType": "jurisdiction",
"city": "Mumbai",
"ward": "A",
"pincodes": ["400001", "400002"],
"ward_officer_id": "off-mum-wo-a",
"corporator_id": "off-mum-corp-a",
"constituency_assembly": "mah-assembly-colaba",
"constituency_parliamentary": "mah-parl-south-mumbai"
}
When migrating to DynamoDB, these records can be loaded directly via BatchWriteItem with no schema transformation. The same GSIs (city-index, entityType filters) will support the query patterns used by the API.
Issue Schema Extensionsβ
Issues now carry additional location fields populated during creation:
| Field | Type | Source |
|---|---|---|
pincode | string | Reverse geocoded from lat/lng, or entered manually |
ward | string | Resolved from jurisdiction lookup |
area | string | Reverse geocoded locality name |
jurisdiction_id | string | FK to the matched jurisdiction record |
Social Integrationsβ
CivicPulse ingests civic complaints from three social platforms. Each follows the same data shape ({ id, text, user, timestamp, category_hint, city_hint, images[], videos[] }) and feeds into the shared ConvertModal β issueService.createIssue() pipeline.
Platform Comparisonβ
| Platform | Model | Backend Service | Env Vars | API |
|---|---|---|---|---|
| X / Twitter | Pull (polling) | xService.js | X_BEARER_TOKEN, X_HANDLE | X API v2 search/recent |
| Push (webhook) | whatsappService.js | WHATSAPP_TOKEN, WHATSAPP_VERIFY_TOKEN, WHATSAPP_PHONE_NUMBER_ID | Meta WhatsApp Business Cloud API | |
| Pull (polling) | igService.js | IG_ACCESS_TOKEN, IG_USER_ID, IG_HANDLE | Instagram Graph API mentioned_media |
Data Flowβ
Demo Modeβ
All three services support graceful fallback to demo mode when API credentials are not configured. Demo mode returns mock mentions that match the live data shape exactly, allowing full UI development and testing without API keys.
| Service | Demo Trigger | Mock Data |
|---|---|---|
| xService | No X_BEARER_TOKEN | 5 sample tweets from Indian cities |
| whatsappService | No WHATSAPP_TOKEN | 5 sample WhatsApp messages with media |
| igService | No IG_ACCESS_TOKEN or IG_USER_ID | 5 sample Instagram posts with images |
Shared Utilitiesβ
All three services use textDetection.js for:
detectCategory(text)β keyword-based civic category detection (road, traffic, infrastructure, hygiene, healthcare, systemic)detectCity(text)β scans for 12 known Indian city namesrelativeTime(isoString)β converts timestamps to human-readable format
See API Reference β X, WhatsApp, and Instagram for endpoint details.
Securityβ
CivicPulse addresses the OWASP Top 10 through the following controls:
| Threat | Control |
|---|---|
| A01 Broken Access Control | Role-based middleware (citizen, official, admin) + ownership checks |
| A02 Cryptographic Failures | JWT HS256 with strong secrets, httpOnly refresh cookies, HTTPS enforced in prod |
| A03 Injection | express-validator, express-mongo-sanitize, parameterised DynamoDB queries |
| A04 Insecure Design | Short-lived access tokens (15 min), token rotation on refresh |
| A05 Security Misconfiguration | helmet(), explicit CORS allowlist, no default credentials |
| A06 Vulnerable Components | Dependabot alerts, npm audit in CI |
| A07 Auth Failures | Rate limit on /api/auth/*, account lockout after repeated failures |
| A08 Software Integrity | Package lock files committed, integrity hashes verified in CI |
| A09 Logging Failures | Structured request logs, assignment audit trail in DynamoDB |
| A10 SSRF | S3 presigned URLs scoped to a single bucket/prefix |
See Security for detailed configuration.