Self-host the witness
The SDKs sign and chain every record on your machine and send nothing until a person opts in. The witness is the part that co-signs records and timestamps them publicly. You can run it yourself, so records, receipts and stored content stay in your environment and verifiers trust your key, not ours.
Self-hosting is part of an Enterprise contract. The witness server is proprietary software, licensed with that contract; contact us for a quote. The steps below are for Enterprise customers who hold the server image or its source.
What does not depend on us. The protocol specification, the SDKs, the CLI, the MCP server and the verifiers (Rust, Python, TypeScript) are open source under Apache-2.0. A verification pack you export holds everything needed to check it, and verifies offline with the open verifier, without our service, for as long as you keep it. If you stop using signafAIde, your packs and your local .signafaide/ chains stay checkable.
Two parties. The guarantee that neither side can rewrite a record alone needs the witness to be run by someone other than the agents' owner. When you self-host, have a separate team or an independent operator run it.
Run signafAIde in your own cloud, with your own service key and your own anchoring. The service is one static binary in a small container image: the API, the web console, the website and the anchoring worker. Data is one SQLite file on a volume.
What changes when you host it:
- Your service key countersigns every batch. Its public key is published by your instance at
/.well-known/signafaide-keys.json; verifiers trust that key, not ours. - Your anchoring: you choose the OpenTimestamps calendars (the public pools, or your own calendar server), how often batches are anchored to a public timestamp, the explorer APIs that confirm a timestamp before an anchor shows as confirmed (
TIMESTAMP_CHECK_URLS), and the drand endpoints used for "not before". Anyone can check the public timestamps independently (see the Verify docs). - Your data: records, stored content and its encryption keys stay in your volume. Stored content is encrypted with a master key only you hold.
- No billing:
SIGNAFAIDE_FIXED_PLAN=enterprisegives every organization every feature.
Build the image #
The server compiles against the protocol core in the SDK repository, passed as a named build context. With both repositories side by side:
cd signafaide
docker build --build-context sdk=../signafaide-sdk -t signafaide:self-host .The build has three stages: Node builds the console and the website, Rust builds a static musl binary with them embedded, and the final image is scratch with the binary, CA roots and an empty /data, running as uid 10001. It listens on 7474 and has a health check (signafaide-server healthcheck).
Keys #
docker run --rm signafaide:self-host keygen # SIGNAFAIDE_SERVICE_KEY=... and its key id
head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=' # SIGNAFAIDE_CONTENT_MASTER_KEYKeep both in your secret store and back them up. Losing the service key means you start a new one (old receipts stay verifiable through the key history the instance publishes); losing the content master key makes stored content unreadable. To rotate the service key, set a new SIGNAFAIDE_SERVICE_KEY and restart: the old key is retired with an end time and stays published.
Run it #
deploy/self-host/docker-compose.yml runs the image with a named volume. Put a .env next to it (never commit it):
SIGNAFAIDE_PUBLIC_URL=https://signafaide.internal.example.com
SIGNAFAIDE_SERVICE_KEY=...
SIGNAFAIDE_CONTENT_MASTER_KEY=...
SMTP_URL=smtps://user:[email protected]:465
SMTP_FROM=signafAIde <[email protected]>cd deploy/self-host
docker compose up -d --build
docker compose logs -f signafaide # "signafAIde listening on 0.0.0.0:7474 (Prod mode, ...)"The port is bound to 127.0.0.1: put your TLS reverse proxy (or load balancer) in front, and set SIGNAFAIDE_TRUST_PROXY to the number of proxies in front (default 1). The client address is then the one that many hops from the right of X-Forwarded-For: entries further left are written by the client and are ignored, so they cannot dodge rate limits. Behind Cloudflare, set SIGNAFAIDE_TRUST_CLOUDFLARE=1 to use CF-Connecting-IP, and lock the origin to Cloudflare's IP ranges (anyone who reaches the origin directly can write that header), or set SIGNAFAIDE_CLOUDFLARE_IPS to Cloudflare's published ranges (comma-separated, e.g. 173.245.48.0/20,103.21.244.0/22,...,2400:cb00::/32) so the header counts only from them. Rate limits count an IPv6 client by its /64. With no proxy at all, set SIGNAFAIDE_TRUST_PROXY=0.
Prod mode needs SIGNAFAIDE_SERVICE_KEY and SIGNAFAIDE_CONTENT_MASTER_KEY; it refuses to start without them. Dev mode (the default when SIGNAFAIDE_ENV is unset) only starts on a loopback address, because it shows sign-in links in responses; the image sets SIGNAFAIDE_ENV=prod.
Alert webhooks and SSO issuers must be public https:// addresses: the server resolves each host and refuses loopback, private, link-local, CGNAT, unique-local IPv6 and cloud metadata addresses, and never follows redirects. Each webhook body is signed (X-Signafaide-Signature; the secret is on the console's Alerts page).
Sign-in #
People sign in to the console with email links (set SMTP_URL and SMTP_FROM), GitHub (GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET), or your identity provider: after the first sign-in, an owner sets up OpenID Connect under SSO and keys in the console, then proves the email domain with the DNS TXT record shown there (_signafaide.<domain>). SSO signs no one in before that, and an existing account signs in by SSO only once it is a member of that organization.
The staff admin (analytics, plans) is off in prod unless Cloudflare Access is configured (CF_ACCESS_TEAM_DOMAIN, CF_ACCESS_AUD); a self-hosted install does not need it.
Anchoring #
| variable | default | meaning |
|---|---|---|
OTS_CALENDARS | a/b.pool.opentimestamps.org, a.pool.eternitywall.com | calendars that receive each anchor root |
OTS_UPGRADE_CALENDARS | alice/bob/finney/catallaxy | calendars allowed when upgrading pending proofs |
ANCHOR_INTERVAL | 3600 | seconds between anchors (0 disables anchoring) |
ANCHOR_UPGRADE_INTERVAL | 900 | seconds between proof upgrades |
DRAND_URLS | api.drand.sh, drand.cloudflare.com | drand endpoints; empty disables "not before" |
TIMESTAMP_CHECK_URLS | mempool.space/api, blockstream.info/api | Esplora-style explorers that confirm a public timestamp before an anchor shows as confirmed; https:// only in prod (the server refuses to start otherwise), and their answers are never followed through a redirect |
To anchor through your own calendar, run an OpenTimestamps calendar server and list only it in both calendar variables. The instance needs outbound HTTPS to the calendars and drand; nothing needs to reach it except your clients.
Clients #
Point the SDKs and the CLI at your instance with SIGNAFAIDE_URL=https://signafaide.internal.example.com and a live API key from your console (SIGNAFAIDE_URL alone sends nothing: without a key the SDKs record locally only). For coding agents, npx signafaide agent setup --url https://signafaide.internal.example.com writes SIGNAFAIDE_URL into each agent's MCP server entry. Verification packs from your instance verify with the public verifier against your instance's keys:
signafaide verify pack.json --keys https://signafaide.internal.example.com/.well-known/signafaide-keys.jsonBackups and upgrades #
Back up the volume's signafaide.db (SQLite in WAL mode: use sqlite3 signafaide.db ".backup ..." or stop the container first) together with the two keys. To upgrade, rebuild the image from the new release and docker compose up -d; the database migrates itself on start.
Configuration reference #
Everything in the server README's configuration table applies. The ones that matter here: SIGNAFAIDE_PUBLIC_URL, SIGNAFAIDE_SERVICE_KEY, SIGNAFAIDE_CONTENT_MASTER_KEY, SIGNAFAIDE_FIXED_PLAN=enterprise, SIGNAFAIDE_TRUST_PROXY, SMTP_URL/SMTP_FROM, the anchoring variables above, and RUST_LOG. Leave the Stripe variables unset.