The Hungarian Accounting Act (Sztv. §169) requires that every accounting-relevant data change be preserved for eight years. An ERP audit trail is the engineering implementation of that obligation. We looked at where the legacy Netorigo ERP gets it right, and where it falls short — the latter became the design starting point for the modern modular stack.
The four mandatory dimensions
Every auditable event must carry:
- Who — user ID + role + login session ID.
- When — UTC timestamp, millisecond precision.
- What — entity (e.g.
invoice,customer,permission), operation (create,update,delete,read), entity ID. - Before-and-after — change diff: prior and new values of the touched fields.
The fourth dimension is missing or patchy in most systems. The legacy ERP handles the transaction journal (transaction_journal table) cleanly: every GL posting carries a before/after snapshot, queryable for eight years. This part is solid.
Where the legacy ERP slips
Master data changes. A user edits a customer's name, tax ID, or bank account. The legacy ERP logs the time, the user, and the new values — but not the old value. Two years ago an audit inquiry had us digging for two days for a tax-ID change that did happen, but we could not tell what it had been changed from. We patched in a customer_audit table after the fact, but not retroactively.
Permission changes. Who granted the accounting_admin role, when, and to whom? The legacy ERP records this only in the web-server access log, which rotates after 30 days. A standard audit question ("show us who could modify invoices in October 2024") is not answerable eight years back from the old log — answerable post-patch, but not for pre-2024 data.
Read events. No read logging anywhere. Not necessarily a problem — Sztv. only requires preservation of changes. But for a GDPR audit ("who looked at this customer's personal data?") we have no answer. The modern stack optionally logs every read, tenant-level toggle.
The modern stack's four principles
- Every mutation audited. No entity table may exist without a parallel
*_audittable. The migrations enforce this as a checked constraint. - Read auditing optional. Tenant-level toggle. Storage cost is significant — small tenants usually off, larger (50+) usually on.
- Append-only. No audit row can be deleted or updated through any API. SQL-level constraint (
DELETE/UPDATEtriggers raising exceptions), even anaudit_adminrole can onlySELECT. - Immutable storage tier after 5 years. See below.
The retention-vs-storage trade-off
Eight years of audit data is a serious storage bill. For a mid-sized ERP tenant (50 people, 1,000 mutations/day) that's 5–8 GB/year hot, 50–60 GB over eight years — and that's audit only, no business data. Keeping it on active SSD is expensive.
What we do: first 5 years on hot tier (PostgreSQL active table), final 3 years on cold tier (S3-compatible object store, Parquet format, partitioned by year). Querying older data is slower (5–30s queries become minutes), but since anyone touches it 0–2 times per year, this is an acceptable trade-off.
The hot → cold migration runs quarterly, automated, and migrated rows are hashed — integrity is verifiable at any time.
What to do about the legacy ERP's audit gaps
The honest answer: nothing retroactively. What the old system did not log, we cannot fabricate. From the migration moment onward the modern stack fully covers the requirement. For audit inquiries we educate tenants: through the migration date the old system gives gappy audit, after it complete.
Not a popular answer, but more honest than the alternative (generating fake audit data after the fact). Tenants accept it, auditors understand it, the rule is simple: start doing it properly today.