System Architecture
A multi-layered architecture combining client-side ZK proofs, a NestJS backend with 48 modules and 22 extracted packages, and private settlement on the Canton Network through a live mainnet validator and DAML attribution ledger.
Three-Layer Architecture at Scale
Agora is built on a three-layer architecture that separates concerns, maximizes security, and scales independently. The layers are connected by an event-driven pipeline and a shared schema package so changes propagate safely across web, mobile, and backend:
- Frontend: Next.js 15 web + React Native 0.81.5 mobile. Client-side proof generation, ZK identity management, offline quota tracking.
- Backend: NestJS 11 with 48 modules and 22 extracted @agora/* packages. PostgreSQL, Redis, Bull Queue, Socket.IO, analytics engine with k-anonymity.
- Settlement: Live Canton Network mainnet validator with a DAML attribution ledger. Canton Coin settlement with a shielded fee pool for private amounts.
Agora runs its own live Canton Network mainnet validator: vote receipts and poll events are anchored on a DAML attribution ledger, and Canton Coin settlement with a shielded fee pool keeps amounts private. Prometheus + Grafana monitor every critical path.
Frontend Layer
Web and mobile clients generate ZK proofs locally and never send secrets to the server. 40+ web routes, 18 mobile feature modules.
Key Responsibilities:
- •Client-side ZK proof generation (Semaphore, RLN, UniRep, Groth16)
- •Global + per-community ZK identity (SecureStore + Poseidon2 on mobile)
- •WebView-based Groth16 proof generation for RLN and Semaphore batch circuits
- •Offline quota tracking, real-time updates via Socket.IO
- •Three theme variants: Modern neon, Classic Light, Classic Dark
Backend Layer
NestJS monorepo with 48 modules and 22 extracted core packages. Runs the API, orchestrates ZK proof batching, drives analytics, and anchors settlement state on the Canton ledger.
Key Responsibilities:
- •48 domain modules across polls, votes, posts, communities, messaging, analytics, moderation, RLN, UniRep, and Canton settlement
- •22 extracted packages under @agora/* (auth, zk-engine, token-engine, messaging, analytics, realtime, anon-credential, anon-receipt…)
- •Merkle tree construction, batch proof queueing, nullifier indexing
- •Event store with schema registry and k-anonymity engine (k ≥ 25)
- •Real-time messaging, Socket.IO backpressure handling
Canton Settlement Layer
Agora runs its own live Canton Network mainnet validator. A DAML attribution ledger records token operations, vote receipts, and poll-created events, while Canton Coin settlement handles poll-activation fees and rewards privately.
Key Responsibilities:
- •DAML attribution ledger: token operations, vote receipts (BatchAnchored / VoteRecorded), poll-created events
- •Vote receipts and poll lifecycle events anchored on the Canton ledger
- •Canton Coin settlement for poll-activation fees and rewards
- •Shielded fee pool (Poseidon2 commitments + Merkle proofs) hides settlement amounts
- •Validator and featured-app rewards settled privately in Canton Coin
ZK Stack — Semaphore + RLN + UniRep + Groth16
Agora combines four ZK primitives. Semaphore v4 handles group membership. RLN provides rate-limiting nullifiers so anonymous posts cannot be spammed. UniRep adds epoch-based pseudonymous reputation. Groth16 provides succinct client-side proofs. Six custom circom circuits extend this foundation.
semaphore_batch.circomBatch Semaphore proofs — multiple votes in a single transaction
rln.circomRate-limiting nullifier for anti-spam in anonymous posts/comments
reputation_proof.circomProve UniRep reputation ≥ threshold without revealing identity
epoch_key.circomUniRep epoch key generation for pseudonymous actions
state_transition.circomUniRep state transition proof between epochs
shielded_fee.circomPrivacy-preserving fee proofs (hides exact poll activation cost)
Zero-Knowledge Proof Flow
Identity Creation
User generates a Semaphore v4 identity (secret + commitment). On mobile the identity is Poseidon2-derived and persisted in SecureStore. The secret never leaves the client.
Identity = (secret, nullifier, commitment)Group Registration
Identity commitment is added to the target group's Merkle tree (poll, community, or global). The backend tracks tree state; the client fetches updated witnesses for proof generation.
Merkle Tree = [commitment₁, commitment₂, ..., commitmentₙ]Proof Generation
The client generates a ZK proof bundling: Semaphore membership, RLN nullifier (for anti-spam), optional UniRep reputation, and an action-specific statement (vote, post, comment, boost).
Proof = ZK(identity ∈ tree ∧ action_valid ∧ identity_hidden ∧ rate_limit_ok)Verification & Canton Anchoring
ZK proofs are verified and vote receipts anchored on Agora's live Canton mainnet validator through a DAML attribution ledger. Poll-activation fees and rewards settle privately in Canton Coin.
verify(proof) → record(action) ∧ check(nullifier)Core Packages — 22 Extracted @agora/* Modules
During v6.0–v8.0 the Agora monorepo extracted 22 independent packages into packages/core and packages/infra. Each package is versioned, unit-tested, and reusable across web, mobile, and backend.
@agora/authJWT, SIWE, OAuth, password, RBAC guards
@agora/identityZK identity, password hashing, RBAC helpers
@agora/identity-vaultSecure credential storage for mobile
@agora/schemas7 shared domain schemas (auth/posts/polls/votes/messages/communities/wallet)
@agora/cryptoEncryption, key derivation, hashing, SIWE verification
@agora/zk-engineSemaphore v4 integration, snarkjs, circuit loader
@agora/anon-credentialAnonymous credential system with eligibility checks
@agora/anon-receiptAnonymous claim receipts for audit trails
@agora/token-engineWallet, rewards, staking, vesting, governance, treasury, batch payout
@agora/blockchainCanton ledger client — DAML contract bindings and settlement helpers
@agora/storageTypeORM entities, repositories, database abstraction
@agora/messagingReal-time messaging infrastructure over Socket.IO
@agora/notificationsEmail/SMS notification service
@agora/analyticsEvent analytics, schema registry, k-anonymity aggregates
@agora/achievementsAchievement definitions and tracking
@agora/reputationUniRep reputation protocol integration
@agora/subscriptionsSubscription tier management (Starter / Pro / Enterprise)
@agora/recommendationsCollaborative filtering recommendations
@agora/followersFollow relationship management
@agora/realtimeSocket.IO event bus, backpressure handling
@agora/cantonCanton validator integration for private enterprise settlement
@agora/smsSMS delivery (Twilio / Vonage)
Canton Settlement Infrastructure
ZK Rollup Stack
- •Gnark Prover (Go, gRPC:50051, 8 CPU / 32 GB RAM)
- •Prover worker pool (2 replicas, 2 CPU / 8 GB each)
- •Redis for job coordination (2 GB max memory)
- •Batch size 50, interval 30 s, max wait 300 s
- •Batches anchored on the Canton ledger
- •Prometheus + Grafana monitoring (port 3001)
Canton Validator
- •Private settlement layer for enterprise customers
- •Canton Coin (CC) flows settle off the public chain, visible only to the counterparties
- •HSM-backed key management for validator signing
- •Integrated via the
@agora/cantonpackage
ZK Canton Wallet — Private Fee Payments
How your identity stays hidden even while paying fees
The in-app wallet exists for one purpose: paying the small fees that keep the platform running — activating a poll or settling platform costs. A naive wallet would expose that payment on the ledger and link every fee back to a single address. Agora routes every fee through a Poseidon2 shielded pool — the fee enters as a commitment and is authorized with a zero-knowledge proof of ownership before it settles. Observers see only the pool; the exact fee amount and who paid it never leave the user's device.
secretMerkle tree
no payer addressCommit the fee privately
When a user funds a fee, the wallet commits a Poseidon2 hash of (amount, secret, nullifier) to the shielded pool. The Canton ledger sees only the commitment — never the fee amount or who funded it.
commitment = Poseidon2(amount, secret, nullifier)Prove the fee in-circuit
To pay a poll-activation or settlement fee, the user generates a Groth16 proof that their commitment exists in the pool's Merkle tree and that they know its secret — without revealing which leaf is theirs. Canton parties and ledger observers learn nothing linkable.
ZK(commitment ∈ pool ∧ payer_knows_secret)Nullifier prevents double-payment
A unique nullifier derived from the secret is published atomically with the fee. The Canton validator records it; the same commitment can never be used twice. The nullifier is unlinkable to the original commitment for any outside observer.
nullifier = Poseidon2(secret, commitment_index)Fee settles into the shielded pool
The fee settles into the shielded pool rather than to a visible address. Balance analysis and transaction-graph de-anonymization break, so the payer's identity and the exact fee amount stay hidden from any outside observer.
settlement = pool_commit(fee_amount, fresh_nullifier)What this buys you
Data Flow Architecture
1. Content Creation (Poll / Post / Community)
Organization or user creates content → Backend stores configuration → PostgreSQL + event store → Poll configuration recorded on the Canton ledger (for polls, the activation fee settles in Canton Coin)
2. Identity Registration
User generates Semaphore identity (client-side, Poseidon2) → Commitment sent to backend → Added to target Merkle tree → Tree root updated → Mobile client persists identity in SecureStore
3. Anonymous Action (Vote / Post / Boost)
Client generates ZK proof (Semaphore + RLN + UniRep if applicable) → Proof + action sent to backend → Nullifier check → Proof verified and the vote receipt anchored on the Canton ledger → Action recorded
4. Aggregation & Analytics
Backend aggregates actions from the Canton ledger and event store → k-anonymity engine (k ≥ 25) produces privacy-preserving metrics → Community health, social health, and funnel dashboards → Results publicly verifiable
5. Rewards & Settlement
Reward calculation determines each epoch's payouts → rewards settle privately in Canton Coin through the shielded fee pool → the DAML attribution ledger records each settlement → unclaimed rewards return to the fee pool
Merkle Trees for Group Membership
Agora uses Merkle trees to efficiently manage groups of eligible voters and community members without storing individual identities.
How It Works:
- Each voter's identity commitment is a leaf
- The tree root represents the entire eligible group
- ZK proofs demonstrate membership without revealing which leaf
- Tree depth 20 supports up to 1,048,576 members per group
Benefits:
- Logarithmic proof size (efficient verification)
- Scales to large voter groups
- No need to store the full member list on the ledger
- Dynamic updates via semaphore_batch circuit