Skip to main content

Running the Server

You need the docker compose file that runs duo-server, auth-svc, policy-svc and the admin dashboard, an .env file with your credentials, and a signing key for duo-server.

two-party-boilerplate-backend/
├── docker-compose.all.published.yml # duo-server + auth-svc + policy-svc + auth-svc-db + admin-dashboard
├── scripts/
| └── db/
| ├── init-auth-db.sql # creates the auth_svc role + database
| └── init-policy-db.sql # creates the policy_svc role + database
|── testdata/
| └── party_0_sk # duo-server signing key
└── .env

0. Get access to the private images

auth-svc is published as a public image. duo-server, policy-svc and admin-dashboard are private packages, so Docker has to be signed in to GitHub Container Registry before it can pull them.

Contact Silence Laboratories to grant your GitHub account read access to these packages, then sign in with a personal access token that has the read:packages scope:

echo "$GITHUB_PAT" | docker login ghcr.io -u <your-github-username> --password-stdin

Without this, docker compose up fails when it tries to pull the images.

1. Prepare the files needed to run the server:

If you have access to the auth-svc repository, every file below is already there. Skip to step 2.

You need to create the docker-compose file, the .env file, and generate a signing key for duo-server. Follow the instructions below to prepare these files.

Save the docker compose file as docker-compose.all.published.yml:

It is the same file that ships in the repository, so the command in step 3 is identical either way. Every service runs a pinned image — nothing is built.

docker-compose.all.published.yml

# The full published stack: duo-server + auth-svc + policy-svc + auth-svc-db +
# admin-dashboard. Every service runs a pinned image from ghcr.io, so nothing is
# built and no source checkout is required.
#
# docker compose -f docker-compose.all.published.yml up
#
#
# The file is self-contained apart from three paths it reads from the working
# directory:
#
# ./scripts/db/init-auth-db.sql
# ./scripts/db/init-policy-db.sql
# ./testdata/party_0_sk
#
# duo-server, policy-svc and admin-dashboard are private packages. Sign in to Docker
# to ghcr.io with a token holding `read:packages` before starting:
#
# echo "$GITHUB_PAT" | docker login ghcr.io -u <github-username> --password-stdin

networks:
duo-net:
driver: bridge

services:
# ── Duo Server ───────────────────────────────────────────────
duo-server:
image: "ghcr.io/silence-laboratories/dkls23-rs/duo-server:v6.4.0"
container_name: duo-server
command: /usr/local/bin/sigpair-node
environment:
RUST_LOG: "info"
LISTEN: "0.0.0.0:8080"
DEV_MASTER_SIGN_KEY: /run/secrets/duo-signing-master-key
FILE_STORAGE_URL: "file:///data/"
# Auth hooks configuration
AUTH_HOOK_TYPE: "webhook"
AUTH_DKG_SETUP_VALIDATOR_URL: "http://auth-svc:9090/hook/dkg_setup_validation"
AUTH_DSG_SETUP_VALIDATOR_URL: "http://auth-svc:9090/hook/dsg_setup_validation"
AUTH_KEY_ID_NOTIFICATION_URL: "http://auth-svc:9090/hook/key_id_notification"
ports:
- "8080:8080"
volumes:
- duo-server-data:/data
secrets:
- duo-signing-master-key
networks:
- duo-net

# ── Auth Service ─────────────────────────────────────────────
auth-svc:
image: ghcr.io/silence-laboratories/two-party-boilerplate-backend:a988c47569b7e5efaf55ad3e40d64d1f775d953a
container_name: auth-svc
ports:
- "9090:9090"
environment:
SECRET_KEY: ${SECRET_KEY:-djangosecretkey}
FEATURE_MOCK_AUTH_PROVIDER: "false"
FEATURE_KEYID_LOOKUP_BY_TOKEN: "true"
FEATURE_ALLOW_ED25519: "true"
AUTH_PROVIDER: "jwt"
AUTH0_DOMAIN: ${AUTH0_DOMAIN:?set AUTH0_DOMAIN in .env (your-tenant.auth0.com)}
AUTH0_AUDIENCE: ${AUTH0_AUDIENCE:?set AUTH0_AUDIENCE in .env (the Auth0 API identifier)}
AUTH0_ALGORITHMS: "RS256"
FACETEC_API_URL: ${FACETEC_API_URL:-https://api.facetec.com/api/v4/biometrics}
FACETEC_DEVICE_KEY: ${FACETEC_DEVICE_KEY}
FACETEC_SESSION_EXPIRY_SECONDS: ${FACETEC_SESSION_EXPIRY_SECONDS:-300}
HELIUS_API_KEY: ${HELIUS_API_KEY}
POLICY_SVC_URL: "http://policy-svc:5002"
PROXY_UPSTREAM_URL: "http://duo-server:8080"
FEATURE_POLICY_EVALUATION: ${FEATURE_POLICY_EVALUATION:-true}
DATABASE_URL: "postgres://auth_svc_postgres:auth_svc_postgres@auth-svc-db:5432/auth_svc"
depends_on:
duo-server:
condition: service_started
policy-svc:
condition: service_started
auth-svc-db:
condition: service_healthy
networks:
- duo-net

# ── Policy Service ──────────────────────────────────────────
policy-svc:
image: ghcr.io/silence-laboratories/two-party-boilerplate-backend/policy-svc:a988c47569b7e5efaf55ad3e40d64d1f775d953a
container_name: policy-svc
ports:
- "5002:5002"
environment:
DATABASE_URL: "postgres://policy_svc_postgres:policy_svc_postgres@auth-svc-db:5432/policy_svc"
LISTEN: "0.0.0.0:5002"
MAX_DB_CONNECTIONS: "10"
RUST_LOG: "info"
# Signing key for protected policy rows: 32 bytes as 64 hex chars
# (openssl rand -hex 32). The default is fine for local development; in
# production load it from your secret manager instead.
POLICY_SVC_HMAC_KID: "1"
POLICY_SVC_HMAC_KEY_HEX: ${POLICY_SVC_HMAC_KEY_HEX:-0000000000000000000000000000000000000000000000000000000000000000}
depends_on:
auth-svc-db:
condition: service_healthy
networks:
- duo-net

# ── Shared PostgreSQL (auth-svc + policy-svc) ───────────────
# One instance hosting both databases. It runs as the default `postgres`
# superuser and leaves the default `postgres` database untouched. The init
# scripts create a dedicated login role per service, each owning its own
# database:
# auth_svc_postgres -> auth_svc
# policy_svc_postgres -> policy_svc
# They run only on a fresh volume: after editing them, `docker compose down -v`
# before starting again or the changes are ignored.
auth-svc-db:
image: postgres:16-alpine
container_name: auth-svc-db
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- auth-svc-db-data:/var/lib/postgresql/data
- ./scripts/db/init-auth-db.sql:/docker-entrypoint-initdb.d/init-auth-db.sql:ro
- ./scripts/db/init-policy-db.sql:/docker-entrypoint-initdb.d/init-policy-db.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 5s
timeout: 5s
retries: 10
networks:
- duo-net

# ── Admin Dashboard ─────────────────────────────────────────
# Authors the group policies that policy-svc evaluates. Signing is denied
# until an admin saves a policy here, so this is part of the base stack
# rather than an add-on.
admin-dashboard:
image: ghcr.io/silence-laboratories/two-party-boilerplate-backend/admin-dashboard:a988c47569b7e5efaf55ad3e40d64d1f775d953a
container_name: admin-dashboard
ports:
- "3000:3000"
environment:
AUTH0_SECRET: ${AUTH0_SECRET:?set AUTH0_SECRET in .env (openssl rand -hex 32)}
# Must exactly match an Auth0 Allowed Callback URL for the application.
APP_BASE_URL: ${APP_BASE_URL:-http://localhost:3000}
AUTH0_DOMAIN: ${AUTH0_DOMAIN}
# The dashboard's own Auth0 Regular Web Application, not the mobile app's.
AUTH0_CLIENT_ID: ${ADMIN_DASHBOARD_AUTH0_CLIENT_ID:?set ADMIN_DASHBOARD_AUTH0_CLIENT_ID in .env}
AUTH0_CLIENT_SECRET: ${ADMIN_DASHBOARD_AUTH0_CLIENT_SECRET:?set ADMIN_DASHBOARD_AUTH0_CLIENT_SECRET in .env}
AUTH0_AUDIENCE: ${AUTH0_AUDIENCE}
# The service name, not localhost: inside this network localhost is the
# dashboard container itself.
AUTH_SVC_URL: "http://auth-svc:9090"
depends_on:
auth-svc:
condition: service_started
networks:
- duo-net

volumes:
duo-server-data:
auth-svc-db-data:

secrets:
duo-signing-master-key:
file: ${MESSAGE_SIGNING_KEY:-./testdata/party_0_sk}

Save the database init scripts:

auth-svc and policy-svc share one PostgreSQL instance but own separate databases. These two scripts run once, on the first boot of an empty data volume, and create a login role and database for each service.

scripts/db/init-auth-db.sql
CREATE ROLE auth_svc_postgres WITH LOGIN PASSWORD 'auth_svc_postgres';
CREATE DATABASE auth_svc OWNER auth_svc_postgres;
scripts/db/init-policy-db.sql
CREATE ROLE policy_svc_postgres WITH LOGIN PASSWORD 'policy_svc_postgres';
CREATE DATABASE policy_svc OWNER policy_svc_postgres;

They only run on a fresh volume. If you change them later, remove the volume (docker compose down -v) before starting again, or the changes are ignored.

Before launch, generate the signing key file:

mkdir -p ./testdata
openssl rand 32 > ./testdata/party_0_sk

2. Update the .env with your credentials

Update the .env and fill in your credentials:

# From Auth0 setup
AUTH0_DOMAIN= # your-tenant.auth0.com
AUTH0_AUDIENCE= # your API identifier

# The admin dashboard's own Auth0 Regular Web Application — also from the Auth0 page
AUTH0_SECRET= # openssl rand -hex 32
APP_BASE_URL=http://localhost:3000 # must match an Auth0 Allowed Callback URL
ADMIN_DASHBOARD_AUTH0_CLIENT_ID= # Regular Web Application client ID
ADMIN_DASHBOARD_AUTH0_CLIENT_SECRET= # its client secret

# FaceTec device key — use the development key for local testing
FACETEC_DEVICE_KEY=

# Helius — optional, for transaction history when using boilerplate application
HELIUS_API_KEY= # from dev.helius.xyz

# Django secret key — unsafe default is fine for dev
SECRET_KEY=djangosecretkey

# Policy service — 32 bytes as 64 hex characters: openssl rand -hex 32
POLICY_SVC_HMAC_KEY_HEX=

# Policy evaluation at signing time — set to false to sign without a policy
FEATURE_POLICY_EVALUATION=true

Compose refuses to start if AUTH0_DOMAIN, AUTH0_AUDIENCE, AUTH0_SECRET, ADMIN_DASHBOARD_AUTH0_CLIENT_ID or ADMIN_DASHBOARD_AUTH0_CLIENT_SECRET is missing, and names the one it needs. Everything else has a working default.

3. Run

docker compose -f docker-compose.all.published.yml up

This starts five services on an internal bridge network:

ServicePortPurpose
duo-server8080MPC signing node
auth-svc9090Authentication, hooks, and the admin API
policy-svc5002Stores and evaluates policies
admin-dashboard3000Web app for authoring policies
auth-svc-dbinternalPostgres, hosting the auth_svc and policy_svc databases

With policy-svc in the stack, FEATURE_POLICY_EVALUATION defaults to true and every signing request is checked against the policy of the group the user belongs to. The seeded default policy contains no rules, and a request that matches no rule is denied — so signing fails until an admin saves a policy.

The dashboard is already running at http://localhost:3000. Set that first policy before you try to sign: Setting the first policy.

To skip enforcement instead, set FEATURE_POLICY_EVALUATION=false in .env and restart — signing then goes through without a policy.

You will see logs from all five containers in the terminal:

duo-server  | WARN: The dotenv file is not set
duo-server | 2026-05-21T19:43:17.825909Z INFO sigpair_node: Creating SimpleStorage
duo-server | 2026-05-21T19:43:17.826633Z INFO sigpair_node: Party VK 01cfa1ff5424d14eb60614d7ddf65a32243d26ddf7000d10007853d7336395efe4
duo-server | 2026-05-21T19:43:17.826650Z INFO sigpair_node: Using HTTPTransport for auth hooks
duo-server | 2026-05-21T19:43:17.826983Z INFO auth_svc::config: DKGSetupValidatorHook: http://auth-svc:9090/hook/dkg_setup_validation
duo-server | 2026-05-21T19:43:17.827002Z INFO auth_svc::config: DSGSetupValidatorHook: http://auth-svc:9090/hook/dsg_setup_validation
duo-server | 2026-05-21T19:43:17.827010Z INFO auth_svc::config: KeyIdNotificationHook: http://auth-svc:9090/hook/key_id_notification
duo-server | 2026-05-21T19:43:17.827015Z INFO auth_svc::config: Initialized 3 auth hooks
duo-server | 2026-05-21T19:43:17.828547Z INFO sigpair_node: listening on 0.0.0.0:8080
auth-svc | Operations to perform:
auth-svc | Apply all migrations: admin, auth, contenttypes, sessions, src
auth-svc | Running migrations:
auth-svc | No migrations to apply.
auth-svc | 2026-05-21 19:43:20 [INFO] daphne.cli: Starting server at tcp:port=9090:interface=0.0.0.0
auth-svc | 2026-05-21 19:43:20 [INFO] daphne.server: HTTP/2 support not enabled (install the http2 and tls Twisted extras)
auth-svc | 2026-05-21 19:43:20 [INFO] daphne.server: Configuring endpoint tcp:port=9090:interface=0.0.0.0
auth-svc | 2026-05-21 19:43:20 [INFO] daphne.server: Listening on TCP address 0.0.0.0:9090
policy-svc | 2026-05-21T19:43:18.281368Z INFO refinery_core::traits::async: applying migration: V1__initial_schema
policy-svc | 2026-05-21T19:43:18.326436Z INFO policy_svc: listening on 0.0.0.0:5002
admin-dashboard | ▲ Next.js 16.2.7
admin-dashboard | - Local: http://a4ee0e226315:3000
admin-dashboard | ✓ Ready in 0ms

Two warnings on the very first start are expected and harmless: duo-server logs WARN: The dotenv file is not set, and auth-svc-db logs no usable system locales were found while Postgres initializes its data directory.

You will also find the Party VK <verifying-key> in the logs that you will use in your client side -- it's called CLOUD_NODE_VERIFYING_KEY by the phone client.


Notes for production

Django secret key

Replace the default djangosecretkey with a securely generated value:

echo "SECRET_KEY=$(openssl rand -base64 64 | tr -dc 'a-z0-9!@#$%^&*(-_=+')"

Policy service signing key

POLICY_SVC_HMAC_KEY_HEX authenticates every protected row in the policy database. The value you generated above is fine for local development. In production, load it from your secret manager and never commit it.

Database passwords

The init scripts and DATABASE_URL values above use the service name as the password. Change both together before deploying anywhere shared.

Duo-server exposure

Not all duo-server endpoints go through auth-svc hooks. Put duo-server behind a reverse proxy and only expose the endpoints that auth-svc authorizes. Block direct public access to the rest.

Admin dashboard exposure

The compose file publishes every service on localhost so you can reach them during setup. In production, only the dashboard and the auth-svc endpoints your clients call should be reachable, each behind TLS.

Next: Set the first policy -- signing is blocked until you do.