Dakwah-Lens
BriefingDiscussionsKitab LibraryFlyer gallery
Dakwah-LensEmpowering da'i with AI-driven media insights. A non-profit project from Sukses & Berkah Group.

Product

BriefingHow it WorksKitab LibraryDonate

More

About UsContactPrivacyTerms
© 2026 Sukses & Berkah GroupDakwah-Lens

For engineers, students, and the curious

Under the hood: how Dakwah-Lens is built.

A non-trivial AI stack delivered as a small, well-bounded monorepo. This page walks through every layer — services, models, data flow, and the trade-offs behind them.

Read the source on GitHubBack to home

TL;DR

Architecture

Monorepo — Next.js 16 frontend + FastAPI/Celery backend sharing one Postgres. Qdrant holds the kitab embeddings; Redis brokers Celery.

AI stack

Provider-agnostic. Gemini Pro for synthesis · Claude Sonnet 4.5 fallback · Gemini Flash-Lite for sentiment + relevance · Gemini for topic discovery.

Hard guarantee

Every Qur'an / hadith reference is retrieved from the corpus — never invented by the LLM.

Open-source under MIT.

The stack at a glance.

Five layers — UI, application, data, AI, infrastructure. Each box names the library + what it does so you can dive into the source by name.

Frontend

  • Next.js 16App Router, server components, server actions. Renders SSR for SEO and the marketing pages.
  • React 19Concurrent rendering enables the route-level loading.tsx fallbacks.
  • Tailwind v4Utility CSS, no design system framework — keeps the bundle thin.
  • next-intl v4i18n for `id` (default) + `en`. All copy lives in /messages/*.json.
  • Auth.js v5Email/password + Google OAuth. JWT session strategy with email verification gate.
  • Drizzle ORMTS-side ORM. Mirrors the SQLAlchemy schema for type-safe reads.
  • lucide-reactSingle icon set, kept consistent across the app.

Backend (Python 3.12, uv-managed)

  • FastAPIAsync API surface for the FE + scripts CLI entry points.
  • SQLAlchemy 2.0Async ORM. Models in api/src/api/models/.
  • AlembicSchema migrations — single source of truth.
  • Celery + RedisScheduled ingest, classification batches, topic clustering, system metrics.
  • uv (Astral)Fast deps + virtualenv via Astral's uv. Replaces poetry/pip.
  • Pydantic v2Settings + request/response models; configuration via env.
  • structlogStructured JSON logs.

Data

  • PostgreSQL 16Primary OLTP — users, social_posts, briefs, every observability table.
  • QdrantVector DB. Holds the kitab corpus embeddings (~6,236 ayah + hadith).
  • RedisCelery broker + result backend.

AI / ML

  • OpenAI Embeddingstext-embedding-3-large for embedding the kitab corpus (Qur'an, hadith, tafsir) and matching brief-topic queries against it via Qdrant.
  • Gemini 2.5 Pro / Flash-LitePro for brief synthesis + weekly briefings; Flash-Lite for sentiment + relevance + topic discovery + daleel reranking + comment moderation.
  • Anthropic Claude Sonnet 4.5Brief synthesis fallback when Gemini is unavailable.
  • trafilaturaArticle body extraction for RSS feeds with thin ledes.

Infrastructure & ops

  • Docker ComposeLocal dev: docker compose up brings Postgres + Qdrant + Redis.
  • IDCloudHost VPSHosted in Jakarta per UU PDP §17 data residency.
  • ResendTransactional email — verification + password reset.
  • ApifyX / Instagram / TikTok scraping actors.
  • YouTube Data API v3Direct YouTube Data API (cheaper than Apify YouTube actors).

System architecture.

Four tiers, each with one job. The Celery worker is a first-class client of the data layer — it speaks Postgres directly, like the web app does.

Clients

Browser

Next.js SSR + RSC

Celery workers

Celery beat + worker

CLI scripts

uv run python -m api.scripts.*

Application

Next.js Server Actions

Auth, briefs, admin

FastAPI

Health, future public API

Data layer

PostgreSQL

OLTP

Qdrant

Vector search

Redis

Queue + broker

External services

LLM providers

OpenAI · Gemini · Anthropic

Scrapers

Apify · YouTube API · RSS

Resend

Transactional email

The ingestion pipeline.

Five stages. Each beat tick fans out N keywords as independent child tasks — failure isolation per keyword, not just per platform.

  1. 01

    Scrape

    services/{apify,rss,youtube}.py

    Five platforms: X / Instagram / TikTok via Apify, YouTube via the Data API, mainstream RSS via feedparser + trafilatura for full-body extraction. All return a unified `ScrapeResult` shape.

  2. 02

    Normalize

    services/normalizers.py

    Per-platform normalizer maps raw payloads → SocialPost row dicts. Author handle extraction, URL truncation, posted_at parsing, region tagging for RSS.

  3. 03

    Classify

    services/sentiment.py · relevance.py

    Four parallel Gemini Flash-Lite passes per post — (1) event-valence sentiment unified across every platform (since IndoBERT was retired 2026-05-25), (2) 9-category da'wah relevance, (3) focused da'wah-opportunity score, (4) classification into one of 14 da'wah theme groups (Hukum & Keadilan, Aqidah & Ibadah, Konflik & Geopolitik, Toleransi & Lintas-Iman, …) written to the `theme_group` column on `social_posts` — the primary axis for the dashboard and weekly briefings. Combined cost: ~$0.0003 per post.

  4. 04

    Upsert

    scripts/ingest.py → social_posts

    Conflict on (platform, external_id) → update text + classifications + region. Re-running a scrape never produces duplicates.

  5. 05

    Topic discovery

    scripts/cluster_topics.py → topics

    Daily Gemini Flash-Lite topic discovery per platform, fired at 04:00 WIB by Celery beat. Reads every post from the last 7 days above a da'wah-opportunity floor (typically 300–600 mainstream posts) and asks Gemini to identify 6–10 themes with human-readable Bahasa Indonesia labels framed from a da'wah angle. Three post-LLM gates protect quality: (a) **elastic-word filter** — labels containing 'misterius/tragis/viral/polemik/kontroversi/heboh/geger' are auto-injected into the exclude list, after a 2026-06-02 audit showed a theme labelled 'Peristiwa Misterius & Horor' was vacuuming up 70% road-accident posts that don't belong; (b) **cosine merge at 0.78** — 3072-dim embeddings dedup near-duplicate themes (lowered from 0.85 when we switched embedding model); (c) **orphan-rescue** — 'Lainnya' posts whose `theme_group` matches a surviving theme and whose cosine to its centroid is ≥0.20 get re-labelled to that theme, so 'Lainnya' doesn't swallow posts that actually fit somewhere. Results land in `topics`.

Scheduled via Celery beat (Asia/Jakarta): mainstream RSS every 2 hours · X weekly every Wednesday 22:00 WIB (500-item cap/keyword, 7-day window) · TikTok weekly Wednesday 22:10 WIB and Instagram weekly Wednesday 22:20 WIB (limit 20 items/keyword, evaluation phase since 2026-05-25) · YouTube whitelist channel sweep weekly every Wednesday 21:00 WIB (Data API uploads path — trusted da'i voices) · daily trending overlay at 12:00 WIB: da'wah-relevant keywords scraped on X (apidojo) + unbounded YouTube search (Data API search.list, with a language gate) · Gemini topic discovery at 04:00 WIB daily (mainstream + YouTube) · X + TikTok + Instagram topic discovery every Thursday 04:00 WIB · insights briefings every Thursday 05:00 WIB — one day before the Friday khutbah (currently paused for cost, triggered manually via Claude) · email digest every Thursday 08:00 WIB · Apify billing reconcile at 06:00 WIB daily. Manual runs via `uv run python -m api.scripts.ingest`.

Keyword discovery: curated rotation + trending overlay.

Two complementary layers. The weekly curated sweep guarantees structural coverage across the 14 theme groups; the daily trending overlay catches today's named events the curated list could never anticipate.

Curated rotation

services/ingest_queries.py · workers/ingest.py::rotating_ingest

~49 keywords across major da'wah theme groups (religious, family, youth, muamalah, social justice, education, health, current events, cultural), admin-editable at /admin/system/queries. CURRENTLY ACTIVE: mainstream RSS (every 2h via feedparser, no keywords needed) and X (apidojo, $0.0004/item, weekly every Wednesday 22:00 WIB, 500-item cap/keyword + 7-day window). 1-WEEK EVALUATION PHASE since 2026-05-25: TikTok (clockworks/free-tiktok-scraper, $0.004/item, weekly Wednesday 22:10 WIB, limit 20 items/keyword) and Instagram (apify/instagram-hashtag-scraper, $0.0023/item, weekly Wednesday 22:20 WIB, limit 20 items/keyword) — cost and sentiment accuracy assessed after the first run. YOUTUBE (since 2026-05-27): verified-channel whitelist sweep weekly every Wednesday 21:00 WIB (free Data API uploads path — the "trusted da'i voices" half of the YouTube split); needs a one-time channel seed + admin verify. All code paths wired.

Trending overlay

services/trending_topics.py · workers/ingest.py::trending_ingest

LIVE since 2026-05-25 — re-enabled alongside the weekly X scraper. Daily 12:00 WIB, three free official sources merge: Google Trends Indonesia RSS, YouTube Data API mostPopular regionCode=ID, Google News Indonesia RSS. A single Gemini Flash-Lite call extracts a short search keyword from each candidate AND judges social-situation relevance (KEEP if it touches society/policy/family/youth/economy/etc., SKIP only purely-entertainment chatter) in one shot. The surviving keywords (up to 8) get ad-hoc scraped on X (apidojo) and searched on YouTube unbounded by channel (Data API search.list, with a language gate to block cross-language spam) — the "whatever is spiking" half of the YouTube split. Marginal cost ~$1/mo (YouTube free within quota).

Why two layers: curated alone leaves 7–25 day refresh gaps per keyword — fine for evergreen topics (riba, ibadah) but blind to a viral incident breaking that morning. Pure trending alone over-indexes on entertainment (K-pop fandom, sports). The hybrid covers both.

How we scrape each platform

The collection strategy differs per platform — tuned to cost, rate-limits, and signal quality. Here's the exact method for each.

Mainstream news (RSS)

NO keywords. We read the FULL RSS feeds of ~27 Indonesian outlets (Kompas, Detik, Antara, Republika, Tempo, CNN, etc.) every 2 hours, then extract full article text via feedparser + trafilatura. Free, no Apify — the primary, densest source.

X (Twitter)

KEYWORD-based. A pool of ~49 da'wah-relevant keywords (superadmin-editable) is scraped weekly every Wednesday 22:00 WIB via Apify (apidojo), with a 500-item cap/keyword and a 7-day window. Plus da'wah-relevant trending keywords from the daily 12:00 WIB overlay.

TikTok

KEYWORD-based. The same keyword pool is scraped weekly Wednesday 22:10 WIB via Apify (clockworks/free-tiktok-scraper), limit 20 items/keyword. In an evaluation phase since 2026-05-25.

Instagram

HASHTAG-based. The keyword pool maps to hashtags, scraped weekly Wednesday 22:20 WIB via Apify (instagram-hashtag-scraper), limit 20 items/keyword. In an evaluation phase.

YouTube — two strategies

(1) WEEKLY, CHANNEL-based: a whitelist of verified da'i channels has its latest uploads pulled every Wednesday 21:00 WIB via the YouTube Data API (cheap uploads path) — the "trusted da'i voices" layer. (2) DAILY, SEARCH-based: da'wah-relevant trending keywords are searched across ALL of YouTube (search.list, unbounded by channel) on the 12:00 WIB overlay, with a language gate to block cross-language spam — the "whatever is spiking" layer. Both are free within the Data API quota.

Mainstream RSS is the backbone (free, dense); paid-Apify social platforms run weekly on keywords/hashtags to keep cost down; YouTube uses the free Data API with two paths at once.

Why are post counts from Instagram & TikTok much lower than other platforms?

The main reason is cost. Mainstream RSS is free (we pull every 2 hours), YouTube Data API is also free, and X is relatively cheap (~$0.0004 per post). But Instagram (~$0.0023 per post via apify/instagram-hashtag-scraper) and TikTok (~$0.004 per post via clockworks/free-tiktok-scraper) charge per item, and our Apify budget is $29/month. That's why we cap items per keyword (IG 60, TT 20) and run weekly on Wednesday nights rather than daily like mainstream RSS.

So why scrape them at all if the volume is small? Because IG and TikTok give us a window into a different audience — especially Gen Z. The language patterns, viral issues, content formats, and emotional reactions on TikTok are very different from mainstream news or X (which skew older / news-reader). Even a small sample of ~600–1,500 posts per week is enough to capture signal: which da'wah categories are trending with Gen Z, what misconceptions are emerging, and what content formats work for younger audiences. Without scraping IG/TT, the da'wah briefing skews toward the news-reader audience (mostly 30+ years old).

Weekly insights briefings.

What the system (or operator) writes every Thursday for everyone — up to 14 briefings per cycle (one per theme group that crosses the volume floor) in Indonesian.

  1. 01

    Trigger

    workers/celery_app.py + scripts/manual_briefing.py

    Celery cron is wired for Thursday 05:00 WIB (1h after the 04:00 topic recluster, a day before the Friday khutbah) — but PAUSED since 2026-05-23 to keep LLM cost at zero during the dev phase. Operator currently runs manually via `manual_briefing.py dump <segment>` which runs the data-prep pipeline and emits the prompt — Claude's reply is saved back via `manual_briefing.py save`. Re-enable the cron by uncommenting the block in celery_app.py.

  2. 02

    Compute headline stats

    services/insights_summary.py::_compute_stats

    Seven-day aggregations from `social_posts`: sentiment mix, top categories with share-pct, top topics joined with sample headlines per topic. When no baseline week exists yet, delta_pp emits null instead of fabricating change. Per-segment briefings filter on dominant-cat above the 0.1 punt floor.

  3. 03

    Retrieve daleel

    services/kitab_retrieval.py + rerank_daleel (top_n=10)

    Flash-Lite builds a retrieval query from stats. OpenAI embeds it. Qdrant searches all 6 kitab collections (~4 per corpus → 15 candidates). Flash-Lite second pass reranks the pool thematically to the 10 strongest matches. Hadith translation_id is filled in where empty — Quran already has both ID + EN (Kemenag + Sahih International). 10-deep pool (was 5) so each Section-4 sub-section can weave 2-3 inline citations without repeating.

  4. 04

    Synthesize (Indonesian)

    Gemini 2.5 Pro (paused) → Claude manual

    Auto mode (cron enabled): Gemini 2.5 Pro called once with max_output 32,768 tokens + 12,288-token thinking budget. Current manual mode: paste prompt + context into Claude (chat), save reply via manual_briefing.py. Output is identical in both modes: 5-section markdown 7,300-9,800 words in Indonesian. Daleel-grounded — model can ONLY cite from the 10-daleel retrieved pool. English version paused since 2026-05-23 (fallback banner shown to EN readers).

  5. 05

    Persist + render

    briefings → /briefings/[id] + Puppeteer flyers

    Writes a row to `insights_summaries` (summary_md, headline_stats, daleel_refs, segment, model, tokens, cost). Public page /briefings/brief/{slug} renders with sticky TOC + share/download toolbar. Section 4 is split into 6 interactive deliverable cards (click to open modal). Flyers are rendered on-demand by Puppeteer + HTML/Tailwind — 2 variants (general + Gen-Z) per briefing plus 6 per-deliverable variants. CDN-cached 1h, immutable.

Anatomy of one briefing

What lands in the content box every week. One briefing = 5 markdown sections (Section 4 carries 8 separate deliverables) + a campus bulletin-board poster with QR + 6 share-ready flyers (incl. sunnah invitation + recitable du'a).

Markdown structure

  • 1~100-130 kata

    Executive summary

    One opening paragraph: top 3 categories with share-pct, sentiment mix, and the week's main themes.

  • 2~250-350 kata

    Numbers & trends

    Detailed headline stats: per-category volume + delta, sample headlines per topic, week-over-week sentiment shift.

  • 3~350-450 kata

    Main themes & patterns

    Thematic narrative that ties raw headlines into a pattern relevant to this week's da'wah.

  • 5~500-700 kata

    Daleel & sources

    Bibliography — 8-10 from the 10-deep retrieved pool. Format: bold citation + translation + 1-2 sentences of context. Ends with the AI-assisted disclaimer.

Section 4 — Content Kit

Da'wah Strategies & Actions — 8 ready-to-use deliverables in one briefing

5,400-7,800 kata
  • Full Friday khutbah

    ~2,300-3,200

    From mukadimah, opening verse, 6-9-paragraph body, closing du'a in Arabic transliteration — just adapt to your jamaah.

  • Women's kajian outline

    ~800-1,100

    45-minute format: 3 talking points + real news examples + anticipated Q&A + close.

  • Home conversation scripts

    ~500-700

    3-4 scenarios for parents with kids (SD/SMP/SMA): opening questions + anticipated responses + close.

  • Creator video scripts

    ~100-130

    60-90 seconds for TikTok/IG Reels/Shorts: 5-second hook → 40-60-second body with 1 daleel → CTA.

  • Mahasiswa campus pack

    ~800-1,100

    Bulletin-board poster (provocative question + QR) → magazine-style article page with realistic Q&A. Logic-first framing for students who push back on top-down sermons.

  • Age-segmented social action

    ~600-900

    4 concrete small-budget (<IDR 500K) actions for kids / teens / adults / elderly — runnable in parallel as one RT campaign.

Visual bonus

6 share-ready flyers + 1 campus poster, rendered on-demand

Puppeteer + HTML/Tailwind renders 1080×1080 PNG flyers from the briefing content that already exists. composeFlyer() picks layout + image from the registry deterministically per edition+slot — so share-URLs stay stable but the visual rotates each week.

  • General flyer — classical Islamic

    Ayat-centered layout with Amiri calligraphy, 8-point star ornaments, mosque / interior photo as backdrop. Fits mosque groups, adult kajian, personal stories.

  • Gen Z flyer — bolder & headline-led

    Punchy 64-78px headline as the focal element, violet→fuchsia→amber palette, ayat as a tilted supporting card. Designed for IG / TikTok feeds.

Plus 6 per-deliverable flyers (khutbah / kajian / home / content / genz / action) shown in the Section-4 card modals. Total 8 flyers × up to 14 briefings = up to 112 share-ready PNGs per cycle, all pre-rendered via Puppeteer for IG/print quality.

Same Sharia discipline as user briefs: the long-form prompt explicitly forbids freelance Quran/hadith citations. Every reference comes from the 10-daleel rerank pool — if the model tries to add more, it's a violation of the prompt rule. Disclaimer baked into Section 5 of every briefing: AI-assisted, not an authoritative fatwa.

How a brief gets generated.

The hard guarantee: every Qur'an + hadith reference is retrieved from the corpus by semantic search — never invented by the LLM. The LLM only frames the surrounding context.

  1. 01

    Capture

    app/[locale]/briefs/new

    User submits topic + segment + tone + locale + optional extra context. Onboarding profile attached server-side.

  2. 02

    Embed the topic

    lib/kitab-retrieval.ts

    OpenAI text-embedding-3-large turns the topic into a 3072-dim vector. Enriched with the audience segment label to bias retrieval toward audience-relevant verses, hadith, and tafsir passages.

  3. 03

    Retrieve daleel

    Qdrant: all kitab collections

    Cosine search across all kitab collections in parallel (~4 candidates per corpus, ~15 total). A second Gemini Flash-Lite pass thematically reranks the pool down to the 5 most relevant — cosine alone surfaces verses that share tokens with the query but miss the actual da'wah angle. If retrieval fails, the brief generator surfaces a typed error to the UI.

  4. 04

    Synthesize

    lib/llm.ts → generateJson()

    Gemini Pro (primary) gets the topic, profile, extra context, and retrieved daleel. Falls back to Claude Sonnet 4.5 if Gemini fails. Output is constrained to a JSON schema.

  5. 05

    Validate & store

    Zod validation → briefs table

    Zod-validates the response shape, attaches the retrieved daleel array, persists to `briefs`. Failures surface as a typed error to the form, never a half-baked row.

Hard rule per PRD §12: the system prompt explicitly forbids the LLM from inventing additional Qur'anic verses or hadith citations. The model only sees the daleel array we retrieved — if it tries to add more, validation catches it.

Models used, and why.

Each choice is the smallest model that gets the job done well, not the largest one available.

ModelRoleWhy this oneWhere it lives

Gemini 2.5 Flash-Lite

gemini-2.5-flash-lite

Unified classifier across all platforms since 2026-05-25 (replaces retired IndoBERT): (a) event-valence sentiment, (b) 9-category da'wah relevance (aqidah, akhlaq, muamalah, social_justice, family, youth, education, economic_ethics, health), (c) opportunity scoring, (d) **classification into 14 da'wah theme groups** written to the `theme_group` column (Hukum & Keadilan / Aqidah & Ibadah / Konflik & Geopolitik / Toleransi & Lintas-Iman / Sosial & Keluarga / Ekonomi & Bisnis / Pendidikan & SDM / Kesehatan & Kehidupan / Lingkungan & Bencana / Pemerintahan & Kebijakan / Patologi Sosial Digital / Teknologi & AI / Pekerja & Pertanian Rakyat / Inspirasi & Kisah Pribadi), (e) daily topic discovery with elastic-word filtering + cosine-merge 0.78 + orphan-rescue, (f) daleel reranking, (g) discussion comment moderation.Cheapest Gemini tier. Cost per post is ~$0.0003 across all three classifier passes — fits the IDR 1.5-2M/month operating-cost cap with comfortable headroom.Called from api/src/api/services/relevance.py (batches of 10), api/src/api/services/topic_discovery.py, and api/src/api/services/kitab_retrieval.py (rerank).

Gemini 2.5 Pro

gemini-2.5-pro

Weekly insights briefings (Thursday 05:00 WIB) — 5 audience segments in Indonesian, ~7,300-9,800-word long-form markdown per briefing, retrieval-grounded daleel. Cron is currently paused for cost; generation is triggered manually via Claude before publish. On-demand admin brief synthesis available as an experimental feature.Top-tier reasoning at ~35% lower cost than Claude Sonnet 4.5. Weekly theme-group briefings cost ~$0.06 per briefing-language (~$3.40/mo for 14 groups × 1 lang); on-demand user briefs route through /briefs/new and bill per request.Briefing path: api/src/api/services/briefings_summary.py. User-brief path: web/src/lib/llm.ts → callGemini().

Claude Sonnet 4.5

claude-sonnet-4-5

Brief synthesis fallback when Gemini errors out.Different cloud, similar quality bracket. Diversifies single-vendor outage risk.Fallback path in web/src/lib/llm.ts → callAnthropic().

OpenAI text-embedding-3-large

text-embedding-3-large

Topic-text → vector for Qur'an semantic retrieval.3072-dim embeddings give noticeably better retrieval than -3-small on classical Arabic translation drift.Called from web/src/lib/quran-retrieval.ts before each Qdrant query.

Estimated monthly operating cost

Real, current numbers — the platform deliberately runs on a tight budget. Total operating cost (server + API) is capped at IDR 1.5-2M (~$92-123) per month. Schedule and rates as of May 2026.

ProviderWhat it doesMonthlyNotes

Gemini 2.5 Pro (user briefs only)

Primary model for weekly executive briefings — currently invoked manually via scripts/manual_briefing.py (auto cron paused 2026-05-23).$0.30On-demand user briefs only · ~$0.07/brief · auto-cron paused

Gemini Flash-Lite

Relevance + news-sentiment + non-ID-fallback + topic discovery + trending filter.$17.40Per-post classifier + daily topic-discovery + daily trending fan-out + weekly social burst

OpenAI embeddings

Kitab corpus embeddings (Qur'an + 4 hadith books).$0.20Topic-recluster vectors + per-brief kitab query embeddings

YouTube Data API

Weekly whitelist channel sweep + daily trending search via Data API v3 (free 10K-unit daily quota).—Inside free daily quota

RSS (feedparser + trafilatura)

Mainstream news every 2 hours, no API cost.—Free

Anthropic Claude (fallback)

Brief synthesis fallback when Gemini Pro errors.—On-demand fallback, scales with usage

VPS · IDCloudHost (Jakarta)

Indonesian VPS: app hosting, Postgres, Redis, Qdrant Cloud (free tier).$18.00≈ Rp 300K/bulan · 2 vCPU · 4 GB RAM · 30 GB disk

Apify · X (apidojo, incl. late-bill)

Scrape X tweets via apidojo/tweet-scraper, weekly Wednesday 22:00 WIB (50 enabled keywords, 500-item cap/keyword, 7-day window).$20.21Weekly Wed 22:00 + daily trending overlay · ~$0.016/tracked-call · apidojo bills late, captured by daily reconcile

Apify · Instagram

Scrape Instagram hashtag posts, weekly Wednesday 22:20 WIB (37 enabled hashtags, limit 60 items/keyword as of 2026-05-30).$5.9737 kw · weekly Wed 22:20 · ~$0.046/item · real volume ~40% of projection

Apify · TikTok

Scrape TikTok videos, weekly Wednesday 22:10 WIB (28 enabled keywords after 2026-05-30 zero-yield prune, limit 20 items/keyword).$12.2828 kw (post-prune) · weekly Wed 22:10 · ~$0.063/call (highest per-call in stack)
Total estimated~$74.36≈ Rp 1.212K/bulan

Budget cap (PRD §13)

$74.36 / $123 · Rp 2.005K

Snapshot as of 2026-06-07. Apify authoritative source: $24.80 spent of the $29 Starter cap on day 20 of 31 (cycle May 19 → June 18) — at the $1.24/day burn rate this cycle will close around ~$38, roughly $9 above the plan cap. TikTok at $0.063/call (highest per-call in the stack) is the biggest next lever. Daily 06:00 billing-reconcile job catches late-billed events on X. Brief synthesis (Claude / Gemini Pro) is on-demand only — scales with user volume after launch.

The kitab corpus — 20 kitabs embedded in Qdrant.

v0 corpus per PRD §12 — 20 kitabs across 5 categories (Qur'an & tafsir, hadith, sirah & sahabat, fiqh, aqidah & adab). Embedded once, never rewritten by the application — admin-only re-embedding via the embed script.

Qur'an & Tafsir

  • Al-Qur'an al-Karim

    Full 6,236 ayah in Arabic + Indonesian (Kemenag) + English (Sahih International). Tagged with topic keywords by Gemini for better retrieval recall.

  • Tafsir Ibn Katsir

    Hafiz Ibn Kathir, Mubarakpuri abridged edition (AR + EN). Verse-by-verse commentary across all 6,236 ayat — chunked into ~1000-token windows so retrieval finds the paragraph, not the whole entry.

  • Tafsir Ath-Thabari

    Ibn Jarir al-Tabari, Jāmiʿ al-Bayān (Arabic) — the foundational verse-by-verse tafsir across all 6,236 ayat. Used as a second mufassir for the weekly Tafsir Pekan Ini reflections, rendered to Indonesian at compose time.

Core hadith

  • Sahih al-Bukhari

    Imam al-Bukhari · 7,275 hadith across 97 books. Full chapter structure with hadith numbers.

  • Sahih Muslim

    Imam Muslim · ~7,500 hadith across 56 books. Bilingual EN+ID embedding — 7,360 manually-curated Indonesian translations.

  • Riyad as-Salihin

    Imam an-Nawawi · 1,896 hadith across 372 chapters. Pedagogical collection organized by akhlak themes.

  • Bulugh al-Maram

    Ibn Hajar al-'Asqalani · 1,567 legal hadith ordered by fiqh topic. Primary reference for daleel-based fiqh.

Sirah & Sahabat

  • Sirah Ibnu Hisyam

    Abdul Malik ibn Hisham (d. 218H) · tahqiq of Ibn Ishaq's Sirah in 2 volumes: lineage, birth, Meccan da'wah, hijrah, ghazwah, conquest of Mecca, farewell pilgrimage.

  • Asy-Syama'il al-Muhammadiyyah

    Imam at-Tirmidhi (d. 279H) · ~415 hadith across 55 chapters on the Prophet's ﷺ physical traits, character, and daily life. A foundational sirah/akhlak text in Indonesian pesantren.

  • Hayatus Shahabah

    Shaykh M. Yusuf al-Kandahlawi · thematic collection of Sahabat stories and biographies (da'wah, faith, jihad, ibadah, akhlak). Widely used in modern halaqahs.

  • Al-Bidayah wan-Nihayah

    Ibn Kathir · Islamic history from creation, through the prophets' stories and Sirah Nabawiyyah, up to the Umayyad and Abbasid caliphates. 2,529 fasal, Hijr edition.

Fiqh

  • Al-Umm

    Imam ash-Shafi'i · the foundational fiqh work of the Shafi'i school, 44 books (Taharah to Nafaqat) in 1,332 chapters. Primary source for the madhab.

  • Fiqh as-Sunnah

    Sayyid Sabiq · contemporary daleel-based fiqh, not bound to a single madhab. 866 chapters across 62 fiqh books.

  • Fath al-Mu'in

    Shaykh Zainuddin al-Malibari (d. 987H) · classical Shafi'i fiqh matn, core curriculum in NU/traditional pesantren. 13 main chapters.

  • Fath al-Qarib al-Mujib

    Ibn Qasim al-Ghazzi (d. 918H), commentary on Abu Shuja's At-Taqrib · the entry point to Shafi'i fiqh in pesantren. 212 fasal across 17 fiqh books.

Aqidah & Adab

  • Bidayatul Hidayah

    Imam al-Ghazali · 28 chapters on the etiquette of worship, avoiding sin, and social conduct. A classical akhlak text, Maktabat Madbouli edition (1413H).

  • Nashaihul Ibad

    Shaykh Nawawi al-Bantani · commentary on Ibn Hajar's Munabbihat. Akhlak & tasawwuf advice organized by number (Tsunai through 'Asyari). A pesantren classic.

  • Adab al-'Alim wa al-Muta'allim

    Badruddin Ibn Jama'ah al-Kinani (d. 733H) · the primary source KH Hasyim Asy'ari drew on for NU's adab curriculum. 5 chapters: the virtue of knowledge, teacher conduct, student conduct, conduct toward kitabs, and conduct in the madrasah.

  • 'Aqidat al-'Awam

    Shaykh Ahmad al-Marzuqi al-Maliki (d. 1262H) · Ash'ari aqidah matn in nazhom (verse). Beginner-level pesantren material (TK-ibtidaiyyah).

  • Thalathat al-Ushul

    Shaykh Muhammad ibn Abd al-Wahhab (d. 1206H) · a concise Salafi tawhid matn covering the three principles every Muslim must know. Pairs with 'Aqidat al-'Awam for a different manhaj perspective.

Every entry in Qdrant carries (a) the original Arabic, (b) the translation in the brief's locale, and (c) the canonical citation (book + chapter + ayah/hadith number). Briefs always link back to the named source so the da'i can verify before delivery.

Browse the kitab library at /kitab

Observability — the boring but important part.

Most of these tables are append-only event logs. They drive the superadmin /admin/system dashboard.

social_posts

Every scraped item, with classifier output. Indexed on (platform, posted_at), (platform, region), and a unique (platform, external_id) for dedup.

topics

Theme clusters discovered by Gemini topic discovery per platform — auto-refreshed nightly.

briefs

User-generated briefs. Stores the retrieved daleel inline (JSONB) so brief reads never re-hit Qdrant.

usage_events

Every paid API call (OpenAI, Gemini, Anthropic, Apify, YouTube, RSS, Resend) with cost + tokens. Drives the API costs dashboard.

system_metrics

psutil snapshot every 60s. Drives the host CPU / memory / disk charts.

ingest_runs

start_run / finish_run lifecycle for every Celery task. Drives the pipeline-health dashboard.

page_views

Anonymous httpOnly session cookie. No IPs, no fingerprints. Drives web analytics.

donations · manual_costs

Manual VPS / domain invoice entries + recorded donations. Drives the public /transparency page.

contact_messages

Public /contact form submissions. Forwarded to ADMIN_EMAIL and stored as durable backup.

users · accounts · verification_tokens

NextAuth.js-standard tables. Verification tokens used for email-verification + password-reset flows.

Choices we made, and what we picked against.

Tech is full of judgement calls. Here's the explicit version of ours.

Why a monorepo with Python + TypeScript instead of a single language?
Python wins for the data/ML side (Gemini / OpenAI / Anthropic client integrations, Qdrant client, embedding pipeline — all most mature in Python). Next.js + Auth.js + Drizzle wins for the user-facing app. Trying to do ML in TS or Auth.js-equivalents in Python would have meant fighting the ecosystem at every step. The cost is two dependency managers, mitigated by uv + npm both being fast.
Why Qdrant instead of pgvector?
We already had Postgres for everything else, so pgvector was tempting. Qdrant won on filtering ergonomics (named payload fields, hybrid search), and on the fact that it's the dominant choice in production RAG stacks today — easier to find debugging help and tooling.
Why was IndoBERT retired and all sentiment moved to Gemini Flash-Lite?
It used to be a hybrid: Gemini event-valence for mainstream news, IndoBERT for social posts (X/IG/TikTok/YouTube). But a manual eval on 37 'korupsi' tweets on 2026-05-25 showed IndoBERT mislabelled 6 of 7 positives (sarcasm read as positive, supportive opinion as negative, rhetorical critique as neutral) — positive-class accuracy ~14%. After switching every platform to Gemini Flash-Lite, positive-class accuracy on X jumped to ~92% on the same sample. Extra cost: ~$1/mo for X sentiment, immaterial against the budget. Trade-off paid: torch + transformers + the HuggingFace cache (~1.5 GB worker image) leave the dependency tree, plus the worker cold-start gets ~10-15s faster.
Why Gemini Pro primary and Claude Sonnet fallback, not the reverse?
Gemini 2.5 Pro is ~35% cheaper than Sonnet 4.5 at comparable quality bracket for this kind of long-form structured-output task. Putting it first lowers the running cost meaningfully; Sonnet's the backstop when Google has an outage.
Why not use a framework like LangChain or LlamaIndex?
Our retrieval + synthesis flow is ~80 lines of TypeScript. Adding a framework would have added 200 transitive dependencies for value we don't use. We're not chaining many models or supporting many vector DBs — when those needs appear, the trade flips.
Why no Facebook scraper on the schedule?
A `scrape_facebook()` function exists in `apify.py`, but no beat entry calls it. Three reasons. (1) Facebook has been declining as a content surface for younger Indonesian Muslims — most of the genuinely-active da'wah conversation has migrated to TikTok and X. (2) Apify's Facebook actor is among the priciest per item, and the marginal yield per IDR didn't justify it. (3) Instagram already covers the Meta ecosystem on our active schedule. We keep the scraper code ready, so the day data tells us we're missing something it can be enabled with a one-line beat entry.

Read the source. Send a PR.

The whole repo is on GitHub under the MIT license. Issues, PRs, du'a, and feedback are all welcome — there is no contributor-license-agreement step and no formal review board, just a small team that will read what you send.

Open the repository

github.com/taufik-adinugraha/dakwah-lens