Open source · Apache 2.0 · self-hosted

Records that need a second signature, with an audit trail that proves itself.

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.

The approver's inbox: changes waiting for a second signature.
2 peoplefor every change to take effect; the same engine enforces it in the browser and on the server
0 updates, 0 deletesthe trail is append-only; PostgreSQL triggers refuse UPDATE, DELETE and TRUNCATE
115 requirements, all tracedeach one is tied to the automated test or walkthrough step that verifies it; the build fails otherwise
Any record typevehicles, homes, patients, instruments: a configuration document, not a code change

What it does

Four things, done strictly

Nothing takes effect on one person's say

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 →
The signature panel: old and new value, author, reason, the checks, and the signer's name, role and meaning.

The approved version stays in force until the next one is signed

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 →
Version history of a record: v1 superseded, v2 approved and effective, with authors, approvers and reasons.

An audit trail you can check, not just read

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 →
The integrity page: chain intact, head hash, and an explanation of the protection.

Settings that inherit, and say where they come from

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 →
Effective options of a vehicle: each value with its source level, the overridden value and a pending change.

Your domain, not ours

One application, any kind of record

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.

Vehicle fleet: effective options of one record.
Vehicle fleetRegion › Depot › Vehicle, grouped by vehicle class
Housing portfolio: effective options of one record.
Housing portfolioCity › Estate › Home, grouped by tenure
Hospital care settings: effective options of one record.
Hospital care settingsHospital › Ward › Patient, grouped by care pathway
{
  "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 }
  ]
}

What you configure

  • Record types with text, number, date, yes/no, selection and reference fields; required, unique, ranges and patterns.
  • The hierarchy: which reference is the parent, which is a grouping, and so the order in which options are inherited.
  • Roles: which record types and option levels each may write, which each may approve.
  • Changes to the configuration need a reason and a second person, and become entries in the trail like everything else.
Configuration reference →
The configuration page: record types, fields, inheritance chains and roles.

From demo to production

The same code, with your database and your identities plugged in

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.

DemoProduction
Where the trail livesThe visitor's browser (localStorage), on top of built-in example dataPostgreSQL (recommended, several servers), SQLite or an append-only file (one server). Your own adapter: one interface, one conformance test.
Who is actingPicked from a list of example peopleMicrosoft Entra IDby default; any OpenID Connect provider (Keycloak, Okta, Auth0, ADFS); or an authenticating reverse proxy. Roles come from the provider's claims.
A signatureA click as the chosen personAn approval is accepted only with a sign-in from the last 15 minutes (configurable); the entry records method, issuer and time.
Protection of the trailShown, and deliberately breakable: the integrity page lets you tamper and watch it be caughtDatabase 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 byNothingCOUNTERSIGN_STORAGE, COUNTERSIGN_AUTH and their credentials

Try it with PostgreSQL in two minutes

git clone <this repository> countersign && cd countersign
docker compose up
# http://localhost:3000  (PostgreSQL + example data, demo sign-in)

Production settings

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

Guides

Coming from another system

Migration with a paper trail of its own

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.

  • Records arrive as approved in the old system, marked with system, old identifier, batch and the approver recorded there; or as Confirmed, for your approvers to sign one by one.
  • The report lists source files with their SHA-256, the reconciliation per record type, every refused row, the audit entries written and a sign-off block.
  • Running it again imports only what is missing.
Migration guide →
A migrated record: its origin system, old identifier, batch and original approver.

For validation leads and auditors

The documents you would otherwise have to write

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.

What the mechanisms cover

  • Attributable, time-stamped, reasoned entries for every action; before and after values.
  • Electronic signatures with printed name, date and time, and stated meaning, bound to one version of one record by the hash chain.
  • Role-based authority checks on every action; nobody approves their own work.
  • Tamper evidence at three levels: database triggers, replay on every read, an externally recorded head hash.
  • Export of the trail and the records as CSV and JSON.

What it does not do for you

  • It is not certified or validated by anyone; no software can be, outside its installation and use.
  • A signature rests on a recent sign-in at your identity provider, not on a password typed into this application.
  • Everyone who is signed in can read every record and the whole trail; there is no per-record read restriction.
  • The server keeps the replayed state in memory: built for master data and settings, tens of thousands of entries, not for transaction volumes.
  • Backup, time synchronisation, account management and training are procedures you run; templates are in the pack.

Resources

All documentation

Use it

Set it up

Understand it

Project