# Membangun Audit Trail dan Activity Log ERP: PostgreSQL, NestJS Interceptors, dan React Activity Feed

> Cara mengimplementasikan sistem logging audit yang komprehensif untuk ERP kustom — mencakup desain skema PostgreSQL, logging berbasis NestJS interceptor, snapshot JSONB sebelum/sesudah, dan komponen activity feed React.

URL: https://www.matthewswong.com/id/blog/erp-audit-trail-activity-log/

- Author: Matthews Wong (https://www.matthewswong.com/id/about/)
- Published: 2025-06-10
- Updated: 2026-10-12
- Category: ERP

Audit trail menjawab pertanyaan terpenting dalam sistem bisnis mana pun: 'siapa yang mengubah ini, kapan, dan bagaimana tampilannya sebelumnya?' Ketika saya membangun ERP kustom sebagai konsultan ERP lepas, audit trail dirancang dari awal sebagai persyaratan kepatuhan dan kepercayaan.

## Apa yang Termasuk dalam Audit Log

Tidak semuanya perlu diaudit, tetapi apa pun yang mempengaruhi catatan keuangan, data pribadi, atau konfigurasi sistem mutlak perlu. Pada ERP kustom yang saya bangun, saya mengaudit semua operasi CREATE/UPDATE/DELETE, semua transisi state workflow, semua perubahan peran, dan semua operasi massal.

### Skema Tabel audit_logs

Tabel audit_logs perlu append-only. Tabel ini menyimpan: user_id, action (CREATE/UPDATE/DELETE), resource dan resource_id, old_data dan new_data sebagai JSONB, ip_address, dan created_at. Indeks parsial memungkinkan kueri riwayat per-entitas yang cepat.

### Memilih Antara Logging Tingkat Aplikasi dan Tingkat Database

Saya menggunakan logging tingkat aplikasi sebagai mekanisme utama karena menangkap konteks permintaan (identitas pengguna, alamat IP) yang tidak dapat diakses trigger database. Saya juga menambahkan trigger PostgreSQL ringan pada tabel paling sensitif sebagai jaring pengaman.

> Simpan snapshot JSON lengkap sebelum/sesudah dalam audit log daripada hanya daftar field yang berubah. Dengan snapshot JSONB lengkap, Anda dapat menelusuri kembali dan menampilkan dengan tepat seperti apa invoice pada tanggal tertentu — yang persis dibutuhkan auditor.

## Implementasi NestJS Audit Interceptor

Saya mengimplementasikan logging audit sebagai NestJS interceptor yang membungkus setiap panggilan API yang mengubah state. Interceptor dapat mengamati permintaan masuk dan respons keluar, menjadikannya ideal untuk menangkap state sebelum/sesudah.

### React Activity Feed untuk Pengguna Akhir

Data audit log diperlihatkan ke pengguna akhir melalui komponen activity feed React. Setiap halaman detail entitas memiliki tab 'Aktivitas' dengan riwayat perubahan lengkap, termasuk nama pengguna, tindakan, cap waktu relatif, dan diff untuk perubahan UPDATE.

## Kepatuhan dan Retensi Data

Pasal 28 ayat (11) UU KUP, sebagaimana diubah dengan UU 28/2007, mewajibkan buku, catatan, dan dokumen yang menjadi dasar pembukuan, termasuk hasil pengolahan data yang dikelola secara elektronik atau secara program aplikasi online, disimpan selama 10 tahun di Indonesia. Perlakukan catatan keuangan beserta riwayat perubahannya sebagai bagian dari kewajiban itu. Rancang strategi retensi sejak hari pertama: gunakan partisi tabel PostgreSQL per tahun, arsipkan partisi lama ke penyimpanan yang lebih murah setelah 2 tahun dengan tetap menyimpannya di Indonesia, dan hapus sebuah partisi hanya setelah umurnya lebih dari 10 tahun. Jangan simpan audit log di database yang sama dengan data aplikasi utama jika instance database Anda kecil.

> **Peringatan Immutabilitas**

> Jangan pernah mengizinkan peran aplikasi mana pun untuk UPDATE atau DELETE baris di tabel audit_logs. Berikan hanya INSERT dan SELECT ke pengguna database aplikasi. Jika perlu 'mengoreksi' entri, gunakan INSERT kompensasi — jangan pernah memodifikasi baris asli.

## Pertimbangan Performa

Menulis ke audit log menambahkan penulisan database sinkron ke setiap operasi API. Untuk operasi massal, gunakan antrian (BullMQ) untuk memproses penulisan audit log secara asinkron — tulis ke antrian segera (non-blocking), dan worker memproses di latar belakang.

## Audit Log sebagai Alat Debugging

Selain kepatuhan, audit log sangat berharga sebagai alat debugging. Ketika pengguna melaporkan 'jumlah invoice salah', buka audit log untuk invoice tersebut dan telusuri kembali setiap perubahan. Ini biasanya menyelesaikan perselisihan dalam hitungan menit.

## Sumber & Bacaan Lebih Lanjut

- PostgreSQL Docs — Trigger Procedures in PL/pgSQL — https://www.postgresql.org/docs/current/plpgsql-trigger.html
- NestJS Docs — Interceptors for Cross-Cutting Concerns — https://docs.nestjs.com/interceptors
- BPK — UU No. 27 Tahun 2022: Undang-Undang Pelindungan Data Pribadi — https://peraturan.bpk.go.id/Details/229798/uu-no-27-tahun-2022
- DJP — UU No. 28 Tahun 2007 tentang perubahan UU KUP (Pasal 28 ayat (11): retensi catatan 10 tahun) — https://www.pajak.go.id/id/undang-undang-nomor-28-tahun-2007

> **Key takeaway:** Audit trail ERP menjawab siapa yang mengubah sebuah record, kapan, dan seperti apa isinya sebelum itu. Tanpa audit trail, angka keuangan yang salah tidak bisa dibedakan antara salah ketik, bug, atau manipulasi yang disengaja. Menangkapnya di NestJS interceptor menjaga logging tetap keluar dari setiap service satu per satu.

## Source links

- https://www.postgresql.org/docs/current/plpgsql-trigger.html
- https://docs.nestjs.com/interceptors
- https://peraturan.bpk.go.id/Details/229798/uu-no-27-tahun-2022
- https://www.pajak.go.id/id/undang-undang-nomor-28-tahun-2007

## FAQ

### Mengapa menggunakan logging di tingkat aplikasi dibanding trigger PostgreSQL untuk audit trail?

Logging di tingkat aplikasi dapat menangkap konteks permintaan — seperti identitas pengguna yang terautentikasi dan alamat IP — yang tidak dapat diakses trigger database dengan mudah. Trigger PostgreSQL tetap digunakan sebagai jaring pengaman ringan pada tabel paling sensitif, tetapi NestJS interceptor menjadi mekanisme utama karena memiliki visibilitas penuh atas siapa yang membuat permintaan dan dari mana.

### Mengapa menyimpan snapshot JSONB lengkap sebelum dan sesudah perubahan, bukan sekadar daftar field yang berubah?

Snapshot JSONB lengkap memungkinkan rekonstruksi tampilan invoice atau data pada tanggal tertentu, yang persis dibutuhkan auditor dan investigator. Jika hanya diff field yang disimpan, tidak mungkin menelusuri kembali riwayat dan mereproduksi tampilan data pada titik waktu tertentu.

### Bagaimana operasi massal seperti impor Excel ditangani agar tidak memperlambat penulisan audit log?

Penulisan ke audit log menambahkan penulisan database sinkron ke setiap operasi API, yang tidak signifikan untuk operasi satu baris tetapi bisa menambah beberapa detik untuk impor 500 baris. Untuk operasi massal, penulisan audit log harus dimasukkan ke antrian via BullMQ agar non-blocking, dan worker di latar belakang yang memproses entri audit secara asinkron.

### Berapa lama audit log harus disimpan berdasarkan regulasi Indonesia?

Pasal 28 ayat (11) UU KUP, sebagaimana diubah dengan UU 28/2007, mewajibkan buku, catatan, dan dokumen dasarnya, termasuk data yang diolah secara elektronik, disimpan selama 10 tahun di Indonesia. Perlakukan catatan keuangan beserta riwayat perubahannya sebagai bagian dari kewajiban itu. Post ini merekomendasikan partisi tabel PostgreSQL per tahun, mengarsipkan partisi lama ke penyimpanan yang lebih murah di Indonesia setelah 2 tahun, dan hanya menghapus partisi yang berumur lebih dari 10 tahun.

### Mengapa tabel audit_logs harus append-only, dan bagaimana immutabilitas ditegakkan?

Immutabilitas adalah yang memberi audit log nilai hukum dan kepatuhan — jika baris dapat dimodifikasi atau dihapus, log tidak dapat dipercaya. Immutabilitas ditegakkan dengan hanya memberikan hak INSERT dan SELECT kepada pengguna database aplikasi, bukan UPDATE atau DELETE. Jika perlu koreksi, gunakan INSERT kompensasi yang mencatat koreksi tersebut, bukan memodifikasi baris asli.

---

- HTML version: https://www.matthewswong.com/id/blog/erp-audit-trail-activity-log/
- English: https://www.matthewswong.com/en/blog/erp-audit-trail-activity-log/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
