Technical Architecture
Deep dive into our system design and technology stack
Three-Layer Architecture at Scale
The anonymous lane has its own front door. Alongside the authenticated API, an anonymous action is posted to a separate endpoint, POST /v1/anonymous-actions, which rejects — rather than strips — any request carrying an authorization header, a cookie, a trace header, an API key, or a user, session or device header, so a client that would have leaked identity gets a 400 instead of a quiet success; the reads that answer questions about anonymous artefacts, such as membership manifests, sit behind the same rule. What the endpoint does not remove is the network: the request reaches the gateway over plain HTTPS and the gateway sees the client address, so an authenticated session and an anonymous submission from the same address seconds apart remain joinable by the operator, and while an authenticated WebSocket is open from that address the join is a lookup rather than an inference. Closing that needs a third-party relay we do not run. This lane runs on Agora's development stack today; production still serves the earlier write path, and the cutover is planned work rather than a shipped state. 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: Agora's own Canton mainnet validator, with a DAML package of write-only, single-signatory records. Canton Coin settlement plus a shielded fee pool that hides which payer funded a fee, not the amount. Ledger recording and the fee pool are both off by default.
Agora operates its own Canton mainnet validator. Poll events, vote records and token operations are modelled as single-signatory, write-only entries in a DAML package; that recording sits behind a configuration gate that is off by default, so it is a built capability rather than a running one. Canton Coin settles fees through a shielded pool that separates a deposit from the spend it later pays for, so a fee does not identify the payer who funded it — the fee amounts themselves are public. The backend exposes a Prometheus scrape endpoint; the Grafana dashboards that would consume it are configuration in this repo, not a deployed stack.
Frontend Layer
Proofs are generated on the client. On web, the v2 path derives the voting identity in the browser and proves in a Web Worker, so the identity secret stays on the device; that path is built but is not yet switched on in production. Shipped mobile builds still run the older flow, in which the server mints the voting identity and sends it to the app. 122 web routes, 20 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 52 modules and 25 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 in a fixed depth-24 Merkle tree) separates a deposit from the spend it funds, so a fee spend does not name the depositor; the fee amount is a public signal checked against a public price table
- •Featured App status is not held, so no app rewards accrue today — the reward rules changed under CIP-0104 and CIP-0116
ZK Stack — Semaphore + RLN + UniRep + Groth16
intro: "Agora's anonymous lane combines four mechanisms, only one of which is a zero-knowledge membership proof. A Privacy Pass publicly-verifiable token — an RFC 9474 RSA blind signature, profile rfc9578-blind-rsa-2048-sha384-pss-48 — authorises the action; it is one-time and unlinkable to its issuance. A Semaphore v4 Groth16 proof over a depth-20 Merkle tree establishes membership without naming the member. An Ed25519 cohort attestation reports which side of the readiness threshold the issuance cohort sits on, as a bucket rather than a count. On the charging lane, a Groth16 circom proof spends a deposit from the shielded fee pool. A ten-field keccak256 request digest binds the credential, the membership proof and the fee proof into one action, so a proof from one submission is refused on another. RLN is a v1 mechanism and v2 does not inherit it: in v1 the identity commitment sits beside the user id, which leaves an RLN-gated post attributable to its author by the operator. The UniRep-inspired reputation layer likewise sits outside this lane, and its circuits are written but not compiled. The lane is implemented and tested on a branch; it has not yet served a production user." title: "Anonymous-Action Stack — Blind-Signature Credential + Semaphore v4 + Cohort Attestation" Matching TR (fixing the same three defects in proposedTr): intro: "Agora'nın anonim hattı, yalnızca biri sıfır bilgi üyelik kanıtı olan dört mekanizmayı bir araya getirir. Bir Privacy Pass açık doğrulanabilir jetonu — rfc9578-blind-rsa-2048-sha384-pss-48 profilinde bir RFC 9474 RSA kör imzası — eylemi yetkilendirir; tek kullanımlıktır ve verilişine bağlanamaz. 20 derinlikli bir Merkle ağacı üzerindeki Semaphore v4 Groth16 kanıtı, üyeyi adlandırmadan üyeliği ortaya koyar. Bir Ed25519 kohort beyanı, veriliş kohortunun hazırlık eşiğinin hangi tarafında olduğunu sayı yerine bir kova olarak bildirir. Ücretli hatta bir Groth16 circom kanıtı, kalkanlı ücret havuzundan bir yatırımı harcar. On alanlı bir keccak256 istek özeti; kimlik bilgisini, üyelik kanıtını ve ücret kanıtını tek bir eyleme bağlar, böylece bir gönderime ait kanıt bir başkasında kabul edilmez. RLN bir v1 mekanizmasıdır ve v2 onu devralmaz: v1'de kimlik taahhüdü kullanıcı kimliğinin yanında durur, bu da RLN kapılı bir gönderiyi işletmeci açısından yazarına atfedilebilir bırakır. UniRep esinli itibar katmanı da bu hattın dışındadır; devreleri yazılmıştır ancak derlenmemiştir. Hat bir dalda uygulanmış ve test edilmiştir; henüz bir üretim kullanıcısına hizmet vermemiştir." title: "Anonim Eylem Yığını — Kör İmza Kimlik Bilgisi + Semaphore v4 + Kohort Beyanı"
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 assembles one envelope: a blind-signed anonymous credential, a Semaphore v4 membership proof, and — only where the action carries a fee — a shielded-pool spend proof. A single request digest, computed over the canonical action, the policy, and the credential's spend nullifier, is folded into the membership proof's message and into the fee proof's public inputs, so a proof minted for one action cannot be replayed onto another.
Proof = ZK(identity ∈ tree ∧ action_valid ∧ identity_hidden ∧ rate_limit_ok)Verification & Canton Anchoring
The server verifies the envelope, records the action, and — since August 2026 — stores the envelope itself in the same transaction, refusing it unless it hashes to the digest the action committed to. Where Canton settlement is switched on, the organisation's own Canton party writes the vote record, deferred until a sealed poll's results unseal. Ledger recording is built but gated off by default, and has not been verified running in production.
verify(proof) → record(action) ∧ check(nullifier)Core Packages — 22 Extracted @agora/* Modules
The Agora monorepo factors shared logic into 28 versioned @agora/* packages: 25 domain and cryptography packages under packages/core, and 3 interface packages under packages/ui. Each app pulls in only what it needs — the backend depends on 24 of them, the web app on 10, the mobile app on 8 — with @agora/schemas, @agora/crypto and @agora/zk-engine shared by all three.
@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 → on a deployment running Canton settlement, activating a poll transfers any non-zero activation fee in Canton Coin from the organisation's Canton party to the treasury party; writing the poll-created record to the Canton ledger is a separate switch that is off by default, and because the fee settles first a failed record leaves the poll active with no ledger entry
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 builds the canonical envelope and generates the Semaphore membership proof in a browser worker → POST /v1/anonymous-actions, an ingress that rejects the request outright — it does not strip it — when it carries an authorization header, a cookie, a trace header or any identity header → the credential is checked against the signed issuer key manifest, the cohort readiness gate is applied, and the membership proof is verified against the pinned verification key → one transaction inserts the spend nullifier and the semantic nullifier under unique constraints, records the action, and stores the canonical envelope as evidence — that insert, not the circuit, is what a second ballot in the same poll collides with → the ledger record follows afterwards, batched on EVM and deferred while a sealed poll is open on Canton, never inline with the request
4. Aggregation & Analytics
Backend aggregates actions from the Canton ledger and event store → k-anonymity engine (k ≥ 25) produces privacy-preserving metrics for the cohort dashboards → Community health, social health, and funnel dashboards → Results anchored on the ledger, where the organisation that ran the poll can check them against the record
5. Rewards & Settlement
Reward calculation runs off-chain → a claimed v2 participation receipt credits a UTC activity day rather than a poll → reward rows carry no poll link, and the points ledger refuses a participation row that references anything but the day → the line this draws: registering says "I may vote" and stays visible to the organiser by design, while being rewarded says "I voted" and is recorded only to the day
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
- Registrations and revocations produce a new snapshot of the tree, and the client proves against the published root