Retention and Immutability for Digital 8130-3 Records

A digital FAA Form 8130-3 authorized release certificate is only as trustworthy as the storage guarantees behind it: if the bytes can be silently edited or prematurely deleted, the release loses its evidentiary weight. This page covers how to enforce the retention windows and write-once immutability that § 43.9, § 43.11, and Part-145 recordkeeping demand for digital 8130-3 records. It extends the FAA Part-145 Recordkeeping Standards material with the specific storage-lifecycle controls a repair station must implement once the certificate leaves the parsing pipeline.

Prerequisites & inputs

The immutability layer receives a structured 8130-3 record whose fields have already been mapped from the form’s blocks — status of the article, part number, serial, quantity, work performed, and the authorized-signature reference in Block 14. That mapping is defined in How to Map FAA 8130-3 to Digital Schemas, and this stage treats its output as the authoritative payload to seal. What this stage adds is the retention clock, the write-once enforcement, and the legal-hold override.

The example record covers a released electrical component at ATA 24-31 (part 622-1876-004, serial SN-CX7741) installed on N207FE, released on 2026-07-16T09:41:00Z. Under Part-145 the associated record must be retained for a defined period after the work is performed; expressing that window as a computed expiry date, rather than a manual reminder, is the entire objective.

Retention lifecycle at a glance

WORM retention lifecycle8130-3recordWORMstoreRetentionclockLegalhold

Step-by-step implementation

1. Seal the payload on ingest. Compute a SHA-256 digest over the canonical serialization of the mapped record and store it alongside the bytes. Any later read recomputes the digest and compares; a mismatch means the store failed its one job.

2. Set the retention clock at seal time. Derive an expiry_date from the release date plus the applicable retention window. The clock must be immutable once set — a shorter window can never be back-applied to a sealed record.

3. Reject mutation attempts. The store exposes seal and read, never update or overwrite. An attempt to re-seal an existing key is an error, not an upsert. This is the write-once-read-many (WORM) contract that makes the record defensible.

4. Gate deletion on both expiry and legal hold. A record may be purged only when its retention window has elapsed and no legal hold is attached. A hold — for an incident investigation or litigation — freezes the record past its natural expiry until explicitly released.

from __future__ import annotations

import hashlib
import json
import logging
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import Any

logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
logger = logging.getLogger("mro.retention.worm")


class ImmutabilityError(RuntimeError):
    """Raised on any attempt to mutate or prematurely purge a sealed record."""


@dataclass
class SealedRecord:
    key: str
    payload: dict[str, Any]
    sha256: str
    sealed_utc: datetime
    expiry_utc: datetime
    legal_hold: bool = False


@dataclass
class WormStore:
    # FAA 14 CFR § 43.9 requires the maintenance record; Part-145
    # § 145.219 requires the certificated repair station to retain such
    # records for at least 2 years after the work is performed.
    retention_days: int = 730
    _records: dict[str, SealedRecord] = field(default_factory=dict)

    @staticmethod
    def _digest(payload: dict[str, Any]) -> str:
        canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
        return hashlib.sha256(canonical.encode("utf-8")).hexdigest()

    def seal(self, key: str, payload: dict[str, Any], released_utc: datetime) -> SealedRecord:
        if key in self._records:
            raise ImmutabilityError(f"record {key} already sealed; overwrite forbidden")
        record = SealedRecord(
            key=key,
            payload=payload,
            sha256=self._digest(payload),
            sealed_utc=datetime.now(timezone.utc),
            expiry_utc=released_utc + timedelta(days=self.retention_days),
        )
        self._records[key] = record
        logger.info("sealed %s digest=%s expiry=%s", key, record.sha256[:12], record.expiry_utc.date())
        return record

    def read(self, key: str) -> dict[str, Any]:
        record = self._records[key]
        if self._digest(record.payload) != record.sha256:
            raise ImmutabilityError(f"integrity failure on {key}: digest mismatch")
        return record.payload

    def apply_legal_hold(self, key: str, hold: bool) -> None:
        self._records[key].legal_hold = hold
        logger.warning("legal_hold=%s applied to %s", hold, key)

    def purge(self, key: str, now: datetime) -> None:
        record = self._records[key]
        if record.legal_hold:
            raise ImmutabilityError(f"{key} under legal hold; purge blocked")
        if now < record.expiry_utc:
            raise ImmutabilityError(f"{key} within retention window until {record.expiry_utc.date()}")
        del self._records[key]
        logger.info("purged %s after retention expiry", key)

Worked example

Seal the N207FE release: store.seal("8130-N207FE-24-31-0001", payload, released_utc=datetime(2026,7,16,9,41,tzinfo=timezone.utc)). With a 730-day window the store logs an expiry of 2028-07-15 and a digest such as 4b8e91c0d3a2…. A subsequent read recomputes the digest and returns the payload unchanged; if a storage-layer bit had flipped, read would raise ImmutabilityError instead of quietly returning corrupted data.

Now attempt the failure paths. Calling seal again on the same key raises overwrite forbidden — the WORM contract holds. Calling purge in 2027 raises within retention window until 2028-07-15. Attach a legal hold, advance past expiry to 2029, and purge still raises under legal hold; purge blocked until the hold is lifted. Only with the window elapsed and no hold does purge succeed. Every one of these refusals is a control an auditor can exercise on demand.

Edge cases & compliance gotchas

Canonicalization drift breaks the digest. If the serializer’s key ordering or whitespace changes between seal and read, the recomputed digest diverges and a valid record looks corrupt. Pin the serialization (sort_keys=True, fixed separators) and treat it as part of the record format, versioned explicitly.

Retention start date, not seal date. The clock must run from when the work was performed and released, not from when the record happened to be ingested. A record digitized months late would otherwise carry an inflated expiry and be purgeable too soon.

Unicode in Block 14 signatures. Authorized-signature and remarks fields carrying names like Łukasz or Renée must be stored as UTF-8; a lossy re-encoding changes the sealed bytes and trips the integrity check on the next read.

Clock skew on the retention boundary. Comparing a naive local now against a UTC expiry_utc can purge a record hours early or hold it late across a daylight-saving transition. Every timestamp in the store is timezone-aware UTC for exactly this reason; a purge decision that hinges on wall-clock time in a technician’s locale is a defect, not a convenience.

Immutability is not the same as durability. WORM stops mutation but not media loss. Pairing this control with an append-only, replicated ledger — the approach in Building an Append-Only Hash-Chained Parts Ledger — chains each seal to the last so a deletion anywhere in the history is detectable, not just prevented at the single-record level.

Retention and immutability are what let a repair station hand a regulator a digital 8130-3 years later and assert, with cryptographic backing, that nothing changed since release. Compute the expiry, forbid the overwrite, honor the hold, and verify the digest on every read — and the certificate keeps the legal weight the paper form always carried.