Skip to content

Repository files navigation

VATComply

Rust API for VAT validation, ECB exchange rates, VAT rates, and IP geolocation. Paths, query parameters, status codes, and JSON bodies match https://api.vatcomply.com. The cut-over is in deploy/PRODUCTION.md.

A release binary serves /rates from an embedded ECB snapshot when disk and network are both unavailable. VIES then returns its documented unavailable error.

Self-hosting

Docker

docker run --rm -p 8000:8000 -v vatcomply-data:/data ghcr.io/madisvain/vatcomply
curl -fsS http://127.0.0.1:8000/healthz
curl -fsS http://127.0.0.1:8000/rates

Build the image yourself from this directory:

docker build -t vatcomply .
docker run --rm -p 8000:8000 vatcomply

The image is FROM scratch, runs as UID 65532, and declares DATA_DIR=/data. A root-owned volume logs a snapshot write error and still serves the embedded rates. To persist refreshes:

docker run --rm -v vatcomply-data:/data alpine chown 65532:65532 /data

HEALTHCHECK runs vatcomply healthcheck (a TCP GET of /healthz). There is no shell and no curl in the image.

Compose

From the repository root:

docker compose -f deploy/docker-compose.yml up -d

The compose file publishes port 8000, sets a 2 request/second limit with burst 4, and mounts a named volume at /data.

Binary and systemd

Install a release binary at /usr/local/bin/vatcomply (GitHub Releases via cargo-dist, or cargo build --release in this directory). Then:

sudo cp deploy/vatcomply.service /etc/systemd/system/vatcomply.service
sudo systemctl daemon-reload
sudo systemctl enable --now vatcomply

The unit uses DynamicUser=yes, StateDirectory=vatcomply, ProtectSystem=strict, NoNewPrivileges=yes, and Restart=on-failure. It listens on 127.0.0.1:8000. Put a reverse proxy in front.

Fly.io

fly volumes create vatcomply_data --region ams --size 1
fly deploy --config deploy/fly.toml

deploy/fly.toml trusts Fly-Client-IP. The public Cloudflare setup is in deploy/PRODUCTION.md.

Configuration

Every setting is an environment variable. vatcomply --help prints the same list.

Variable Default Purpose
PORT 8000 Listen port
BIND 0.0.0.0 Listen address
DATA_DIR ./data (Docker /data) Atomic rate snapshot directory
RATES_REFRESH_SECS 3600 ECB refresh interval
VIES_TIMEOUT_SECS 10 Per-attempt VIES timeout
VIES_CACHE_TTL_SECS 300 Cache TTL for valid: true results
RATE_LIMIT_RPS 0 Requests per second per client IP. 0 is off. Public deploy files set 2
RATE_LIMIT_BURST 4 Burst size
TRUSTED_IP_HEADER empty Client IP header. Empty uses the socket peer. X-Forwarded-For is never read unless this names it
GEO_COUNTRY_HEADERS CF-IPCountry,Cdn-RequestCountryCode First matching header wins
GEOIP_DB_PATH empty Optional MaxMind country .mmdb, used only when no country header matches
PUBLIC_BASE_URL http://localhost:8000 Absolute origin written into GET /
LOG_FORMAT pretty pretty or json
LOG_LEVEL info tracing filter
METRICS 0 1 mounts GET /metrics

ECB_HIST_URL, ECB_HIST_90D_URL, and VIES_URL override the upstream URLs. Leave them unset in production.

/health, /healthz, /ready, /readyz, and /metrics are not rate limited.

Reverse proxy and Cloudflare

Terminate TLS at the proxy. Forward only the headers you name.

For Cloudflare:

TRUSTED_IP_HEADER=CF-Connecting-IP

Leave GEO_COUNTRY_HEADERS at the default so CF-IPCountry fills /geolocate. Cache /rates, /vat_rates, /countries, /currencies, and the OpenAPI documents at the edge. Bypass /vat and /geolocate. Details are in deploy/PRODUCTION.md.

Operate

  • GET /healthz — process is up.
  • GET /readyz — JSON with the loaded rates date, age, source (embedded, disk, or ecb), and any non-closed VIES breakers. 503 until a book is loaded.
  • GET /ready — the v1 database shim, while a book is loaded.
  • GET /docs — Scalar. The script is served from /docs/scalar.js on the same origin.
  • GET /openapi.json and GET /docs/openapi.json — the same document. The second uses application/vnd.oai.openapi+json.

Stop with SIGTERM or SIGINT. The process stops accepting, drains in-flight requests, cancels the refresh task, and writes the snapshot.

Development

Rust 1.91.1 (rust-toolchain.toml).

cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo test

Tests use wiremock. They do not call ECB or VIES. Golden fixtures live in tests/golden/. Regenerate the OpenAPI snapshot with:

UPDATE_OPENAPI=1 cargo test --test openapi

bench/oha.sh load-tests /rates, /countries, /vat_rates, and /currencies. It is not a CI gate.

Fuzz targets (ecb_xml, vies_soap, query) live in fuzz/ and run on the weekly workflow, not on pull requests.

Intentional differences from v1 are in PLAN.md section 7.

About

VATcomply is a free API service for vat number validation, user ip geolocation and foreign exchange rates.

Topics

Resources

Stars

69 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages