japri.id Open ↗ Has Plan
57
DOM 73 PLN 100 REV 10 EAS 30
plan.md
PREVIEW

japri.id — Caller App Plan

Bd issue: ucok-e03 · Status: Plan / pre-build One-liner: A Truecaller-style Android app with a WhatsApp-style UI, riding entirely on the existing japri.com backend.


1. Product Concept

What Android caller-ID + spam shield + chat, in one app
Who Indonesian phone users tired of penipuan/pinjol calls
Why now Truecaller has weakened reputation in ID (privacy backlash, ads); Indonesia's UU PDP (in force 2024) makes "no contact-book scraping" a differentiator
Domain japri.id (.id = Indonesia, signals local trust)
Tagline "Tahu siapa yang nelpon, sebelum kamu angkat."

What we are not building: a Western Truecaller clone. No bulk address-book upload. No selling user data. No "find my friend" reverse search. Privacy is the wedge.


2. Inheritance from japri.com

japri.id is a second front-end on the same backend, not a new product silo.

Reuse From japri.com How
Auth WA OTP via +6287882530000 Same /auth/wa-otp endpoint
User accounts D1 users table Same user_id namespace
Wallet D1 wallet + Solana escrow For premium subscriptions
Chat Durable Object ChatRoom Same WebSocket protocol
Avatars R2 library bucket Shared keys
WA outbound wa.japri.com PM2 relay OTP, notifications
Backend CF Worker japri-api Add /caller/* routes; do not create a separate worker
Business directory D1 businesses Auto-shows verified business names on incoming calls

Why one worker, not two: A user who registers in japri.id should immediately be able to chat in japri.com. Two workers would force account federation and double the maintenance. The cost is a slightly bigger worker — well under the 10MB CF Worker size limit.


3. Architecture

┌─────────────────────┐     FCM data push      ┌──────────────────────┐
│  Android app        │ ◄──────────────────── │  CF Worker            │
│  (Kotlin+Compose)   │                       │  api.japri.com        │
│                     │  HTTPS / WebSocket    │                       │
│  - Call log render  │ ─────────────────────►│  /caller/*  (NEW)     │
│  - Overlay caller   │                       │  /chat/*    (reused)  │
│  - WA-style UI      │                       │  /auth/*    (reused)  │
└─────────────────────┘                       └──────────┬───────────┘
                                                          │
                                          ┌───────────────┼───────────────┐
                                          ▼               ▼               ▼
                                       D1 japri-db    DO ChatRoom    R2 library
                                       (+ new tables)
                                          │
                                          ▼
                                  Workers AI (spam classifier)

Why "100% backend"

  • Android app holds zero business logic. It renders what /caller/feed, /caller/lookup, /caller/me return.
  • Spam scoring, identity dedup, business matching all happen in the Worker.
  • All lists/tiles are JSON — server can ship a new banner, promo, or settings row by editing the response, not by Play Store release.
  • This is data-SDUI, not full layout-SDUI. Screens are native Compose; the content is server-owned. (Full layout-SDUI risks a Play Store rejection for "interpreting remote code.")

4. Backend (additions to api.japri.com)

4.1 New D1 tables

-- Verified identity for a phone number (one row per phone)
CREATE TABLE caller_identities (
  phone TEXT PRIMARY KEY,            -- E.164, e.g. +6287882530000
  user_id TEXT,                      -- FK to users (if claimed)
  business_id TEXT,                  -- FK to businesses (if business)
  display_name TEXT NOT NULL,
  badge TEXT,                        -- 'verified' | 'business' | 'gov' | 'spam'
  avatar_url TEXT,
  bio TEXT,
  city TEXT,
  spam_score INTEGER DEFAULT 0,      -- 0-100, derived
  spam_count INTEGER DEFAULT 0,
  lookup_count INTEGER DEFAULT 0,
  updated_at INTEGER NOT NULL
);
CREATE INDEX idx_ci_user ON caller_identities(user_id);
CREATE INDEX idx_ci_spam ON caller_identities(spam_score);

-- Crowd reports (one row per report, anonymized)
CREATE TABLE spam_reports (
  id TEXT PRIMARY KEY,
  phone TEXT NOT NULL,
  reporter_user_id TEXT NOT NULL,    -- never exposed
  category TEXT NOT NULL,            -- 'penipuan'|'pinjol'|'telemarketing'|'robocall'|'lainnya'
  note TEXT,
  created_at INTEGER NOT NULL
);
CREATE INDEX idx_sr_phone ON spam_reports(phone);
CREATE INDEX idx_sr_reporter ON spam_reports(reporter_user_id);

-- Per-user block list
CREATE TABLE blocked_numbers (
  user_id TEXT NOT NULL,
  phone TEXT NOT NULL,
  reason TEXT,
  created_at INTEGER NOT NULL,
  PRIMARY KEY(user_id, phone)
);

-- Device push registry
CREATE TABLE devices_fcm (
  id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL,
  fcm_token TEXT NOT NULL,
  platform TEXT NOT NULL,            -- 'android' for v1
  app TEXT NOT NULL,                 -- 'japri.id' | 'japri.com'
  app_version TEXT,
  last_seen INTEGER NOT NULL,
  created_at INTEGER NOT NULL
);
CREATE UNIQUE INDEX idx_dev_token ON devices_fcm(fcm_token);
CREATE INDEX idx_dev_user ON devices_fcm(user_id);

4.2 New endpoints (under api.japri.com)

Method Path Purpose
GET /caller/lookup?phone=+62... Single number → identity + spam score (cached 24h in KV)
POST /caller/bulk-lookup {phones:[...]} → array, used to hydrate call log on app open
POST /caller/report {phone, category, note?} — JWT required, rate-limited 10/day
GET /caller/me Current user's own listing
PUT /caller/me Update own display name/bio/avatar
POST /caller/me/verify Trigger WA OTP to confirm ownership of a phone (reuses /auth/wa-otp)
POST /caller/block / /caller/unblock Personal block list
GET /caller/blocks List user's blocks
POST /caller/devices Register/refresh FCM token
GET /caller/feed Server-driven home tiles (stats, recent spam in your area, premium upsell, news)
GET /caller/inbox Server-driven settings/notification screen

4.3 Spam scoring (Worker logic)

A simple, transparent formula — explainable scoring beats a black-box ML classifier for trust:

score = clamp(
  reports_last_30d * 8
  + reports_last_7d  * 4
  + (lookup_count > 50 ? 5 : 0)        // weak signal: many strangers searching
  - (verified_business ? 60 : 0)
  - (verified_user     ? 40 : 0),
  0, 100
)
badge = score >= 70 ? 'spam' : (verified ? 'verified' : null)

Workers AI (@cf/meta/llama-3.3-70b-instruct-fp8-fast) is used only to classify a free-text user note into a category (free, no per-call cost). We do not let AI decide spam-vs-not — too easy to game and too hard to defend.

4.4 Real-time path for incoming calls (sub-800ms)

When the Android phone state goes to RINGING, we have ~5–8 seconds before the user decides to answer. Strategy:

  1. Pre-warm cache. On app launch, the app calls /caller/bulk-lookup for the last 200 numbers in the call log. Worker caches all of them in KV for 24h.
  2. Live lookup. On PHONE_STATE_RINGING, the app fires /caller/lookup?phone=.... Worker hits KV first (30ms), falls back to D1 (80ms). Total target: < 200ms over 4G.
  3. FCM data-message fallback. If the carrier delivers the SIP INVITE before the broadcast (it sometimes does), the backend can also push a data message via FCM with the lookup result, so the overlay shows even before the OS-level call screen does.

5. Android App

5.1 Stack

Layer Choice Why
Language Kotlin Standard for new Android
UI Jetpack Compose + Material3 First-class Compose, WhatsApp-green theme
Min SDK 26 (Android 8.0) ~98% device coverage
Target SDK 35 (Android 15) Play Store requirement
Net Retrofit + OkHttp + kotlinx.serialization Standard, well-known
Local cache Room (read-through cache only — backend is source of truth)
Image Coil Compose-native
Push Firebase Cloud Messaging For caller pre-warm + notifications
Build Gradle KTS, single module to start

We considered Flutter and Capacitor/WebView. Rejected because:

  • Caller overlay + default-dialer + telephony APIs need native — wrappers fight the OS.
  • Battery: a WebView always running for incoming-call detection is unacceptable.
  • The "100% backend" bet is about content and logic, not about avoiding native code on the device.

5.2 Permissions (and the trust we ask for)

Permission Purpose Optional?
READ_PHONE_STATE Detect incoming call ringing state Required
READ_CALL_LOG Render call history with names/badges Required
POST_NOTIFICATIONS (33+) Show overlay + notifications Required
SYSTEM_ALERT_WINDOW Floating overlay over call screen Optional (degrades to notification)
ROLE_CALL_SCREENING (29+) Best-in-class spam blocking before ring Optional
READ_CONTACTS Local-only — match names from user's own contacts Optional, never uploaded
RECORD_AUDIO (later, for in-app calls) Out of scope v1

No READ_CONTACTS upload. This is the single most important UX difference from Truecaller. Privacy policy makes this explicit.

5.3 WhatsApp-style UI inherited from japri.com

WA-style is a visual language, not pixel-perfect copying:

  • Bottom tab bar: Telpon · Pesan · Kontak · Saya (4 tabs)
  • App-bar color: WA-green (#075E54) with japri orange accent (#FF6B35 for FAB and active states)
  • Round avatars, chip badges (verified ✓, spam ⚠, business 🏢)
  • Long-press for context menu (block, report, save)
  • Swipe-left on call log row → quick-block; swipe-right → message
  • FAB lower-right → new call / new chat (per tab)
  • System font stack from CLAUDE.md design standards
  • Dark mode follows system
  • Safe-area insets respected for notched devices

Mock screen list (drives the Compose nav graph):

NavHost("calls")
├── calls           — list of recent calls, search bar at top
├── call_detail/{phone}
├── chats           — chat list (reuses japri.com WS)
├── chat/{room_id}
├── contacts        — user's japri-verified network
├── identity/{phone}— public profile of a phone
├── me              — settings, premium, block list, privacy
├── login           — WA OTP flow
└── overlay         — bottom-sheet shown on incoming call

5.4 What's "server-driven"

Screen SDUI? Notes
Home (Calls tab top tiles) ✅ Full Stats / promo / news mixed via tile types
Saya (Settings) ✅ Full Toggle rows + sections from /caller/inbox
Identity profile ⚠ Partial Layout native, content from /caller/lookup
Call log ❌ Native Performance + telephony coupling
Chat thread ❌ Native Reuses japri.com WS protocol
Overlay ❌ Native Sub-200ms render budget

Tile schema (returned by /caller/feed and /caller/inbox):

{
  "tiles": [
    {"type": "stat",     "title": "12 panggilan terblokir minggu ini", "icon": "shield"},
    {"type": "promo",    "title": "Premium IDR 49K/bln", "cta_url": "japri://upgrade"},
    {"type": "news",     "title": "Modus penipuan terbaru: ...", "url": "https://..."},
    {"type": "row",      "label": "Mode Senyap", "control": "switch", "key": "silent_mode"},
    {"type": "section",  "title": "Privasi"}
  ]
}

Adding a new tile type is a Worker change + Compose when (tile.type) branch. Cheap to extend, hard to abuse.


6. Privacy, Legal, Distribution

  • UU PDP (Indonesia, in force 2024): caller-ID falls under personal data. Our position: every listing is either (a) the user's own, claimed via WA OTP, (b) a registered business with a verifiable site, or (c) a number with crowd reports — and crowd reports never expose reporter identity. No third-party data brokers.
  • Play Store: Caller-ID apps are heavily scrutinized. Need Default Phone App declaration, sensitive permission disclosures, and a published privacy policy at japri.id/privacy. We will not request READ_CONTACTS until the user explicitly opens the "match names from my contacts" feature.
  • Distribution: Play Store + signed APK at japri.id/download. Signed APK is needed because Play Store reviews of caller apps can take 2–6 weeks initially.
  • CF residency: CF has no Indonesia DC; data is at edge nodes regionally. PDP doesn't currently mandate in-country storage for non-public-sector data — fine for v1, flag for legal review at scale.

7. Phases

Phase Scope Output
P1 — Backend New D1 tables, /caller/* endpoints, KV cache, FCM key in secrets wrangler deploy of japri-api; curl works
P2 — Android scaffold Compose project, theme, nav graph, login via WA OTP, empty tabs APK that logs in
P3 — Calls tab Read call log, bulk-lookup hydration, overlay on incoming, block "Truecaller-lite" demo
P4 — Chats tab WebSocket to existing ChatRoom DO, message UI Full chat parity with japri.com
P5 — Spam reporting + community Report flow, public profile, badge logic, news tiles Network effect kicks in
P6 — Premium IDR subscription via Japri wallet (Solana stablecoin or Xendit), gold checkmark, who-viewed-me, advanced stats First revenue

P1 is the unblock. Everything else is a thin client over P1.


8. Risks & Mitigations

Risk Mitigation
Play Store rejects caller-app listing Have a signed APK ready at japri.id/download as fallback
Empty database = no caller ID for unknown numbers Pre-seed with businesses table + scrape government white-page sources (BUMN, kementerian, polisi/pemadam hotlines)
Spam reports get weaponized (rivals reporting competitors) Rate-limit per reporter, require account ≥ 7 days old, weight by reporter's own verification
FCM data-message latency varies Local lookup is the primary path; FCM is a redundancy
Sub-second overlay on low-end phones Coil image cache + Room cache for last 200 numbers — overlay must render even offline
Compose-only excludes legacy phones Min SDK 26 = ~98% coverage in ID; acceptable

9. Locked Decisions (2026-05-01)

  1. Monetization: IDR 199K/year subscription via Japri wallet (Xendit primary, Solana fallback). 65% effective discount vs monthly. No ads.
  2. Phone role: Request Default Phone App role (Android RoleManager.ROLE_DIALER + ROLE_CALL_SCREENING). Best-in-class spam blocking pre-ring; accept stricter Play Store review.
  3. iOS: Android-only indefinitely. Backend stays iOS-friendly but no iOS client work scheduled.
  4. Seed strategy: Seed with everything we can find — japri businesses + gov hotlines + public Indonesian business directories. Provenance logged in caller_seed_sources for UU PDP audit.
  5. Brand: japri.id (matches domain). Play Store keywords: caller id indonesia, pemblokir spam, identifikasi penelpon, anti penipuan, anti pinjol.

10. Definition of Done (for this plan stage)

  • Plan committed to /home/ucok/web/japri.id/plan.md
  • Bd issue ucok-e03 filed
  • Open decisions answered by user
  • P1 backend bd sub-issues filed and dependencies wired
  • Android scaffold bd sub-issues filed

⚙ HARD CONSTRAINTS (enforced for all sites)

This domain MUST operate within these constraints — no exceptions:

  • 100% Cloudflare serverless — Workers + D1 + R2 + KV + Workers AI + Vectorize. NEVER PM2, NEVER VPS, NEVER Docker in production path.
  • 100% AI-automated — every customer interaction, every moderation decision, every transaction reconcile = AI. No manual queue, no live human chat support, no physical fulfillment.
  • 1-operator solo — one person can run the entire operation from a phone. No team meetings, no shared inbox, no shift rotation.
  • WhatsApp AI bot for all support (24/7, instant response, no SLA promises that need humans).
  • Mayar QRIS for all Indonesian payments (subscription auto-renew, no manual invoicing).
  • Indonesian UI primary — bahasa-first, English fallback only where unavoidable.
  • Privacy — opt-in only, delete-on-request honored within 24h (cron-driven).
  • No physical goods, no inventory — digital products + affiliate referrals only.

If the plan above describes any flow that violates these constraints, treat the plan as ASPIRATIONAL only and rework before building. The constraint trifecta wins.

AI ASSISTANT

Ask AI to research, improve, or generate content.

Try: "Research competitors for this niche"

Actions