Skip to main content

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​

ItemDetail
FrameworkReact 19
Build toolVite 7
Entry pointsrc/civic-pulse.jsx
StylingCSS custom properties (design tokens), no CSS-in-JS library
Font systemPlus Jakarta Sans (display) + Inter (body)
State managementReact built-ins: useState, useEffect, useCallback
Real-time clientSocket.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​

ItemDetail
RuntimeNode.js 18+
FrameworkExpress 4
Process modelcluster module β€” 1 worker per os.cpus().length
Real-timeSocket.IO 4, shares the HTTP server
AuthJWT (HS256), access + refresh token pattern
Validationexpress-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:

  1. helmet() β€” sets 14 security-related HTTP headers
  2. cors() β€” allowlist-based, supports credentials
  3. express-rate-limit β€” 100 req/15 min general, 20 req/15 min for /api/auth/*
  4. hpp() β€” HTTP Parameter Pollution protection
  5. express-mongo-sanitize β€” strips $ and . from user input
  6. express.json({ limit: '10kb' }) β€” body parser with size cap
  7. Custom XSS sanitizer β€” strips HTML tags from string fields
  8. Route-level express-validator chains
  9. JWT authenticateToken middleware (applied per-route, not globally)

Database β€” DynamoDB​

Table Design​

CivicPulse uses a single DynamoDB table with the following key schema:

AttributeTypeRole
PKString (partition key)Entity ID β€” e.g. ISSUE#<uuid>
SKString (sort key)Entity type or sub-item β€” e.g. METADATA
entityTypeStringISSUE, MESSAGE, SCORE, ASSIGNMENT, JURISDICTION, OFFICIAL, DEPARTMENT, POLICE_STATION

Global Secondary Indexes​

Index NamePartition KeySort KeyQuery Pattern
city-indexcitycreatedAtIssues by city, newest first
category-indexcategorycreatedAtIssues by category
status-indexstatuspriorityIssues 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:

  1. Client calls POST /api/issues/:id/media with { type: "upload", mimeType: "image/jpeg" }
  2. Backend generates a presigned PUT URL (15-minute TTL) and returns it with a mediaId
  3. Client uploads the file directly to S3 using the presigned URL
  4. Client notifies the backend that upload is complete (optional confirm step)
  5. Backend stores the S3 object key against the mediaId on 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 TypePK PatternDescription
jurisdictionJURISDICTION#<id>A ward or area with pincode ranges, constituency codes, and references to its officials
officialOFFICIAL#<id>An elected or appointed official (Ward Officer, Corporator, MLA, MP) with contact info
departmentDEPARTMENT#<id>A municipal department responsible for a category of issues (roads, water, etc.)
police_stationPOLICE_STATION#<id>A police station with jurisdiction over specific pincodes

Each jurisdiction record contains:

  • pincodes β€” array of 6-digit pincodes covered by this ward
  • constituency_assembly / constituency_parliamentary β€” codes linking to MLA and MP
  • ward_officer_id, corporator_id β€” direct foreign keys to official records

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:

CityWardsOfficialsDepartmentsPolice StationsTotal Records
Mumbai8~24~6~4~42
Thane3~9~3~2~17
Navi Mumbai3~9~3~1~16
Total14~42~12~775

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:

FieldTypeSource
pincodestringReverse geocoded from lat/lng, or entered manually
wardstringResolved from jurisdiction lookup
areastringReverse geocoded locality name
jurisdiction_idstringFK 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​

PlatformModelBackend ServiceEnv VarsAPI
X / TwitterPull (polling)xService.jsX_BEARER_TOKEN, X_HANDLEX API v2 search/recent
WhatsAppPush (webhook)whatsappService.jsWHATSAPP_TOKEN, WHATSAPP_VERIFY_TOKEN, WHATSAPP_PHONE_NUMBER_IDMeta WhatsApp Business Cloud API
InstagramPull (polling)igService.jsIG_ACCESS_TOKEN, IG_USER_ID, IG_HANDLEInstagram 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.

ServiceDemo TriggerMock Data
xServiceNo X_BEARER_TOKEN5 sample tweets from Indian cities
whatsappServiceNo WHATSAPP_TOKEN5 sample WhatsApp messages with media
igServiceNo IG_ACCESS_TOKEN or IG_USER_ID5 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 names
  • relativeTime(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:

ThreatControl
A01 Broken Access ControlRole-based middleware (citizen, official, admin) + ownership checks
A02 Cryptographic FailuresJWT HS256 with strong secrets, httpOnly refresh cookies, HTTPS enforced in prod
A03 Injectionexpress-validator, express-mongo-sanitize, parameterised DynamoDB queries
A04 Insecure DesignShort-lived access tokens (15 min), token rotation on refresh
A05 Security Misconfigurationhelmet(), explicit CORS allowlist, no default credentials
A06 Vulnerable ComponentsDependabot alerts, npm audit in CI
A07 Auth FailuresRate limit on /api/auth/*, account lockout after repeated failures
A08 Software IntegrityPackage lock files committed, integrity hashes verified in CI
A09 Logging FailuresStructured request logs, assignment audit trail in DynamoDB
A10 SSRFS3 presigned URLs scoped to a single bucket/prefix

See Security for detailed configuration.