Infrastructure

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

ComponentTechnologyPurpose
Web frameworkAxum 0.7 (Tokio async)HTTP routing, middleware, request handling
DatabasePostgreSQL 16 (sqlx, compile-time checked)Persistent storage, proof records, verification index
CacheRedis 7Session caching, rate limit state
Object storageCloudflare R2 (S3-compatible)Encrypted media blobs, served via CDN
ZK verificationBarretenberg sidecar (Node.js)Proof verification service
BlockchainAztec submitter sidecar (Node.js)Transaction relay to Aztec network

Request flow

Backend request flow: HTTPS request → Rate limiter → CORS check → Auth middleware → Route handler → Service layer → branches to PostgreSQL, Cloudflare R2, ZK verifier sidecar, External APIs (Moderation, NCMEC) → HTTPS response

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:

GroupAuth requiredPurpose
AuthenticationNoPasskey registration, login challenge/verify, token refresh
MediaYesUpload (up to 500 MB), retrieve, delete, verification status
Proof verificationYes (rate-limited)Submit split proofs, aggregated proofs, selective disclosure proofs, poll status
Media lookupNo (rate-limited)PDQ hash lookup, vPDQ batch lookup, TMK temporal lookup
Merkle treeNo (rate-limited)Current root, consistency proofs for audit
Trust listNoFull trust list, Merkle root, Merkle proofs for intermediates
Anonymous credentialsNoMerkle path by commitment, current tree roots
Identity proofsNo (credential-identified)Submit/retrieve JWT identity proofs, passport proofs
Wallet backupsNo (content-addressed)Store/retrieve encrypted wallet backups
ReportingYesUser content reports
AdminAdmin JWTModeration 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Replay prevention. The proof bytes are hashed and checked against the used_proof_hashes table. If the hash exists, the proof has been submitted before and is rejected. Otherwise, the hash is stored.
  6. Storage. The verified proof record is inserted into the zk_proofs table. 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.

SidecarRole
ZK verifierVerifies UltraHonk proofs, returns boolean result
Aztec submitterRelays 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.

FieldPurpose
pdq_hashSHA-256 of the media (for lookup)
statusverified_camera, verified_ai, unverified, failed
verified_atWhen the verification was recorded
merkle_saltRandom salt for Merkle leaf commitment
leaf_indexPosition 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:

WorkerIntervalResponsibility
BlockchainWorker5 secondsPolls Aztec for transaction confirmations, updates proof status, prunes confirmed proof bytes after 24 hours
MerkleCommitterConfigurable (default 5 min)Batches verification index entries into Merkle tree, signs and publishes root
AuthorityReportWorker30 secondsProcesses pending authority reports (NCMEC CyberTipline submissions)
NcmecHashSync1 hourSyncs known-illegal hash lists from NCMEC (if enabled)
DataRetentionDailyScrubs IP addresses from logs after retention period expires

Database schema

The PostgreSQL database has 36 incremental migrations covering:

DomainKey tablesPurpose
Users and authusers, sessionsProfiles, JWT refresh tokens, device tracking
Mediamedia, pdq_hashes, video_metadataUpload records, perceptual hashes, video codec/resolution/duration
Proofszk_proofs, aggregated_proofs, nullifiers, used_proof_hashesProof records, batch aggregation state, replay prevention
Verification indexverification_index, merkle_roots, merkle_tree_nodesAnonymous verification log, Merkle tree structure
Trust listtrust_list_versions, trusted_signers, intermediatesOracle bundle versions, promoted certificates
Identityanonymous_credentials, identity_proofs, passport_identity_proofsCredential tree, JWT identity, ZKPassport proofs
Blockchaincryptocurrency_transactions, aggregation_batchesAztec TX status tracking
Moderationmoderation_queue, known_illegal_hashes, authority_reports, illegal_hash_matchesFlagged content, CSAM hash database, NCMEC reports
Videopdq_keyframe_hashes, tmk_fingerprintsVideo keyframe hashes, temporal fingerprints
Backupswallet_backupsEncrypted wallet backup storage (content-addressed)
Key Insight
The verification index is deliberately anonymous. It contains PDQ hashes and verification status — no user IDs, no image URLs, no upload timestamps beyond the verification date. If the database is compromised, an attacker learns which PDQ hashes have been verified, but cannot link them to users or reconstruct images.

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.

  1. 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.
  2. 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.
  3. User confirms. The browser calls the backend's confirm-aztec-tx endpoint with the transaction hash, linking the proof record to the on-chain transaction.
  4. 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 groupDefault rate
Proof verification10 requests/minute
Credential operations5 requests/minute
Media lookups10 requests/minute
General requests60 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.