Menyimpan file biner di database relasional seperti PostgreSQL akan membuat tabel membengkak dan menurunkan performa query. Postingan ini menjelaskan bahwa hanya kunci GCS (path) yang disimpan di PostgreSQL, sementara file asli berada di Google Cloud Storage dengan struktur folder berdasarkan entity type, entity ID, jenis dokumen, versi, dan nama file. Akses dikendalikan melalui signed URL dengan TTL yang dapat dikonfigurasi — 1 jam untuk tampilan dalam aplikasi dan 24 jam untuk tautan unduhan via email — sehingga file tidak pernah dapat diakses publik meski path-nya diketahui.
Postingan ini menyebut versioning sebagai permintaan paling umum setelah go-live: pengguna sering mengunggah file yang salah dan perlu menggantinya tanpa kehilangan salinan sebelumnya. Sistem membuat baris versi baru untuk setiap penggantian dan menjadikan nomor versi tertinggi sebagai dokumen aktif, sambil mempertahankan semua versi lama. Sejak versioning diterapkan di Commsult, tim tidak pernah lagi harus melakukan pemulihan data akibat upload dokumen yang keliru.
File PDF mentah tidak dapat dicari secara langsung, sehingga sistem menjalankan Google Cloud Vision API untuk OCR pada setiap PDF atau gambar yang diunggah, lalu menyimpan teks yang diekstrak sebagai PostgreSQL tsvector di tabel document_search. Query menggunakan ts_query PostgreSQL yang mendukung teks Indonesia maupun Inggris. Postingan mencatat bahwa pencarian auditor atas semua faktur dari vendor tertentu dalam rentang tanggal tertentu menghasilkan dokumen yang relevan beserta cuplikan yang disorot dalam waktu kurang dari satu detik.
Regulasi Indonesia mengharuskan pelacakan sertifikat dengan tanggal kedaluwarsa yang ketat — sertifikat PKP, SIUP, lisensi impor API, sertifikasi SNI, sertifikat ISO, dan sertifikat kualifikasi staf. Sistem menyimpan field expires_at opsional per dokumen dan mengirimkan email peringatan otomatis pada 60, 30, dan 7 hari sebelum kedaluwarsa. Modul pengadaan juga memblokir pembuatan PO baru untuk vendor yang sertifikat PKP-nya sudah kedaluwarsa, menggantikan sistem pengingat kalender manual yang sering melewatkan tenggat.
Postingan memperingatkan bahwa perusahaan yang mengarsipkan faktur, kontrak, foto, dan laporan selama lima tahun dapat dengan mudah mengumpulkan 500 GB hingga 1 TB data, dengan biaya Google Cloud Storage di wilayah asia-southeast2 (Jakarta) sekitar $20 per bulan per terabyte ditambah biaya egress. Untuk mengendalikan biaya, dokumen yang berusia lebih dari satu tahun dipindahkan ke penyimpanan Coldline yang sekitar 10 kali lebih murah untuk arsip yang jarang diakses. Ukuran unggahan per dokumen dibatasi 50 MB dan per entitas dibatasi 500 MB untuk mencegah pengunggahan file video secara tidak sengaja.
Menurut Deloitte, 79% pemimpin bisnis melaporkan bahwa produktivitas tim mereka terhambat oleh ketidakselarasan antara sistem. Tidak ada yang lebih benar dari manajemen dokumen: faktur yang dikirim via email sebagai PDF, kontrak tersimpan di hard drive seseorang, gambar teknis tersebar di folder Google Drive. Ketika dokumen hidup di luar ERP, mereka tidak dapat ditautkan ke transaksi, tidak dapat memicu workflow, dan tidak dapat ditemukan dengan cepat selama audit.
Setiap dokumen dalam DMS ini memiliki: document_id, entity_type (vendor, produk, faktur, proyek, karyawan), entity_id (rekord ERP yang ditautkan), document_type (kontrak, faktur, sertifikat, gambar, foto), filename, storage_path (kunci Google Cloud Storage), file_size, mime_type, version, uploaded_by, uploaded_at, dan expires_at opsional.
Semua file dokumen disimpan di Google Cloud Storage, bukan di database. Saya hanya menyimpan kunci GCS (path) di PostgreSQL. File diorganisir berdasarkan struktur folder: '[entity_type]'/'[entity_id]'/'[document_type]'/'[version]'/'[filename]'. Akses melalui GCS signed URL dengan TTL yang dapat dikonfigurasi: 1 jam untuk tampilan dalam aplikasi, 24 jam untuk tautan unduhan yang dikirim via email.
ERP Document Management Architecture
ERP Entity (vendor, invoice, project, employee)
│ linked via entity_type + entity_id
▼
┌─────────────────────────────────────────────────────┐
│ documents table (PostgreSQL) │
│ │
│ id │ UUID │
│ entity_type │ 'vendor' | 'invoice' | 'project' │
│ entity_id │ UUID (FK to entity) │
│ document_type│ 'contract' | 'invoice' | 'cert' │
│ storage_path │ GCS key (not the file itself) │
│ version │ INT (increments on replacement) │
│ expires_at │ TIMESTAMPTZ (nullable) │
│ text_content │ tsvector (for full-text search) │
└──────────────────────┬──────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ GCS Storage │ │ OCR Extract │ │ Expiry Monitor │
│ (files) │ │ Vision API │ │ → alerts 60/30/ │
│ Signed URLs │ │ → tsvector │ │ 7 days before │
└──────────────┘ └──────────────┘ └──────────────────┘
Full-text search:
SELECT * FROM documents
WHERE text_content @@ to_tsquery('english', 'Sumber & Makmur')
AND uploaded_at BETWEEN '2025-01-01' AND '2025-03-31';Dari pengalaman saya membangun sistem ERP kustom untuk klien secara freelance: implementasikan versioning dokumen dari awal. Permintaan manajemen dokumen paling umum setelah go-live adalah 'Saya mengunggah versi kontrak yang salah — bisakah saya menggantinya tanpa kehilangan yang lama?' Jika Anda tidak memiliki versioning, jawabannya menyakitkan. Sistem saya membuat baris versi baru untuk setiap penggantian, mempertahankan semua versi sebelumnya. 'Versi aktif' adalah nomor versi tertinggi.
Dokumen harus memicu dan berpartisipasi dalam workflow. Ketika vendor mengunggah sertifikat NPWP melalui portal vendor, dokumen muncul di antrian tinjauan tim pengadaan ERP. Ketika faktur AP disetujui, PDF faktur yang dilampirkan secara otomatis diarsipkan ke folder dokumen vendor. Integrasi workflow ini mengubah DMS dari lemari arsip menjadi peserta aktif dalam proses bisnis.
File PDF mentah tidak dapat dicari kecuali Anda mengekstrak teksnya. Saya menggunakan Google Cloud Vision API untuk OCR pada PDF dan gambar yang diunggah: konten teks diekstrak dan disimpan dalam tabel document_search sebagai PostgreSQL tsvector. Pencarian full-text menggunakan ts_query PostgreSQL, yang menangani teks Indonesia dan Inggris.
// NestJS: Document upload with GCS storage + OCR
@Post('/documents/upload')
@UseInterceptors(FileInterceptor('file'))
async uploadDocument(
@UploadedFile() file: Express.Multer.File,
@Body() dto: UploadDocumentDto,
@GetUser() user: User,
) {
// 1. Determine storage path and version
const existing = await this.docRepo.findLatestVersion(
dto.entityType, dto.entityId, dto.documentType
);
const version = (existing?.version ?? 0) + 1;
const storagePath = [
dto.entityType, dto.entityId, dto.documentType,
`v${version}`, file.originalname
].join('/');
// 2. Upload to GCS
await this.storageService.upload(storagePath, file.buffer, file.mimetype);
// 3. Extract text via Google Cloud Vision (async)
const doc = await this.docRepo.save({
entityType: dto.entityType,
entityId: dto.entityId,
documentType: dto.documentType,
storagePath, version,
fileSize: file.size,
mimeType: file.mimetype,
uploadedBy: user.id,
expiresAt: dto.expiresAt,
});
// Trigger OCR extraction asynchronously
await this.ocrQueue.add('extract-text', { documentId: doc.id, storagePath });
return { documentId: doc.id, version, storagePath };
}
// OCR processor: updates tsvector for full-text search
@Process('extract-text')
async extractText(job: Job<{ documentId: string; storagePath: string }>) {
const imageBytes = await this.storageService.download(job.data.storagePath);
const [result] = await this.visionClient.textDetection({
image: { content: imageBytes },
});
const text = result.fullTextAnnotation?.text ?? '';
await this.docRepo.update(job.data.documentId, {
textContent: () => `to_tsvector('english', ${JSON.stringify(text)})`,
});
}Backend DMS adalah NestJS DocumentModule dengan StorageService (membungkus GCS), DocumentService (CRUD metadata), dan SearchService (full-text PostgreSQL). Frontend adalah penampil dokumen React menggunakan react-pdf untuk rendering PDF dan tampilan pohon folder yang dibangun dengan react-arborist. Dokumen terbuka di panel samping tanpa meninggalkan halaman ERP saat ini.
Biaya penyimpanan dokumen menumpuk lebih cepat dari yang diharapkan. Perusahaan yang menyimpan semua faktur, kontrak, foto, dan laporan selama 5 tahun dapat dengan mudah mengumpulkan 500GB-1TB file. Biaya Google Cloud Storage untuk 1TB di wilayah asia-southeast2 (Jakarta) sekitar $20/bulan untuk penyimpanan ditambah biaya egress untuk unduhan. Implementasikan kebijakan siklus hidup dokumen: pindahkan dokumen yang lebih dari 1 tahun ke penyimpanan Coldline (10x lebih murah untuk arsip yang jarang diakses).
Kepatuhan regulasi Indonesia mengharuskan pelacakan dokumen dengan tanggal kedaluwarsa: sertifikat PKP, SIUP, API (lisensi impor), sertifikasi SNI, sertifikat ISO, dan sertifikat kualifikasi staf. DMS ini mengirimkan email peringatan 60 hari, 30 hari, dan 7 hari sebelum dokumen kepatuhan kedaluwarsa. Modul pengadaan memeriksa validitas dokumen sebelum mengeluarkan PO kepada vendor.
Selama audit (audit pajak oleh DJP, atau audit kepatuhan yang diperlukan klien), auditor meminta dokumen tertentu. DMS ini mendukung: ekspor massal berdasarkan rentang tanggal, jenis entitas, dan jenis dokumen; audit trail yang menunjukkan siapa yang mengakses setiap dokumen dan kapan; dan tingkat akses auditor read-only yang memberikan akses tampilan saja ke set dokumen tertentu.