back to work

Consensus

Period 2025–26Role Founding Engineer

A polling and discussion platform designed to keep participant identities separate from voting and reporting data. Organizations receive live results, moderation tools, and province-level insights without access to individual voting histories.

Problem

The core product problem was supporting registered and guest participation while enforcing one vote per question and exposing only aggregate reporting to organizations.

Contribution

I designed the system architecture and built the Flutter app, backend services, data model, event-driven workflows, recommendation features, and AWS deployment.

Result

Private pilot with an institutional launch partner.

System

Flutter and React clients supported by 16 NestJS services running on AWS EKS, with PostgreSQL, Redis, Kafka, and RabbitMQ.

Main challenge

Keeping participant identity separate from voting data while coordinating live reporting, notifications, and recommendations across multiple services.

Consensus mobile app showing a daily poll and sentiment response options
Consensus mobile app showing the discussion below a poll
Consensus organization dashboard with poll results, province distribution, participation metrics, and recent polls
Overview

Consensus has three interfaces: a Flutter app for participants, a React operations panel for staff, and a React reporting panel for organizations. Polls can be scheduled or published as breaking questions, with threaded discussions and moderation workflows around them.

Registered and guest votes use different identifiers and PostgreSQL uniqueness constraints. Reporting services consume aggregate results, while organization users cannot access individual voting histories.

Service topology

The 16 NestJS services fall into four domains: identity and access; content and voting; social and messaging; and governance. The corporate BFF shapes organization-facing reads without exposing internal service boundaries.

Clients use REST and WebSocket through the Application Load Balancer. Direct internal dependencies use typed gRPC contracts; aggregation and delivery work leaves the request path through brokers, keeping vote submission independent from downstream effects.

Data ownership and anonymity

Each service writes only to its PostgreSQL schema; other services use its interface or consume its events instead of joining across schemas. This preserves ownership while avoiding database-per-service overhead at beta scale.

App, staff, and corporate users have separate JWT scopes. Guest identity becomes an HMAC-derived identity_hash, and question-scoped vote_hash values support database constraints for registered and guest votes. After a poll closes, the vote hash can be purged while aggregate results remain.

Event backbone

Kafka carries domain events for fan-out, replay, aggregation, and read-model updates. Independent consumers process votes.cast, consensus.closed, user.registered, and message events without extending the originating request.

RabbitMQ carries point-to-point delivery jobs for push and email workers. These jobs use acknowledgements, bounded retries, and dead-letter queues so failed deliveries can be retried or inspected independently.

Recommendation subsystem

NestJS remains the API and orchestration layer. Python owns offline ingestion, feature engineering, training, and candidate generation; Rust handles request-time scoring and compute-heavy aggregation, where predictable latency matters more than iteration speed.

Deployment and observability

Route 53 and CloudFront serve the public edge; Kubernetes Ingress provisions the ALB, and EKS runs service Deployments, scheduled workers, readiness probes, and HPA. RDS, ElastiCache, MSK, and Amazon MQ carry the stateful dependencies; GitHub Actions, ECR, and Helm handle delivery.

Prometheus scrapes RED metrics from each service, with Grafana and Alertmanager covering SLOs and alerts. Product signals such as votes per minute, active WebSocket connections, and push success rate sit beside infrastructure metrics.

Technical deep dive
System topology — clients, ingress, and domain servicesThree client applications enter through AWS edge and load-balancing infrastructure. Behind the ingress, 16 NestJS services are grouped by domain, communicate synchronously over gRPC when needed, and use service-owned PostgreSQL schemas together with shared infrastructure such as Redis and external providers.
System topology — clients, ingress, and domain servicesThree client applications enter through AWS edge and load-balancing infrastructure. Behind the ingress, 16 NestJS services are grouped by domain, communicate synchronously over gRPC when needed, and use service-owned PostgreSQL schemas together with shared infrastructure such as Redis and external providers.16 NestJS domain services · schema ownershipstaff SPAcorporate SPAmobile API / WSAPI pathsREST + WebSocketeast-west sync · gRPC · HTTP/2owned schemascache / limitsmediaauth / push / emailAdmin panelReact + Vite · staffCorporate panelReact + Vite · org usersConsensus AppFlutter · iOS / AndroidCloudFront + ACMS3 static bundles · TLSApplication Load Balancer/api · /socket.ioHTTP/2 + WebSocketIdentity & accessauth · identity · profileadmin · org · corporateContent & votingconsensus · votecomment · survey · heatmapSocial & messagingsocial · messagenotificationGovernancemoderationsuspension · auditPostgreSQLschema-per-serviceDB uniqueness invariantsRedisrate limit keysephemeral cacheCloud Storagemedia references in DBbinary blobs in bucketFirebase / FCM / SMTPphone authpush + email
Async backbone — Kafka events, RabbitMQ jobsKafka distributes domain events used for aggregation, read-model updates, cleanup, and profile creation without extending the request path. RabbitMQ handles point-to-point delivery jobs that require acknowledgements, retries, and dead-letter queues, such as push and email notifications.
Async backbone — Kafka events, RabbitMQ jobsKafka distributes domain events used for aggregation, read-model updates, cleanup, and profile creation without extending the request path. RabbitMQ handles point-to-point delivery jobs that require acknowledgements, retries, and dead-letter queues, such as push and email notifications.publishcountersvotes.castconsensus.closeduser.registeredmessages.*aggregateanonymizeprofile statedelivery jobscorporate statsEvent producersvote · consensusidentity · messageKafka topicsvotes.cast · consensus.closeduser.registeredmessages.sent/readCorporate live counterslive panel statsvotes.cast · consensus.closedHeatmap aggregatesvote deltasVote cleanuppurge vote_hash on closeProfile creationregistered user CEPNotify + WSpush, read receipts, live statsMaterialized outputsaggregatescorporate statsread modelsRabbitMQdelivery job queuesretry · DLQ · workers → FCM/SMTP
Recommendation and scoring — polyglot subsystemThe core platform stays on NestJS, while recommendation work is split by latency profile: Python owns offline ML and Rust owns request-time scoring and compute-heavy aggregation.
Recommendation and scoring — polyglot subsystemThe core platform stays on NestJS, while recommendation work is split by latency profile: Python owns offline ML and Rust owns request-time scoring and compute-heavy aggregation.Offline plane · PythonOnline plane · Rustbatch streamtraining datacandidate signalsfeaturesembeddingsmodel artifactANN indexonline featurescandidatesmodel versionevent streamranked feedtrending fallbackBehavioral signalsvotes.cast · user.registeredfollows · views · dwellFeature pipelineKafka -> ETLModel trainingCF + content embeddingsCandidate generationANN recallFeature storeonline + offline featuresEmbeddings / ANNpgvectorModel registryversioned artifactsRanker / scorerfeatures + candidatesgRPC · low latencyCEP + heatmap enginecompute-heavy aggregationRecommendation BFFNestJS API surfacefallback to trending
Identity separation and duplicate-vote preventionIdentity, voting, and reporting services own different data. PostgreSQL uniqueness constraints use registered-user or question-scoped guest voter keys to prevent duplicate votes, while reporting services store aggregate results rather than individual voting histories.
Identity separation and duplicate-vote preventionIdentity, voting, and reporting services own different data. PostgreSQL uniqueness constraints use registered-user or question-scoped guest voter keys to prevent duplicate votes, while reporting services store aggregate results rather than individual voting histories.Schema-per-service PostgreSQLapp identitystaff scopecorporate scopevote_hash HMACaggregate onlyrate-limit keysephemeral cacheApp tokenscope: nonemobile endpointsStaff tokenscope: staffadmin / moderationCorporate tokenscope: corporateown content + statsidentity.usersguest_id -> identity_hash7-day rotationadmin.staff_accountspanel accounts onlyorganization.memberscorporate accounts onlyconsensus / vote / commentUNIQUE(question,user)anon UNIQUE(question,vote_hash)heatmap.vote_aggregatesaggregate onlyno per-user traceabilityRedisIP · hash · user rate limitsephemeral sessions
Deployment and observability — AWS EKSTraffic enters through AWS edge services, workloads run as autoscaled pods in EKS, stateful dependencies are managed services, and Prometheus/Grafana track both technical and product signals.
Deployment and observability — AWS EKSTraffic enters through AWS edge services, workloads run as autoscaled pods in EKS, stateful dependencies are managed services, and Prometheus/Grafana track both technical and product signals.AWS EKS · Kubernetes cluster · VPC multi-AZDNSHTTPSIngress → ALB rulesREST + WSClusterIPjobsstatecacheevents/metricssecretsrolling deployexternal APIsRoute 53api / admin / corporate recordsCloudFront + S3static SPA bundlesApplication Load BalancerHTTPS · L7 routingAWS LB ControllerIngress -> ALB rulesService pods x16one Deployment per servicereadiness + HPAScheduler / workerclose polls · expire bansPrometheus + GrafanaRED metricsSLO panels + alertsConfig + secretsAWS Secrets Manager CSIRDS PostgreSQLMulti-AZ · schemasElastiCache Redisrate-limit · cacheAmazon MSKmanaged KafkaGitHub Actions -> ECRimage build + Helm deployGCS + Firebase / FCMmedia · phone auth · push
Key decisions & why
01

Domain-aligned service ownership

The 16 services follow product domains such as identity, voting, messaging, moderation, and reporting. Each service owns its schema and communicates through typed gRPC contracts or domain events.

02

Kafka for domain events, RabbitMQ for delivery jobs

Kafka provides replayable fan-out for domain events and read-model updates. RabbitMQ handles push and email jobs that need per-message acknowledgements, retries, and dead-letter queues.

03

Schema-per-service on one PostgreSQL

Schema ownership prevents cross-service writes and joins while one PostgreSQL instance keeps beta-stage operations manageable. A service can move to its own database later without changing the ownership contract.

04

Rust and Python only where they earn it

NestJS remains the default. Python is limited to the offline ML lifecycle, while Rust handles latency-sensitive scoring and aggregation; each additional runtime has a specific workload and interface.

05

EKS for workloads, managed services for state

Running 16 services on EKS was not required for pilot scale. It was an intentional operational learning exercise around Kubernetes deployment, service ownership, observability, and event-driven workflows.

Stack
  • TypeScript
  • NestJS
  • Flutter
  • React
  • PostgreSQL
  • Prisma ORM
  • Redis
  • gRPC
  • Kafka
  • RabbitMQ
  • Python
  • Rust
  • Docker
  • AWS EKS
  • Prometheus
  • Grafana