Configuration
Components, prerequisites, environment variables, and topology for running the self-hosted io.sign backend
This page covers how to configure and run the io.sign backend on your own infrastructure. The backend is the signing service that coordinates approvals and an eligible signer set to produce a signature. It does not broadcast transactions: io.sign returns a raw signature, which you broadcast yourself or hand to io.vault to broadcast automatically. Your Mobile Signers and Virtual Signers connect to the backend to take part in signing; the backend never holds a key share. This page documents the self-hosted backend option from Deploying io.sign, which also covers the io.finnet-managed alternative and where your Virtual Signers run. For how a signature is produced, see How io.sign Signs Transactions.
Release scope: this reference describes the io.sign launch configuration. Pin the API and migrations images to compatible versions supplied for your environment. The documentation version
v2.0is not a container tag.
Service components
The signing service is a single Node process that hosts four components. You toggle each one with an ENABLE_* environment variable, so you can run them together or run a subset.
| Component | Protocol and port | Requires |
|---|---|---|
| REST API (oRPC) | HTTP/1.1 on port 3000 | Postgres |
| gRPC API (Connect RPC) | HTTP/2 on port 50051 | Postgres and Redis |
| Cron tasks | In-process scheduler, no port | Postgres |
| Orchestrator | Background coordinator, no port | Postgres and Redis |
All four components share Postgres. The orchestrator and the gRPC signing streams also need Redis, which they use for pub/sub and distributed locks. Whenever the orchestrator runs anywhere in your topology, every other component must set ENABLE_REDIS=true so they can publish and consume events on the shared bus.
The web console (apps/frontend) is a separate static single-page app served by nginx. It ships as its own image and is an optional, alpha deployment documented in Web console.
The webhooks service also runs separately. Set its signing root key and rotation variables on that service, following Webhook signing configuration.
Prerequisites
To run the service on your own infrastructure, provide the following:
- A PostgreSQL 17 instance reachable over TCP.
- A Redis 7 instance reachable over TCP.
- A license key issued by io.finnet - it will deploy without the license key in read only mode.
- A container runtime such as Docker or Kubernetes.
- Access to the registry where io.finnet publishes your release images. Obtain the registry, image names, compatible version tags, and pull credentials from io.finnet.
- TLS certificates, only if you terminate gRPC TLS in the process rather than at a load balancer.
Handle secrets through a secret manager
LICENSE_KEY, database and Redis passwords, andJWT_PROVIDERSare sensitive. Inject them through your secret manager (Kubernetes Secrets, Docker secrets, HashiCorp Vault, or a KMS-backed store). Do not commit a populated.envfile to source control.
Configuration reference
Create a .env file or inject these as container environment variables. The values below are a minimum production-style set.
Runtime and components
NODE_ENV=production
STAGE=prod
ENV=prod
# Toggle the components this deployment runs (see the topology table)
ENABLE_ORPC=true
ENABLE_GRPC=true
ENABLE_GRPC_STREAM=true
ENABLE_CRON_TASKS=false
ENABLE_ORCHESTRATOR=true
ENABLE_REDIS=true
# Ports
API_PORT=3000
GRPC_PORT=50051ENABLE_GRPC_STREAM=true enables the bidirectional signing stream handlers. Set it to false to expose only the unary gRPC endpoints.
Postgres
The service connects to a writer and, optionally, a read replica. Omit DB_READER_HOST to disable the reader.
DB_HOST=postgres.internal
DB_PORT=5432
DB_NAME=io_sign
DB_USER=io_sign
DB_PASSWORD=<secret>
DB_SSL_ENABLED=true
# Reader replica (optional)
DB_READER_HOST=postgres-ro.internal
DB_READER_PORT=5432
DB_READER_NAME=io_sign
DB_READER_USER=io_sign
DB_READER_PASSWORD=<secret>
DB_READER_SSL_ENABLED=trueRedis
The password is optional. Omit it if you restrict access at the network layer.
REDIS_HOST=redis.internal
REDIS_PORT=6379
# REDIS_PASSWORD=<secret>License
LICENSE_KEY=<base64-license-issued-by-iofinnet>If the license key is missing, expired, or signed with a rotated key, the service starts in read-only mode. See Troubleshooting.
Authentication
Configure your identity provider, token validation, organisation claims, and client sign-in using Authentication.
Logging
Configure log output, distributed tracing, and metrics using Logging and observability.
Optional: gRPC TLS in the process
Set these only if you terminate TLS in the process rather than at a load balancer.
GRPC_TLS_CERT=/etc/io-sign/tls/cert.pem
GRPC_TLS_KEY=/etc/io-sign/tls/key.pemPublic app configuration for Mobile Signers
The unauthenticated GET /v2/app/config endpoint serves these values so client apps can discover the environment and bootstrap authentication before a user logs in. Any that are unset are omitted from the response. Each falls back to a system-wide row in the settings table when the environment variable is not set.
APP_CONFIG_ENV_KEY=acme-sign-prod
APP_CONFIG_LABEL="Acme signing"
APP_CONFIG_IO_SIGN_API_URL=https://sign.example.com
# Optional: only if your environment has Treasury
# APP_CONFIG_TREASURY_API_URL=https://treasury.example.com
APP_CONFIG_AUTH_STRATEGY=clerk
APP_CONFIG_AUTH_PUBLISHABLE_KEY=pk_live_...The Mobile Signer requires envKey, label, ioSignApiUrl, and a complete auth object. Optional on the backend does not mean optional for mobile provisioning. Choose a stable, organisation-specific key and label; the app rejects names reserved for built-in environments, including production.
For OpenID Connect (OIDC), replace the Clerk authentication variables with:
APP_CONFIG_AUTH_STRATEGY=oidc
APP_CONFIG_AUTH_ISSUER_URL=https://identity.example.com
APP_CONFIG_AUTH_CLIENT_ID=<public-mobile-client-id>
APP_CONFIG_AUTH_ORGANISATION_ID=<organisation-id>The mobile OIDC client uses Proof Key for Code Exchange (PKCE). Configure the identity provider for the mobile release's redirect URI and public-client flow; never publish a client secret. The API's JWT_PROVIDERS still controls which tokens the backend accepts. These app variables configure sign-in discovery, not token validation.
After changing environment variables, recreate or redeploy the API container. Confirm the response before sharing the URL:
curl --fail-with-body https://sign.example.com/v2/app/configCheck that the response contains your environment key, label, reachable API base URL, and the authentication fields above. Supply the full configuration URL, including /v2/app/config, to users following Connect to a custom environment. A phone's localhost refers to the phone itself, not your backend host.
This endpoint is publicOnly set values here that are safe to expose without authentication: public API URLs, the auth strategy, and publishable client keys such as Clerk publishable keys, which are designed to be public. Never put secrets in these variables.
Signer messaging
A complete signing deployment also needs an MQTT broker with authentication and TLS configured for your environment. Redis is the backend event bus; it does not replace signer messaging.
SETTINGS_DEFAULT_MQTT_BROKER_URL=wss://mqtt.example.com:8084/mqtt
# Set the broker certificate value if required by your deployment's trust configuration.
# SETTINGS_DEFAULT_MQTT_BROKER_CERTIFICATE=<certificate-value>These variables provide default settings; check the effective broker settings in an existing database before assuming a changed default overrides them. Configure each Virtual Signer's IOSIGN_SIGNER_MQTT_URL for the same environment. Mobile devices must reach the advertised broker, as well as the API and identity provider. See Virtual Signer configuration.
Deployment topology
The service is one process with four toggleable components. For an on-premise deployment, run each component as its own deployment against a shared Postgres and Redis.
| Deployment | Flags | Replicas |
|---|---|---|
| REST API | ENABLE_ORPC=true, ENABLE_REDIS=true | 2 or more |
| gRPC API | ENABLE_GRPC=true, ENABLE_GRPC_STREAM=true, ENABLE_REDIS=true | 2 or more |
| Orchestrator | ENABLE_ORCHESTRATOR=true, ENABLE_REDIS=true | 1 or more |
| Cron | ENABLE_CRON_TASKS=true, ENABLE_REDIS=true | 1 (do not scale) |
Set all other ENABLE_* flags to false on each deployment. Do not run more than one cron replica: it runs scheduled tasks such as expiring stale requests, and a second replica would duplicate that work.
Running the service
Step 1: Prepare the database
Follow Migrations and upgrades to create the database and run migrations once before starting the API.
Step 2: Start the service
io.finnet publishes an API image and a migrations image per release. After running migrations in Step 1, start the API with the compatible version tag from your registry:
docker run -d \
--name io-sign-api \
--env-file ./prod.env \
-p 3000:3000 -p 50051:50051 \
<registry>/<api-image>:<tag>Step 3: Verify
curl http://<host>:3000/v2/health/check
curl --http2-prior-knowledge http://<host>:50051/healthThe REST check returns 200 with a body like {"status":"healthy","database":"connected",...}. Check the body, not just the status code: an unmatched path (for example, omitting the /v2 prefix) falls through to the API playground, which also returns 200 but serves HTML. The gRPC check returns 200 with body {"status":"healthy","server":"grpc"}. The gRPC port is HTTP/2 only, so a bare curl without --http2-prior-knowledge cannot reach it.
Upgrades and rollback
Follow Migrations and upgrades for the upgrade sequence, rollback commands, and database backup requirements.
Observability
See Logging and observability for OpenTelemetry configuration, sampling, and export settings.
Troubleshooting
- Service starts in read-only mode. The startup logs show a line such as "License key not found", "Invalid license", or "License expired".
LICENSE_KEYis missing, expired, or signed with a rotated key. Contact io.finnet for a fresh key. - Redis connection failure. The logs show "Redis client error" with a Node error such as
ECONNREFUSED,ETIMEDOUT, orENOTFOUND. The orchestrator and gRPC streams need Redis. VerifyREDIS_HOSTandREDIS_PORTand network reachability, or disable those components. - All requests return 401. The logs show "Invalid token issuer" (
JWT_INVALID_ISSUER). The token'sissclaim does not match any entry inJWT_PROVIDERS. Decode the token and confirm theissmatches anissuervalue exactly, including the scheme and any trailing slash. - Migrations fail with permission errors. See Migrations troubleshooting.
The configuration and deployment details on this page are drawn from the io-sign repository documentation. Content was rephrased for compliance with licensing restrictions.
Updated about 1 month ago