Open source · Apache 2.0 · self-hosted
Countersign keeps the master data and settings your regulated work depends on. You describe your record types; it makes sure no change applies until a second authorised person has approved it, keeps every version, and writes every step into a hash-chained trail that cannot be edited without it showing.
The demo needs no account and stores nothing outside your browser. Three example domains are built in.

What it does
A record is Created by its author, Confirmed by its author, and Approved by someone else who holds the approve right for that kind of record. The rule is enforced in one place, the engine, and the screen shows the reason whenever an action is not offered.
The workflow, step by step →
Editing an approved record never changes it. The edit becomes a new version that waits for approval while the old one keeps applying. Every version stays, with who wrote it, who confirmed it, who approved it and why it was needed.
Versions, step back, retire, remove →
Each action is one entry: who, when, what changed from what to what, why, and how the person was identified. Each entry carries the SHA-256 hash of the one before it, and the records themselves are nothing but the replay of those entries, so an altered, removed or forged entry is found by the server, by every browser and by a command-line check.
How the trail is built and verified →
Define an option once (a number with a range, a yes/no, a text, a choice list) and set it at any level of your hierarchy. For every record the screen shows the value in effect, the level and the record it came from, what it overrides and what is pending.
Hierarchy and inheritance →
Your domain, not ours
Record types, their fields, how they nest, which roles may write and approve what: all of it is a configuration document that the system checks, applies and records in its own audit trail. The demo runs the same code with three of them.



{
"id": "instrument", "label": "Instrument", "plural": "Instruments",
"prefix": "IN", "nameField": "tag",
"fields": [
{ "key": "tag", "label": "Tag", "type": "text", "required": true, "unique": true },
{ "key": "calibrationDue", "label": "Calibration due", "type": "date" },
{ "key": "classId", "label": "Class", "type": "ref", "ref": "class",
"role": "group", "required": true },
{ "key": "labId", "label": "Laboratory", "type": "ref", "ref": "lab",
"role": "parent", "required": true }
]
}From demo to production
Without any setting, Countersign is the demo: no database, no sign-in, each browser keeps its own changes. Two settings turn on the production path. Nothing else changes: the engine that checks every action is the same file in both.
| Demo | Production | |
|---|---|---|
| Where the trail lives | The visitor's browser (localStorage), on top of built-in example data | PostgreSQL (recommended, several servers), SQLite or an append-only file (one server). Your own adapter: one interface, one conformance test. |
| Who is acting | Picked from a list of example people | Microsoft Entra IDby default; any OpenID Connect provider (Keycloak, Okta, Auth0, ADFS); or an authenticating reverse proxy. Roles come from the provider's claims. |
| A signature | A click as the chosen person | An approval is accepted only with a sign-in from the last 15 minutes (configurable); the entry records method, issuer and time. |
| Protection of the trail | Shown, and deliberately breakable: the integrity page lets you tamper and watch it be caught | Database triggers refuse changes and broken chains; the server re-checks every entry and stops accepting changes on a fault; a recorded head hash exposes even a full rewrite. |
| Set by | Nothing | COUNTERSIGN_STORAGE, COUNTERSIGN_AUTH and their credentials |
git clone <this repository> countersign && cd countersign docker compose up # http://localhost:3000 (PostgreSQL + example data, demo sign-in)
COUNTERSIGN_STORAGE=postgres COUNTERSIGN_DATABASE_URL=postgres://countersign_app:…@db/countersign COUNTERSIGN_AUTH=entra COUNTERSIGN_ENTRA_TENANT_ID=… COUNTERSIGN_ENTRA_CLIENT_ID=… COUNTERSIGN_ENTRA_CLIENT_SECRET=… COUNTERSIGN_SESSION_SECRET=… # openssl rand -base64 48 COUNTERSIGN_PUBLIC_URL=https://records.example.com COUNTERSIGN_CONFIG=./my-domain.config.json
Coming from another system
A mapping file says which export file feeds which record type and how each column becomes a field. The import tool rehearses the whole run first, resolves references from old identifiers to new ones, and accounts for every source row: imported, already present, or refused with the reason.

For validation leads and auditors
15 user requirements, 115 functional requirements, and a traceability matrix that is generated from the test sources and fails the build when a requirement has no test. The pack is honest about the split: the software supplies mechanisms and evidence; validating your configured installation, your procedures and your infrastructure remains yours.
Resources