Guides

Self-hosting

The entire stack runs from a single docker compose file: the identification API, the web app, Postgres for visitor storage, and Redis for rate limiting and caching. One command boots all four.

Boot the stack

terminal
docker compose up

Once the containers are healthy:

  • API on http://localhost:8080
  • Web app and dashboard on their compose-mapped ports
  • Demo keys seeded automatically: public vp_pk_demo_public_key, secret vp_sk_demo_secret_key, dashboard token veriprint-dashboard-demo

Verify the API is up with the seeded public key:

terminal
curl -X POST http://localhost:8080/v1/identify \
  -H "x-api-key: vp_pk_demo_public_key" \
  -H "content-type: application/json" \
  -d '{"signals":{"timezone":"Europe/Berlin","platform":"MacIntel"}}'

Architecture

Four services, one network:

ServiceRole
apiThe identification engine. Serves POST /v1/identify and GET /v1/events/:requestId on port 8080, runs similarity matching, and writes events.
webThe Next.js site and dashboard: event stream, API key management, docs.
postgresDurable storage: visitor clusters (signal vectors, weights, timestamps), identification events, and API keys.
redisPer-key rate limit counters and hot-path caching in front of Postgres.

Environment variables

The compose file ships working defaults for local development. Override these for any real deployment:

VariablePurpose
DATABASE_URLPostgres connection string used by the API for visitors, events, and keys.
REDIS_URLRedis connection string used for rate limiting and caching.
DEMO_PUBLIC_KEYPublic key seeded at boot (default vp_pk_demo_public_key). Set your own or disable seeding in production.
DEMO_SECRET_KEYSecret key seeded at boot (default vp_sk_demo_secret_key).
RATE_LIMIT_MAXPer-key request ceiling enforced via Redis. Exceeding it returns 429 with the limit echoed in the body.
.env
DATABASE_URL=postgres://veriprint:change-me@postgres:5432/veriprint
REDIS_URL=redis://redis:6379
DEMO_PUBLIC_KEY=vp_pk_your_public_key
DEMO_SECRET_KEY=vp_sk_your_secret_key
RATE_LIMIT_MAX=120

Enabling server-side TLS fingerprinting

The strongest bot and spoof-resistance signal is the TLS handshake (JA4/JA3), which JavaScript cannot read or forge. The API does not compute it (Node terminates TLS before your code runs); instead it reads it from a header set by a fingerprinting reverse proxy in front of the API. Put something like fingerproxy or a JA4-capable edge (or Cloudflare Bot Management) ahead of the API so it sets one of x-ja4, cf-ja4, or x-http2-fingerprint. Veriprint folds the value in as the tlsFingerprint signal automatically. Override the header names it looks for with VERIPRINT_TLS_HEADERS (comma-separated). With no such proxy the signal is simply absent, and everything else works unchanged.

Before production
Rotate the seeded demo credentials and delete or change the seeded demo account ([email protected]), which is public knowledge from this documentation. The dashboard uses real email and password accounts with httpOnly session cookies. Put the API behind TLS, and point the browser SDK at your deployment with the endpoint option (or proxy /api/veriprint/v1/identify from your own origin, which is the SDK default and keeps identify traffic first-party).