Trust & honesty
Production hardening
Signata validates its configuration at startup and refuses to boot in an unsafe production state. This page lists what production requires and why. Environment variables are referenced by their exact names.
Fail-fast configuration
Configuration is parsed and validated once at boot. In production the validator hard-fails rather than starting with insecure defaults. A missing secret or a SQLite DATABASE_URL aborts startup with a precise error instead of silently running in a demo posture.
Demo defaults are not for production
file: database URL and the dev placeholder secret both cause a boot-time failure.Required secrets
In production the following must be set to strong values, or the process refuses to start:
SIGNATA_SECRET: the root secret.SIGNATA_COOKIE_SECRET,SIGNATA_API_KEY_PEPPER, andSIGNATA_WEBHOOK_SECRET_PEPPERderive from it when not set independently; set them explicitly for separation.SIGNATA_ISSUER_PRIVATE_KEY_JWKandSIGNATA_ISSUER_PUBLIC_KEY_JWK: both required in production. The boot check enforces this so issuer keys never live in the database.
# Core: required and validated at boot in production.
SIGNATA_SECRET= # 32+ byte random; base for derived secrets
SIGNATA_APP_URL=https://app.signata.dev
DATABASE_URL=postgres://… # MUST be PostgreSQL in prod (no file: URLs)
REDIS_URL=redis://… # required for multi-instance correctness
# Issuer signing keys: loaded from env/KMS, never stored in the database.
SIGNATA_ISSUER_PRIVATE_KEY_JWK={"kty":"OKP",…}
SIGNATA_ISSUER_PUBLIC_KEY_JWK={"kty":"OKP",…}
SIGNATA_ISSUER_KID=key_2026_01
# Optional, but recommended to set explicitly in prod.
SIGNATA_COOKIE_SECRET= # defaults to SIGNATA_SECRET if unset
SIGNATA_API_KEY_PEPPER= # peppers stored API-key hashes
SIGNATA_WEBHOOK_SECRET_PEPPER= # peppers stored webhook secrets
SIGNATA_ALLOWED_ORIGINS=https://app.example.com,https://www.example.com
SIGNATA_BASE_URL= # public base URL if it differs from APP_URLIssuer keys from KMS, never the database
The keys Signata signs credentials with are loaded from the environment (sourced from a KMS or secret manager), not persisted in application storage. Keeping private key material out of the database means a database compromise cannot forge credentials. The matching public keys are published at the JWKS endpoint so anyone can verify Signata-issued signatures independently; rotate by issuing a new SIGNATA_ISSUER_KID and keeping the prior public key in the set until old credentials age out.
Postgres & Redis
Production uses PostgreSQL. The validator rejects file: URLs. Set REDIS_URL as well: rate-limit counters and idempotency records belong in a shared store so they are correct across instances. Without Redis the app falls back to the database and logs a warning, which is acceptable for a single instance but not for a horizontally scaled deployment.
Security headers, CORS & CSRF
Security headers are applied to responses, and browser-origin requests are checked against SIGNATA_ALLOWED_ORIGINS (a comma-separated allowlist) for CORS and CSRF protection. Set it to the exact origins that embed the badge or call the API from a browser; cookie-authenticated state-changing requests from other origins are rejected. API-key requests are bearer-style and not subject to the browser-origin check.
Rate limiting & idempotency
Endpoints are rate-limited per key, and POSTs honor an idempotency-key header so retries are safe and do not double-process. Both are backed by Redis in production for correctness across instances; a processed idempotency key is remembered for 24 hours.
Audit log & signed webhooks
Administrative actions (key issuance, trust changes, policy edits) are written to an append-only audit log. Webhook deliveries are signed with an HMAC over the payload in the x-signata-webhook-signature header; verify it on receipt (the signing secret is peppered with SIGNATA_WEBHOOK_SECRET_PEPPER) to reject forged or replayed deliveries.
Health, readiness & JWKS
Wire GET /api/health to your liveness probe and GET /api/ready (which checks the database, and Redis when configured) to your readiness gate, so traffic is only routed to instances whose dependencies are reachable. The JWKS endpoint is public.
Docker & Compose
A minimal production stack (the web image plus Postgres and Redis):
services:
web:
image: signata/web:latest
environment:
SIGNATA_SECRET: ${SIGNATA_SECRET}
DATABASE_URL: postgres://signata:${PG_PASSWORD}@db:5432/signata
REDIS_URL: redis://cache:6379
SIGNATA_ISSUER_PRIVATE_KEY_JWK: ${SIGNATA_ISSUER_PRIVATE_KEY_JWK}
SIGNATA_ISSUER_PUBLIC_KEY_JWK: ${SIGNATA_ISSUER_PUBLIC_KEY_JWK}
SIGNATA_ISSUER_KID: ${SIGNATA_ISSUER_KID}
SIGNATA_ALLOWED_ORIGINS: https://app.example.com
depends_on: [db, cache]
db:
image: postgres:16
cache:
image: redis:7