Articles

11 min read

Astro Tanpa API: Baca Postgres Langsung Saat Build (dan Kapan Sebaiknya Tidak)

by , Fullstack Web Developer

Astro Content Layer bisa membaca Postgres saat build tanpa API. Kapan pola ini lebih baik dari Live Content Collections, dan kenapa peran database harus read-only. Data per Agustus 2026.

Diagram alur: Laravel menulis ke Postgres, Astro membaca saat build lewat peran read-only, hasilnya HTML statis di CDN.

TL;DR

  1. Ya, Astro bisa membaca Postgres langsung saat build tanpa API di antaranya, lewat mekanisme resmi bernama Content Loader API.
  2. Koleksi build-time mempertahankan dukungan MDX dan optimasi gambar; koleksi live tidak punya keduanya. Untuk konten yang berubah beberapa kali seminggu, build-time menang.
  3. Kredensial yang dipakai proses build harus peran database read-only. Di sistem ini INSERT, UPDATE, dan DELETE semuanya mengembalikan permission denied for table posts (SQLSTATE 42501).
  4. Sisi tulis dan sisi baca boleh aplikasi berbeda. Di sini admin panel Laravel yang menulis, Astro yang membaca, bertemu hanya di database.
  5. Jangan pakai pola ini untuk harga live, stok, personalisasi per pengunjung, atau redaksi yang mengedit sepanjang hari.
  6. Biayanya bisa Rp 0 per bulan di jalur gratis, dan build 6 halaman selesai dalam 3,53 detik.

Data per Agustus 2026.

Bisakah Astro membaca database langsung tanpa API?

Ya. Astro tidak butuh API di antara dirinya dan database. Dokumentasi resmi Astro menyebutnya eksplisit: “You can build a custom loader using the Content Loader API to fetch remote content from any data source, such as a CMS, a database, or an API endpoint.” (docs.astro.build, Content Collections).

Perhatikan urutan kalimatnya: a CMS, a database, or an API endpoint — database disebut sebagai sumber sejajar, bukan sebagai sesuatu yang harus dibungkus API dulu. Content Loader API adalah antarmuka resmi Astro untuk itu, dan sebuah loader hanyalah objek dengan fungsi load() yang mengisi store.

Per Agustus 2026, artinya kalau datamu sudah ada di Postgres dan kamu sedang menimbang membangun REST atau GraphQL layer hanya supaya Astro bisa membacanya, lapisan itu tidak dibutuhkan. Yang dibaca proses build adalah database, bukan API. Tidak ada HTTP, tidak ada serialisasi JSON, tidak ada endpoint yang harus diamankan, di-rate-limit, dan diversikan.

Yang perlu diputuskan bukan “bisa atau tidak”, melainkan kapan pola ini benar dan berapa harganya. Sisa artikel ini menjawab itu.

Kapan build-time lebih baik daripada Live Content Collections?

Build-time lebih baik ketika kontenmu berubah beberapa kali seminggu, bukan beberapa kali semenit. Astro punya dua jalur — koleksi build-time dan Live Content Collections — dan dokumentasinya jujur soal harga masing-masing.

Koleksi live membayar dengan dua fitur besar. Dokumentasi Astro menyebut “No MDX support: MDX cannot be rendered at runtime” dan “No image optimization: Images cannot be processed at runtime” (docs.astro.build, Content Collections). Sebaliknya, koleksi build-time disebut unggul saat “You want to benefit from build-time optimization and caching” — sumber yang sama.

AspekKoleksi build-timeKoleksi live
Kapan data diambilSekali, saat buildSetiap request
Dukungan MDXAdaTidak ada
Optimasi gambarAdaTidak ada
Kebutuhan runtimeTidak ada — output HTML statisServer atau adapter yang hidup terus
Data yang berubah antar-buildBasi sampai build berikutnyaSelalu segar
BiayaHosting statis, sering nolCompute per request + koneksi database

Kesimpulan yang keluar dari tabel itu: untuk artikel, portofolio, dokumentasi, dan halaman produk yang jarang berubah, koleksi build-time menang telak — pengunjung menerima HTML dari CDN dan database tidak pernah berada di jalur request. Untuk harga live, stok, dan dashboard, koleksi build-time salah, dan tidak ada trik yang membuatnya benar.

Satu jebakan yang jarang disebut: pilihan ini bukan cuma soal kesegaran data. Memilih koleksi live berarti melepas optimasi gambar Astro, dan di situs yang berat gambar itu adalah pertukaran performa yang biasanya lebih mahal daripada kesegaran yang didapat.

Bagaimana Content Layer loader membaca Postgres?

Content Layer loader membaca Postgres dengan menjalankan query di dalam load() lalu memanggil store.set() untuk setiap baris. Yang paling sering dilewatkan bukan query-nya, melainkan renderMarkdown() pada context loader.

renderMarkdown() didokumentasikan di Content Loader API reference sebagai fungsi yang “Renders a Markdown string to HTML, returning a RenderedContent object.” Fungsi itu menjalankan pipeline Markdown yang sama persis dengan loader glob() bawaan Astro. Konsekuensinya: body yang tersimpan di kolom database diperlakukan identik dengan body yang tersimpan di file .md — syntax highlighting Shiki, id heading otomatis, tanda kutip pintar, semuanya jalan.

for (const row of rows) {
  store.set({
    id: row.slug,
    data: { slug: row.slug, title: row.title, publishedAt: row.published_at },
    // Baris ini yang membuat render() dan <Content /> bekerja.
    rendered: await renderMarkdown(row.body),
  });
}

Tulis eksplisit konsekuensinya, karena ini keputusan arsitektur dan bukan detail teknis: menyimpan konten di file .md hanya membelikan riwayat git untuk prosa. Harganya adalah memelihara dua salinan yang sama-sama bisa ditulis — satu di repo, satu di database — dan pekerjaan menyinkronkan keduanya selamanya. Kalau riwayat git untuk prosa bukan sesuatu yang benar-benar kamu pakai, kolom text di Postgres adalah pilihan yang lebih murah.

Kodenya sendiri sekitar sepuluh baris dan bukan nilai jual pola ini. Yang mahal adalah dua keputusan berikutnya: peran database dan pemisahan tulis/baca.

Kenapa peran database harus read-only?

Karena proses build hanya perlu SELECT, dan memberinya lebih dari itu berarti kalau environment build bocor, penyerang bisa menulis. Ini bagian yang tidak dibahas satu pun tutorial “Astro + PostgreSQL” yang beredar per Agustus 2026: semuanya menyerahkan connection string ber-privilege penuh ke proses build dan berhenti di situ.

Peran read-only dibuat dengan lima perintah:

create role portofolio_readonly login password '...';
grant connect on database portofolio to portofolio_readonly;
grant usage on schema public to portofolio_readonly;
grant select on all tables in schema public to portofolio_readonly;
alter default privileges in schema public grant select on tables to portofolio_readonly;

Baris terakhir yang paling sering dilupakan: tanpa alter default privileges, tabel yang dibuat migrasi berikutnya tidak akan terlihat oleh peran itu, dan build akan gagal dengan permission denied pada tabel yang baru — beberapa minggu setelah kamu lupa kenapa.

Sekarang buktinya, dijalankan dengan kredensial build yang sama persis pada PostgreSQL 17.10:

current_user = portofolio_readonly
INSERT: ERROR: permission denied for table posts (code 42501)
UPDATE: ERROR: permission denied for table posts (code 42501)
DELETE: ERROR: permission denied for table posts (code 42501)
CREATE TABLE: ERROR: permission denied for schema public (code 42501)

Peran itu memegang SELECT pada 20 tabel dan nol grant selain SELECT. “Frontend hanya membaca” bukan konvensi yang dijaga code review; itu ditegakkan database.

Satu hal yang gagal, dan ini penting. grant select on all tables in schema public berarti semua tabel — termasuk users dan sessions. Kredensial build yang tidak bisa menulis apa pun tetap bisa membaca hash password dan payload sesi. Perintah yang nyaman itu terlalu longgar. Perbaikannya adalah mengganti baris keempat dengan grant per tabel pada tabel konten saja (posts, projects, tech, dan tabel pivotnya), dan menerima bahwa setiap tabel baru harus di-grant manual. Longgar dan aman itu dua hal berbeda; tutorial yang ada bahkan tidak sampai ke persoalan ini.

Bagaimana kalau sisi tulisnya aplikasi lain?

Tidak masalah — dan justru di situ pola ini paling kuat. Yang menulis dan yang membaca boleh dua aplikasi berbeda dengan kredensial berbeda, yang bertemu hanya di database. Di sistem ini sisi tulisnya adalah admin panel Laravel, sisi bacanya Astro.

  Admin panel Laravel                Astro (proses build)
  (kredensial penuh)                 (peran read-only)
          │                                   │
          │ INSERT / UPDATE                   │ SELECT
          ▼                                   ▼
     ┌──────────────────────────────────────────┐
     │              PostgreSQL                  │
     └──────────────────────────────────────────┘
                                                │
                                                ▼
                                     HTML statis  ──►  CDN  ──►  Pengunjung

  Tidak ada API di antaranya. Tidak ada database di jalur request.

Pemisahan itu yang jadi inti argumennya. Laravel memegang kredensial tulis dan tidak pernah dipakai membaca untuk publik; Astro memegang peran read-only dan tidak pernah bisa menulis. Keduanya tidak saling memanggil, tidak berbagi kode, dan tidak punya kontrak API yang harus diversikan. Kontraknya adalah skema database.

Untuk developer Laravel yang sudah punya panel admin dan sedang menimbang membangun API supaya frontend bisa membacanya: API itu tidak perlu ada. Kalau frontend-nya statis, jalur “Laravel menulis, Astro membaca saat build” menghapus satu lapisan penuh — beserta autentikasinya, rate limiting-nya, serialisasi-nya, dan versinya.

Kapan sebaiknya TIDAK pakai pola ini?

Pola ini kalah pada empat kondisi yang jelas, dan mengabaikannya akan terasa dua bulan kemudian, bukan hari ini.

Pertama, data yang berubah antar-build. Halaman menampilkan snapshot dari saat build terakhir. Kalau nilai berubah lima menit setelah build, halaman salah selama sisa hari itu.

Kedua, harga dan stok live. Harga basi bukan cuma tampilan yang kurang enak — itu janji yang tidak bisa kamu penuhi. Ini kasus yang benar untuk Live Content Collections, atau untuk sepotong data yang diambil di sisi klien.

Ketiga, konten yang diedit banyak orang sepanjang hari. Setiap penerbitan memicu rebuild. Redaksi delapan orang yang menerbitkan tiga puluh kali sehari akan menghabiskan kuota build gratis dan tetap merasa lambat, karena penulisnya menunggu pipeline, bukan menekan tombol.

Keempat, personalisasi per pengunjung. Halaman statis yang sama dikirim ke semua orang. Dashboard, harga khusus per akun, atau apa pun yang bergantung pada siapa yang sedang login tidak bisa di-build sebelumnya.

Dan satu kondisi di mana pola ini kalah meskipun kontennya jarang berubah: arsip yang sangat besar. Build 6 halaman di sistem ini selesai dalam 3,53 detik, tapi waktu build tumbuh linear terhadap jumlah halaman. Pada puluhan ribu halaman, rebuild penuh untuk satu typo berhenti masuk akal, dan build inkremental jadi masalah yang harus diselesaikan sendiri.

Berapa biayanya?

Nol rupiah per bulan, kalau kamu memilih jalur gratis yang tersedia per Agustus 2026 — dan itu bukan trial yang habis.

Ketiga komponennya punya tingkat gratis yang nyata:

KomponenPilihan gratisBatasnya
Postgres Supabase Free 500 MB database, 5 GB egress, maksimum 2 proyek aktif, proyek dijeda setelah 1 minggu tanpa aktivitas
Hosting statis Cloudflare Pages Free 500 build per bulan, 20.000 file per situs, 25 MiB per file, 100 custom domain
Domain DigitalPlat FreeDomain Subdomain gratis (.dpdns.org, .qzz.io, .us.kg, .xx.kg); proyek open-source AGPL-3.0 di bawah The Hack Foundation

Batas 500 build per bulan itu setara sekitar 16 penerbitan per hari — jauh di atas kebutuhan blog yang terbit beberapa kali seminggu, dan persis batas yang mulai menggigit pada redaksi di H2 sebelumnya.

Pembandingnya, pada volume yang sama: Sanity Growth $15 per seat per bulan, dan Contentful Lite $300 per bulan. Dengan kurs tengah Bank Indonesia 4 Agustus 2026, Rp 17.991 per dolar AS, itu Rp 269.865 dan Rp 5.397.300 per bulan. Supabase Pro, kalau tingkat gratisnya sudah tidak cukup, $25 per bulan atau Rp 449.775.

Jujurnya: Sanity dan Contentful juga punya tingkat gratis — Contentful Free memberi 100 ribu API call per bulan, Sanity Free memberi 10 ribu dokumen. Jadi perbandingan yang benar bukan “gratis lawan berbayar”, melainkan di mana langit-langitnya dan siapa yang memegang datanya. Titik di mana perbandingan berbalik ada dua: saat kamu butuh alur kerja redaksi sungguhan (peran, persetujuan, penjadwalan, riwayat perubahan), CMS berbayar lebih murah daripada membangunnya sendiri; dan saat database melewati batas tingkat gratis, biaya Postgres mulai berjalan sementara CMS masih gratis. Di bawah kedua titik itu, pola ini benar-benar Rp 0 dan datanya ada di databasemu sendiri.

FAQ

Apakah Astro butuh API untuk baca database?

Tidak. Content Loader API memungkinkan loader kustom mengambil konten dari database secara langsung saat build. Dokumentasi Astro menyebut database sebagai sumber data yang sejajar dengan CMS dan endpoint API. Tidak ada lapisan HTTP di antaranya, dan tidak ada endpoint yang perlu diamankan.

Apakah harus rebuild setiap kali data berubah?

Ya, untuk koleksi build-time. Halaman yang dihasilkan adalah snapshot dari saat build. Umumnya rebuild dipicu webhook dari sisi tulis, atau jadwal cron. Kalau kamu butuh data segar setiap request tanpa rebuild, itu kasus untuk Live Content Collections, bukan untuk pola ini.

Bisakah pakai MySQL, SQLite, atau MongoDB?

Bisa. Content Loader API tidak peduli database apa yang kamu pakai — loader hanya perlu mengembalikan baris dan memanggil store.set(). Yang berubah cuma driver dan sintaks query. Peran read-only juga tersedia di MySQL (grant select) dan MongoDB (role read).

Apakah aman menaruh kredensial database di CI?

Aman jika, dan hanya jika, kredensial itu peran read-only dan koneksinya memakai TLS. Simpan sebagai secret CI, jangan di repo. Kalau kredensial itu bisa INSERT atau DELETE, satu kebocoran log build sudah cukup untuk kehilangan data.

Bagaimana kalau build gagal karena database mati?

Build gagal, dan situs yang sudah live tetap berdiri. Ini justru keunggulan pola build-time: database tidak ada di jalur request, jadi database yang mati hanya menghalangi penerbitan berikutnya, bukan pengunjung. Deploy sebelumnya tetap dilayani CDN sampai build berikutnya berhasil.

Apakah body Markdown di kolom database diperlakukan sama dengan file .md?

Ya. renderMarkdown() pada context loader menjalankan pipeline Markdown yang sama dengan loader glob() bawaan Astro. Syntax highlighting, id heading, dan tanda kutip pintar bekerja identik. Menyimpan di file hanya membelikan riwayat git untuk prosa, dengan harga dua salinan yang harus disinkronkan.

Berapa lama build-nya kalau halamannya banyak?

Waktu build tumbuh linear terhadap jumlah halaman. Sistem ini membangun 6 halaman dalam 3,53 detik per Agustus 2026, jadi ordenya ratusan milidetik per halaman. Pada puluhan ribu halaman, rebuild penuh untuk satu perubahan kecil berhenti masuk akal dan kamu butuh strategi build inkremental.

Apakah peran read-only cukup untuk mengamankan build?

Peran read-only menghilangkan risiko tulis, bukan risiko baca. grant select on all tables juga memberi akses ke tabel users dan sessions. Batasi grant ke tabel konten saja kalau database yang sama menyimpan data sensitif; peran read-only adalah lantai keamanan, bukan langit-langitnya.

Sumber & tanggal pembaruan

Output permission denied dan waktu build pada artikel ini diambil dari sistem yang sedang berjalan, bukan dari contoh, dan diverifikasi ulang pada 6 Agustus 2026.

Terakhir diperbarui: 6 Agustus 2026.


Fikri AchmadFullstack Web Developer · Indonesia

I build internal web applications end to end, Laravel with Inertia and React on PostgreSQL. I own the deployment path: Ubuntu, Nginx, Docker, Cloudflare, and CI/CD. I use Python and Flask when a service is better kept separate, and IoT when the work touches hardware. Outside of work, I regularly build personal projects to sharpen my skills and experiment with new technologies.