Backend Architecture
Rust API, sidecar verification, nullifier enforcement, and database schema.
The Apertrue backend is a Rust service built on Axum. It handles proof verification, media storage, trust list management, content moderation, authority reporting, and blockchain coordination. This section covers the architecture — how requests flow through the system, what each service does, and how the components fit together.
Stack
| Component | Technology | Purpose |
|---|---|---|
| Web framework | Axum 0.7 (Tokio async) | HTTP routing, middleware, request handling |
| Database | PostgreSQL 16 (sqlx, compile-time checked) | Persistent storage, proof records, verification index |
| Cache | Redis 7 | Session caching, rate limit state |
| Object storage | Cloudflare R2 (S3-compatible) | Encrypted media blobs, served via CDN |
| ZK verification | Barretenberg sidecar (Node.js) | Proof verification service |
| Blockchain | Aztec submitter sidecar (Node.js) | Transaction relay to Aztec network |
Request flow
Every request passes through rate limiting (token bucket per IP address), CORS validation, and optional JWT authentication before reaching the route handler. Handlers are thin — they extract parameters and delegate to service layer functions that contain the business logic. Services interact with the database, object storage, sidecar services, and external APIs as needed.
API surface
The API is organised into route groups by domain:
| Group | Auth required | Purpose |
|---|---|---|
| Authentication | No | Passkey registration, login challenge/verify, token refresh |
| Media | Yes | Upload (up to 500 MB), retrieve, delete, verification status |
| Proof verification | Yes (rate-limited) | Submit split proofs, aggregated proofs, selective disclosure proofs, poll status |
| Media lookup | No (rate-limited) | PDQ hash lookup, vPDQ batch lookup, TMK temporal lookup |
| Merkle tree | No (rate-limited) | Current root, consistency proofs for audit |
| Trust list | No | Full trust list, Merkle root, Merkle proofs for intermediates |
| Anonymous credentials | No | Merkle path by commitment, current tree roots |
| Identity proofs | No (credential-identified) | Submit/retrieve JWT identity proofs, passport proofs |
| Wallet backups | No (content-addressed) | Store/retrieve encrypted wallet backups |
| Reporting | Yes | User content reports |
| Admin | Admin JWT | Moderation queue, hash management, authority reports |
Public endpoints (media lookup, trust list, credentials) require no authentication — anyone can verify content or query the trust list. Rate limiting prevents abuse on these open endpoints.
Proof verification
The backend verifies ZK proofs submitted by the browser. It does not generate proofs — proof generation happens entirely in the user's browser. The backend's role is to confirm that the proof is valid, the public inputs are consistent, and the proof hasn't been replayed.
Verification flow
- Input validation. The backend checks that the proof bytes are well-formed, the public inputs are valid hex strings, and the proof timestamp is within 5 minutes of the current time.
- Content hash check. The backend fetches the uploaded image from object storage, computes its SHA-256 hash, and compares it against the content hash in the proof's public inputs. This confirms the proof covers the actual uploaded file.
- Cryptographic verification. The proof and public inputs are sent to the ZK verification sidecar — a separate Node.js service running Barretenberg. The sidecar verifies the UltraHonk proof against the verification key and returns a boolean result. For split proofs, ProofA and ProofB are verified independently.
- Trust list root check. The trust list root in the proof's public inputs is compared against the Oracle's current root. This confirms the proof was generated against the current trust list, not an outdated or fabricated one.
- Replay prevention. The proof bytes are hashed and checked against the
used_proof_hashestable. If the hash exists, the proof has been submitted before and is rejected. Otherwise, the hash is stored. - Storage. The verified proof record is inserted into the
zk_proofstable. The content is added to the verification index — an anonymous table that maps PDQ hashes to verification status without linking to users or images.
Sidecar architecture
The backend does not embed a ZK verifier. Instead, it delegates cryptographic verification to a sidecar service — a lightweight Node.js process running Barretenberg's WASM verifier. This separation allows the verifier to be scaled, updated, and restarted independently of the main Rust service.
| Sidecar | Role |
|---|---|
| ZK verifier | Verifies UltraHonk proofs, returns boolean result |
| Aztec submitter | Relays proven transactions to Aztec network, polls status |
Media storage
Uploaded media is stored as encrypted blobs in Cloudflare R2, an S3-compatible object store. The backend stores only ciphertext — encryption happens in the browser before upload.
- Upload. The browser sends an encrypted blob as a multipart form upload (up to 500 MB). The backend generates a UUID as the media ID and stores the blob in R2 under a path keyed by user ID and media ID.
- CDN delivery. R2 is fronted by Cloudflare's CDN. The backend returns a CDN URL that the browser uses to fetch the encrypted blob for decryption and display.
- Metadata. The database stores the media record: storage key, CDN URL, dimensions, verification status, PDQ hash, disclosure preferences, and aggregation batch ID. The actual image content is never in the database.
- Deletion. Deleting media removes the R2 blob and the database record. Because the encryption key is stored only in the user's browser (IndexedDB), deleting the key achieves cryptographic erasure — even if the blob persists in backups, it cannot be decrypted.
Oracle service
The backend loads the Oracle's signed trust list bundle at startup and serves it to browsers for proof generation. On each request, the backend validates the bundle's Ed25519 multi-signatures (2-of-3 threshold), rejects bundles older than 7 days, and enforces version monotonicity. It also serves Merkle proofs for individual intermediate certificates — when a browser generates a proof for an image signed by a specific camera, it fetches that certificate's Merkle path from the backend. The full bundle structure and trust list mechanics are detailed in The Oracle & Trust List.
Verification index
The verification index is a privacy-preserving transparency log. It maps PDQ hashes to verification status without storing any link to users, images, or upload sessions.
| Field | Purpose |
|---|---|
| pdq_hash | SHA-256 of the media (for lookup) |
| status | verified_camera, verified_ai, unverified, failed |
| verified_at | When the verification was recorded |
| merkle_salt | Random salt for Merkle leaf commitment |
| leaf_index | Position in the Merkle tree |
A background worker (MerkleCommitter) periodically batches new verification index entries into a Merkle tree. The tree root is signed with an Ed25519 key and published. Anyone can request a consistency proof to verify that the tree is append-only — entries are never removed or modified, providing an auditable log of all verifications.
Background workers
Five background workers run alongside the HTTP server:
| Worker | Interval | Responsibility |
|---|---|---|
| BlockchainWorker | 5 seconds | Polls Aztec for transaction confirmations, updates proof status, prunes confirmed proof bytes after 24 hours |
| MerkleCommitter | Configurable (default 5 min) | Batches verification index entries into Merkle tree, signs and publishes root |
| AuthorityReportWorker | 30 seconds | Processes pending authority reports (NCMEC CyberTipline submissions) |
| NcmecHashSync | 1 hour | Syncs known-illegal hash lists from NCMEC (if enabled) |
| DataRetention | Daily | Scrubs IP addresses from logs after retention period expires |
Database schema
The PostgreSQL database has 36 incremental migrations covering:
| Domain | Key tables | Purpose |
|---|---|---|
| Users and auth | users, sessions | Profiles, JWT refresh tokens, device tracking |
| Media | media, pdq_hashes, video_metadata | Upload records, perceptual hashes, video codec/resolution/duration |
| Proofs | zk_proofs, aggregated_proofs, nullifiers, used_proof_hashes | Proof records, batch aggregation state, replay prevention |
| Verification index | verification_index, merkle_roots, merkle_tree_nodes | Anonymous verification log, Merkle tree structure |
| Trust list | trust_list_versions, trusted_signers, intermediates | Oracle bundle versions, promoted certificates |
| Identity | anonymous_credentials, identity_proofs, passport_identity_proofs | Credential tree, JWT identity, ZKPassport proofs |
| Blockchain | cryptocurrency_transactions, aggregation_batches | Aztec TX status tracking |
| Moderation | moderation_queue, known_illegal_hashes, authority_reports, illegal_hash_matches | Flagged content, CSAM hash database, NCMEC reports |
| Video | pdq_keyframe_hashes, tmk_fingerprints | Video keyframe hashes, temporal fingerprints |
| Backups | wallet_backups | Encrypted wallet backup storage (content-addressed) |
Aztec blockchain integration
The backend coordinates blockchain submission but does not execute Aztec transactions directly. The browser generates and proves transactions locally, then relays the proven transaction through the sidecar to the Aztec network.
- Browser proves TX. The browser constructs the Aztec transaction with private inputs (root commitment, image commitments, trust list root) and proves it locally using the WebAuthn account contract's entrypoint.
- Relay via sidecar. The proven transaction bytes are sent to the Aztec submitter sidecar, which relays them to the Aztec network. The sidecar returns a transaction hash.
- User confirms. The browser calls the backend's
confirm-aztec-txendpoint with the transaction hash, linking the proof record to the on-chain transaction. - Background polling. The BlockchainWorker polls the Aztec network for transaction confirmations. When confirmed, the proof record's blockchain status is updated with the block number.
Rate limiting
Rate limiting uses a token bucket algorithm per IP address. Each endpoint group has independent limits:
| Endpoint group | Default rate |
|---|---|
| Proof verification | 10 requests/minute |
| Credential operations | 5 requests/minute |
| Media lookups | 10 requests/minute |
| General requests | 60 requests/minute |
Stale token buckets are cleaned up every 5 minutes to prevent memory growth from unique IP addresses.
Data residency
All infrastructure is hosted in UK data centers:
- Database. PostgreSQL in AWS eu-west-2 (London).
- Cache. Redis in eu-west-2 (London).
- Object storage. Cloudflare R2 with EU jurisdiction.
- CDN. Cloudflare with EU cache preference.
- Compute. Application servers in London region with minimum 2 instances for high availability.
No data is replicated to US or Asia regions. IP addresses are scrubbed after the configured retention period. PII fields in the database are encrypted at the field level with AES-256-GCM.
The next section is the circuit reference — a catalogue of all 19 Noir circuits with their inputs, outputs, and constraint counts.