Skip to main content

Technology Decisions

Every technology choice has trade-offs. This page documents what we chose, why, and what alternatives were considered.


Frontend: Next.js 14

Chosen: Next.js 14 with App Router, TypeScript, standalone output

Why:

  • Server-side rendering for initial page load and SEO
  • App Router provides modern React patterns (Server Components, streaming)
  • output: 'standalone' produces a minimal Docker image (~90 MB)
  • Largest ecosystem for React-based web apps
  • Runtime environment variable injection via entrypoint.sh — same image for DEV and PRD

Considered:

  • Vite + React — lighter, faster dev server, but no SSR without additional setup. Good for SPAs, but we want the option for server-rendered pages (e.g. public wallet views, SEO)
  • SvelteKit — excellent DX, but smaller ecosystem. Team familiarity with React was decisive
  • Plain HTML/JS — the ZeroSync prototype uses this. Too limited for a production app with state management, routing, and component reuse

Styling: Tailwind CSS

Chosen: Tailwind CSS with custom dark theme and Bitcoin orange accent (#f7931a)

Why:

  • Utility-first — no context switching between CSS files and components
  • Consistent design system through tailwind.config.ts (colors, fonts)
  • Tiny production CSS — only used classes are bundled
  • Dark theme by default — matches the crypto/privacy aesthetic

Considered:

  • Flowbite — used in other DFX projects (dEURO, JuiceDollar). Skipped here because zkCoins has a distinct brand identity that doesn't fit Flowbite's component style
  • shadcn/ui — excellent components but adds complexity. May adopt later for complex UI elements
  • CSS Modules — more isolation but slower development velocity

State Management: Zustand

Chosen: Zustand with localStorage persistence

Why:

  • Minimal boilerplate — no providers, no reducers, no actions
  • Direct store access from any component via hooks
  • Built-in persistence middleware for localStorage
  • ~1 KB bundle size vs Redux (~7 KB) or MobX (~15 KB)
  • Perfect fit for a wallet app with simple state (account, balance, transactions)

Considered:

  • Redux Toolkit — used in DFX projects. Overkill for a wallet with 3 state slices
  • React Context — no persistence, re-renders entire tree on state change
  • Jotai/Recoil — atomic state model is elegant but less established

Cryptography: Rust → WebAssembly

Chosen: Rust compiled to WASM via wasm-pack, with JS fallback

Why:

  • secp256k1 in Rust — battle-tested, same implementation as Bitcoin Core
  • BIP32 HD wallets — deterministic key derivation in the browser
  • Schnorr signatures — required by the Shielded CSV protocol
  • Performance — WASM is 10-100x faster than pure JS for cryptographic operations
  • Security — compiled from audited Rust crates, not hand-written JS crypto

Why JS fallback:

  • WASM requires LLVM with wasm32 target for compilation (secp256k1 C library)
  • CI environments may not have the full Rust+LLVM toolchain
  • Fallback allows the app to function (with limited features) when WASM is unavailable

Considered:

  • noble-secp256k1 (JS) — pure JavaScript, no WASM needed. But: not the same implementation as Bitcoin Core, less confidence in edge cases
  • WebCrypto API — browser-native but doesn't support secp256k1 or Schnorr
  • libsodium.js — excellent for general crypto but doesn't support Bitcoin's specific curves

Backend: Rust / Axum

Chosen: Rust with Axum (async web framework)

Why:

  • Same language as the ZK circuits — shared types between the server and the Plonky2 circuit crates
  • Performance — Rust handles concurrent blockchain scanning and proof generation efficiently
  • Memory safety — critical for a system that manages cryptographic state
  • Axum — built on Tokio, idiomatic Rust, excellent middleware ecosystem
  • Direct port of ZeroSync prototype — minimal rewrite needed

Considered:

  • Node.js/TypeScript — used in DFX projects (indexer, prover). Would require rewriting all Rust types and Merkle tree logic. The shared Rust workspace between the server and the Plonky2 circuit is a significant advantage
  • Go — good for concurrent servers but cannot use the Plonky2 prover (a Rust library) natively, would need an FFI bridge
  • Python/FastAPI — too slow for proof generation and blockchain scanning

ZK Proofs: Plonky2 + Poseidon-Goldilocks

Chosen: Plonky2 with cyclic recursion — an in-process CPU prover (Apple Silicon)

The proving system began as an SP1 + SHA256 prototype and was migrated to Plonky2 + Poseidon-Goldilocks, which is the current implementation.

Why:

  • Cyclic recursion — a single circuit that recursively verifies proofs of itself. This is what realises Proof-Carrying Data (PCD) with constant proof size and constant verification time, the core requirement of Shielded CSV
  • No external prover dependency — proving runs in one in-process Rust prover: no external prover service and no decentralized proving network to depend on or trust
  • Shared Rust workspace — the circuit and the node live in the same Rust workspace and share types directly: no DSL, no language boundary, no FFI
  • Plonky3 as the long-term path — Plonky2 is treated as bridge technology; Plonky3 (Poseidon2, BabyBear field, active upstream development) is the long-term destination. The circuit is kept implementation-agnostic to ease that future port

Considered:

  • General-purpose zkVMs — let you write proofs in standard Rust, but add a VM execution layer and typically lean on an external or decentralized prover network for scaling. The migration to an in-process Plonky2 prover deliberately dropped that external dependency
  • Circom + Groth16 — used in Shade Protocol. Excellent for EVM verification but a poor fit for Bitcoin (no on-chain verifier). Also requires a trusted setup
  • Halo2 — used by Zcash. Mature but a complex DSL with a steep learning curve

Data Structures: Sparse Merkle Tree + Merkle Mountain Range

Chosen: SMT for state, MMR for history

Why:

  • SMT — supports both inclusion and non-inclusion proofs. Essential for proving a nullifier has NOT been published (double-spend prevention)
  • MMR — append-only, efficient for accumulating block-by-block commitment history
  • Both defined in the Shielded CSV paper — following the protocol specification
  • 256-bit key space — matches Bitcoin's hash output size

Considered:

  • Binary Merkle Tree — simpler but doesn't support non-inclusion proofs efficiently
  • Verkle Trees — more space-efficient but newer, less battle-tested, not specified by the protocol

Bitcoin Integration: Taproot Inscriptions via Esplora API

Chosen: Publish nullifiers as Taproot Inscriptions, scan via Esplora REST API

Why:

  • Taproot Inscriptions — arbitrary data in witness, minimal on-chain footprint
  • Esplora API — RESTful, well-documented, compatible with Bitcoin Core's REST interface
  • Prefix marker (4242) — simple filter for identifying zkCoins inscriptions among all Bitcoin transactions
  • No consensus changes — works with Bitcoin as it exists today

Considered:

  • OP_RETURN — simpler but limited to 80 bytes. Nullifiers fit, but Taproot Inscriptions are more future-proof for larger payloads
  • Bitcoin Core RPC directly — possible, but Esplora provides a cleaner REST interface for block scanning
  • Electrum protocol — efficient for UTXO queries but not designed for inscription scanning

Documentation: Docusaurus 3.9

Chosen: Docusaurus with dark mode default, deployed via Cloudflare Pages

Why:

  • Markdown-based — fast to write, version-controlled, diff-friendly
  • Used by Shade Protocol — consistent pattern across our projects
  • Built-in search, versioning, i18n — features we'll need as the project grows
  • Cloudflare Pages — zero-infrastructure hosting, automatic deploys on push
  • Separate repo — docs deploy independently from app/server

Considered:

  • Nextra — originally planned, but Docusaurus was chosen for consistency with Shade Protocol
  • GitBook — SaaS dependency, less control
  • VitePress — excellent for Vue projects, less established for React-adjacent teams

Hosting: Cloudflare Tunnel + Docker

Chosen: Docker containers on Mac Studio servers, exposed via Cloudflare Tunnel

Why:

  • No public IP needed — servers are LAN-only, Cloudflare Tunnel provides secure ingress
  • Existing infrastructure — dedicated servers already run 20+ containers
  • ARM64 native — Apple Silicon, no emulation overhead
  • Consistent deploy pattern — same Traefik setup, same monitoring across all services
  • Bitcoin node co-location — server connects to Bitcoin node via shared Docker network, no network latency

Considered:

  • Cloudflare Workers — serverless, but can't run Rust/Axum natively or maintain state (SMT/MMR)
  • AWS/GCP — would work but adds cost and complexity vs. existing on-prem infrastructure
  • Vercel — excellent for Next.js but can't host the Rust backend

CI/CD: GitHub Actions with SSH Deploy

Chosen: GitHub Actions (ARM64 runners) → Docker build → push to Docker Hub → SSH deploy via Cloudflare Tunnel

Why:

  • ARM64 runners — native builds for Apple Silicon, no cross-compilation
  • Docker Hub — simple, reliable image registry
  • SSH through Cloudflare Tunnel — secure deploy without exposing server ports
  • Command-restricted SSH keydeploy.sh limits what CI can execute on the server
  • Consistent with DFX deploy pattern — same workflow structure across all projects

Deploy flow:

Push to develop → GitHub Actions → Docker build (ARM64) → Push zkcoin/app:beta
→ SSH to DEV server → deploy.sh zkcoins-app
→ docker compose pull + up -d

Monitoring: Uptime Kuma

Chosen: Uptime Kuma (self-hosted), 6 monitors, 2 status pages

Why:

  • Already running — central monitoring for all DFX projects
  • Public status pages — status.zkcoins.app and dev-status.zkcoins.app
  • Simple — HTTP health checks, no agent installation needed
  • Self-hosted — no SaaS dependency

PWA: Service Worker + Web App Manifest

Chosen: Standalone PWA with cache-first strategy

Why:

  • Installable — users add to home screen on iOS/Android
  • Standalone mode — no browser chrome, native app feel
  • Offline capable — static assets cached, API calls network-first with fallback
  • No app store — instant updates, no review process
  • Bitcoin orange theme — status bar matches brand

Considered:

  • React Native — true native app, but doubles development effort. PWA is sufficient for MVP
  • Capacitor/Ionic — wraps web app in native shell. Adds complexity without significant benefit over PWA
  • Electron — desktop only, not mobile