Overview

Three components and one module provide the client-side parts of the 21 CFR Part 11 requirements for electronic records: the signing dialog (smart-esignature), the signature manifestation on a signed record (smart-signature-block), the reviewer's view of the audit trail (smart-audit-trail), and the hash chain, timestamps and record hashes used by all three (Smart.Industrial.Audit).

None of them verifies a password, stores data or decides who may sign what. The application verifies credentials against its user directory, persists the entries, enforces authority and keeps the server-side copy of the trail. The Part 11 statement lists for each clause which side is responsible. This page describes how to use the components.

1. The audit trail

A Trail is a hash chain: every entry contains the hash of the previous entry and its own hash, so an entry that is edited, removed or reordered later breaks the chain from that point on, and verify() reports where. One change cannot be seen in the copy itself: entries removed from the end leave a shorter chain that still verifies. Keep the last hash (trail.head) and the entry count (trail.length) somewhere else, on the server for example, and pass them back as an anchor: trail.verify({ head, count }), Audit.verifyEntries(entries, genesis, { head, count }) and the viewer's expectedHead and expectedCount properties report a copy that no longer contains that entry, or holds fewer entries, as broken. Entries appended after the anchor was taken do not break it. Entries are stamped in ISO 8601 with the local UTC offset and millisecond precision, and an update keeps both the previous and the new value. An at passed in the entry replaces the stamp (a Date or a number is formatted the same way, a string is kept as it is), so a trail that must record the system's time should not pass one from elsewhere.

const Audit = Smart.Industrial.Audit;
const trail = new Audit.Trail({ system: 'HMI-3' });

const entry = trail.append({
    user: 'jdoe', userName: 'J. Doe', action: 'update',
    record: 'FIC-101', field: 'setpoint', oldValue: 78, newValue: 82,
    reason: 'Batch 0912 recipe step 4'
});
//entry: { seq, id, at, system, user, userName, action, record, field, oldValue, newValue,
//         reason, signature, previousHash, hash } - frozen; the fields not given are left out.
//An entry without a user or an action is refused with an error.

trail.verify();                //{ ok: true, count: 1, brokenAt: -1, reason: '' }
const anchor = { head: trail.head, count: trail.length };   //kept outside the trail, on the server
trail.verify(anchor);          //also fails when entries were cut off the end
trail.head;                    //the hash the next entry will link to; trail.length is the count
trail.subscribe(function (entry, trail) { /* persist it */ });
trail.toJSON();  trail.toCSV({ columns: ['at', 'user', 'action', 'record', 'field', 'oldValue', 'newValue'] });

//Reloading what the host persisted (the toJSON() form, as an object or as text). The entries
//are verified on the way in, and a copy that does not verify throws.
const restored = Audit.Trail.from(json, { system: 'HMI-3' });

The viewer takes a list of entries, or receives them as they are created:

<smart-audit-trail id="changes" label="Setpoint changes" show-integrity show-review max-rows="200"></smart-audit-trail>

<script>
    const view = document.getElementById('changes');

    view.entries = trail.entries();
    trail.subscribe(function (entry) { view.addEntry(entry); });

    view.addEventListener('integrityChange', function (event) {
        //An intact trail replaced by a copy that breaks, or the reverse.
    });
    view.addEventListener('reviewRequest', function (event) {
        //The 'Mark reviewed' button. Record the review, signed if it must be.
    });
    view.download('csv');   //the full trail, not the filtered view
</script>

Filters, sorting and the row limit affect the display only; the export contains the whole trail unless { filtered: true } is passed. The genesis property is the previousHash the first entry has to carry when a page shows the continuation of a chain that started elsewhere.

A JSON export can be verified as it is, with Audit.Trail.from() or Audit.verifyEntries(). A CSV export carries the same hashes, but each hash is computed over the entry's values with their types, so a CSV copy verifies only after every value has been converted back to its original type (a number to a number, a signature to its object). Keep the JSON form where a copy has to be checked later.

2. Signing

The e-signature panel shows the record being signed and its SHA-256 hash, asks for the meaning of the signature, the user ID and the password, and a reason where the procedure requires one. It then raises the signRequest event and waits. The password is passed in the event only, and the password field is cleared when the event is raised.

<smart-esignature id="sign" label="Sign the batch record" require-reason max-attempts="3" auto-close="1500"></smart-esignature>

<script>
    const sign = document.getElementById('sign');

    //The host verifies. This is the only place a password is seen.
    sign.addEventListener('signRequest', function (event) {
        const { userId, password, meaning, reason, recordHash } = event.detail;

        directory.verify(userId, password).then(function (user) {
            if (user && user.mayApprove) {
                sign.accept({ userName: user.displayName });
            }
            else {
                sign.reject();
            }
        });
    });

    sign.addEventListener('lockout', function (event) {
        //Refusals reached maxAttempts. Enforce the same on the server; reset() unlocks.
    });

    //Asking for a signature returns a promise of the signature.
    sign.sign({ id: 'B-0912', title: 'Batch record B-0912', fields: [{ label: 'Product', value: 'ASA 500 mg' }] },
        { meaning: 'approved' })
        .then(function (signature) {
            //{ id, userId, userName, meaning, meaningLabel, reason, at, recordId, recordHash }
            trail.append({ user: signature.userId, userName: signature.userName, action: 'sign',
                record: 'B-0912', field: 'release', newValue: signature.meaning, reason: signature.reason,
                signature: signature });
        }, function (error) {
            //error.reason: 'cancel', 'lockout' (maxAttempts refusals) or 'superseded' (a newer
            //sign() call). A single reject() keeps the panel open for another attempt.
        });
</script>
  • meanings: the meanings offered. When empty, the panel offers approved, reviewed, authored, responsible, verified and witnessed (§11.50). lockMeaning fixes the meaning.
  • continuousSession: a repeat signing by sessionUserId within sessionTimeout asks for the password only, as allowed by §11.200(a)(1)(ii). The first signing always requires both components.
  • maxAttempts: the number of rejections after which the panel is locked. The lockout event informs the application, which enforces the same limit on the server.
  • record: { id, title, fields: [{ label, value }] } or any object. The whole object is hashed into the signature.

3. The signature manifestation

A signed record shows the printed name of the signer, the date and time with the UTC offset and the meaning of the signature (§11.50), and the signature is linked to the record it signed (§11.70). The signature block displays this information and verifies the link:

<smart-signature-block id="release" show-reason show-hash></smart-signature-block>

<script>
    const block = document.getElementById('release');

    block.signature = signature;
    block.record = batchRecord;      //the record as it is now
    block.verified();                //false if the record changed under the signature, or the
                                     //signature was copied from another record
    block.text();                    //the manifestation as one line, for a printout or a log
</script>

The same checks are available without a component:

Audit.recordHash(record);                       //SHA-256 of the canonical JSON
Audit.verifySignature(signature, record);       //true when the hashes agree
Audit.manifest(signature);                      //"Approved by J. Doe, 2026-09-18 14:02:11 +02:00, Batch 0912"
Audit.stamp();  Audit.formatStamp(iso, { milliseconds: true });
Audit.verifyEntries(entries, genesis, expected); //{ ok, count, brokenAt, reason }
//expected is { head, count }, the newest hash and the entry count recorded somewhere the
//station cannot write. Without it, a trail with its newest entries cut off still verifies:
//every entry that is left is chained correctly. With it, a shorter trail fails with reason
//'count', and one that does not reach the recorded head fails with reason 'head'.

What the hash proves. The record hash and the chain hashes are plain SHA-256 without a key. A match shows that the record or the entry is the one that was hashed; it does not show who produced the hash, because anyone who can write the record store can also write a new record, a new signature object and a new hash that agree. Proof of authorship comes from where the signatures and the chain are kept: a server that the operator station cannot write to, or a server-side signature over the hash.

Responsibilities of the application

  • Identity: verifying the two components of a signature against a user directory and enforcing lockouts on the server.
  • Authority: who may sign which records and with which meaning (§11.10(g)).
  • Storage and retention: the entries, the signatures and the records, including the server-side copy of the chain.
  • Validation of the system as a whole. Using the components does not make an application compliant; the application is compliant when the system around them meets the remaining requirements.

The batch, settings and reports screens show the complete flow: a change is staged, signed, appended to a trail and displayed with its manifestation.