# 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](mailto:sales@signafaide.co.uk). 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=enterprise` gives 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:

```sh
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

```sh
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_KEY
```

Keep 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):

```sh
SIGNAFAIDE_PUBLIC_URL=https://signafaide.internal.example.com
SIGNAFAIDE_SERVICE_KEY=...
SIGNAFAIDE_CONTENT_MASTER_KEY=...
SMTP_URL=smtps://user:password@smtp.example.com:465
SMTP_FROM=signafAIde <signafaide@example.com>
```

```sh
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:

```sh
signafaide verify pack.json --keys https://signafaide.internal.example.com/.well-known/signafaide-keys.json
```

## Backups 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.
