A production-ready gRPC-web proxy stack that bridges browser-based Zcash wallets to lightwalletd and Zaino — open-source, self-hostable, and operator-ready in under 20 minutes.
Browsers cannot speak native gRPC. lightwalletd and Zaino require it. That gap means every browser-based Zcash wallet needs a translation layer — a gRPC-web proxy — to function at all.
The WebZjs documentation is explicit:
"To work in the web these need to be a special gRPC-web proxy to a regular lightwalletd instance. Using an unproxied URL will NOT work."
Before this project, teams either depended on ChainSafe's hosted endpoints (a single point of failure) or built fragile one-off setups with inconsistent security and no monitoring. Zcash Web Gateway solves this with a standardised, hardened, and fully documented stack that any operator can run.
Browser (WebZjs)
│ gRPC-web / HTTP 1.1
▼
Envoy Proxy ←── this repo
│ native gRPC / HTTP 2
▼
lightwalletd or Zaino
│ JSON-RPC
▼
zebrad (Zcash full node)
| Component | Role |
|---|---|
| Envoy | gRPC-web ↔ gRPC translation, TLS termination, CORS, rate limiting, health checks |
| lightwalletd | ZIP-307 gRPC server, connects to zebrad |
| zebrad | Zcash full node (regtest / mainnet) |
| WebZjs demo | Browser smoke test — confirms the full path works end to end |
Prerequisites: Docker, Docker Compose
git clone https://github.com/ZeroIQ/zcash-web-gateway.git
cd zcash-web-gateway
cp .env.example .env
docker compose upWhen all services are healthy:
curl http://localhost:8080/healthz
# → 200 OKGateway is live at http://localhost:8080. Point any WebZjs instance at it.
docker compose up gateway-mainnetDefault upstream: mainnet.lightwalletd.com:9067 (TLS enabled). Override via .env:
UPSTREAM_MAINNET_HOST=mainnet.lightwalletd.com
UPSTREAM_MAINNET_PORT=9067docker compose -f docker-compose.yml -f docker-compose.zaino.yml upDefault upstream: zaino.zfnd.org:8137. Override UPSTREAM_HOST and UPSTREAM_PORT in .env.
All runtime config is driven by environment variables. Copy .env.example to .env and adjust.
| Variable | Default | Description |
|---|---|---|
UPSTREAM_HOST |
lightwalletd (internal) |
Upstream hostname |
UPSTREAM_PORT |
9067 |
Upstream port |
UPSTREAM_TLS |
true for mainnet, false for regtest |
Enable TLS to upstream |
GATEWAY_REGTEST_PORT |
8080 |
Host port for regtest gateway |
GATEWAY_MAINNET_PORT |
8081 |
Host port for mainnet gateway |
These are on by default. You don't have to configure them.
- Zero IP logging —
%DOWNSTREAM_REMOTE_ADDRESS%is deliberately absent from the access log format. Client IPs are never written to disk. - Rate limiting — configured at the Envoy filter layer to protect against abuse.
- CORS — allows gRPC-web required headers (
x-grpc-web,grpc-timeout,content-type) with correctexpose_headersfor trailers. - Circuit breakers —
max_connections: 1000,max_requests: 5000,max_retries: 3. - TCP keepalive — probes every 10s, 3 probes before disconnect, 30s idle threshold.
- TLS to upstream — SNI-aware, uses system CA bundle. Disabled only for local regtest plaintext.
The Envoy admin interface binds to 127.0.0.1:9901 only — never exposed publicly.
| Method | Timeout | Reason |
|---|---|---|
GetLatestBlock, GetBlock, GetTreeState, GetLatestTreeState |
10s | Fast unary reads |
GetTransaction, GetAddressUtxos |
15s | Medium unary |
SendTransaction |
30s | Broadcast propagation time |
GetTaddressTxids, GetAddressUtxosStream, GetSubtreeRoots |
30s | Bounded streaming |
GetBlockRange, GetMempoolTx, GetMempoolStream |
0s (no timeout) | Long-lived sync streams |
| All others | 20s | Catch-all |
A minimal browser app that exercises the full stack — useful for smoke testing any gateway endpoint.
cd demo-webzjs
npm install
npm run devOpen http://localhost:5173. Point the Gateway URL at your running instance and click Get Latest Block. A block height response confirms the entire chain — browser → Envoy → lightwalletd → zebrad — is working.
Note: Building WebZjs from source requires Rust nightly,
wasm-pack, andjust. Runbootstrap-webzjs.shonce beforenpm run dev. Pre-built packages are used by default.
zcash-web-gateway/
├── gateway/
│ ├── Dockerfile # Two-stage build: validates config before shipping it
│ ├── entrypoint.sh # envsubst + Envoy startup
│ └── envoy.yaml.tmpl # Envoy config template (upstream injected at runtime)
├── config/
│ ├── zebrad.toml # zebrad regtest config
│ └── zcash.conf # lightwalletd connection config
├── proto/
│ └── service.proto # ZIP-307 CompactTxStreamer (vendored, do not edit)
├── demo-webzjs/ # Browser smoke test app (TypeScript + Vite)
├── docker-compose.yml # Regtest stack (zebrad + lightwalletd + gateway)
├── docker-compose.zaino.yml # Zaino upstream override
├── .env.example
└── README.md
| Upstream | Port | TLS | Status |
|---|---|---|---|
| lightwalletd (ECC) | 9067 | Yes (mainnet), No (regtest) | Fully tested |
| Zaino (Zcash Foundation) | 8137 | Yes | Tested against zaino.zfnd.org |
- ZIP-307 — All CompactTxStreamer RPC methods are routed with purpose-tuned timeouts
- ZIP-316 — Unified Address payloads pass through Envoy without truncation (buffer limits validated)
- ZIP-317 —
RawTransaction.datais never modified in transit (Envoy passes bytes through unmodified)
# Gateway health
curl http://localhost:8080/healthz
# lightwalletd direct (bypass gateway)
grpcurl -plaintext localhost:9067 cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLightdInfoIssues, PRs, and operator registrations are welcome. If you run a public gateway endpoint, open a PR to add it to the registry.
MIT — see LICENSE.
Built with support from a Zcash Community . Depends on Zebra and lightwalletd.