Bd issue:
ucok-e03· Status: Plan / pre-build One-liner: A Truecaller-style Android app with a WhatsApp-style UI, riding entirely on the existingjapri.combackend.
| 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.
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.
┌─────────────────────┐ 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)
/caller/feed, /caller/lookup, /caller/me return.-- 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);
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 |
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.
When the Android phone state goes to RINGING, we have ~5–8 seconds before the user decides to answer. Strategy:
/caller/bulk-lookup for the last 200 numbers in the call log. Worker caches all of them in KV for 24h.PHONE_STATE_RINGING, the app fires /caller/lookup?phone=.... Worker hits KV first (| 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:
| 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.
WA-style is a visual language, not pixel-perfect copying:
#075E54) with japri orange accent (#FF6B35 for FAB and active states)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
| 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.
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.japri.id/download. Signed APK is needed because Play Store reviews of caller apps can take 2–6 weeks initially.| 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.
| 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 |
RoleManager.ROLE_DIALER + ROLE_CALL_SCREENING). Best-in-class spam blocking pre-ring; accept stricter Play Store review.caller_seed_sources for UU PDP audit.japri.id (matches domain). Play Store keywords: caller id indonesia, pemblokir spam, identifikasi penelpon, anti penipuan, anti pinjol./home/ucok/web/japri.id/plan.mducok-e03 filedThis domain MUST operate within these constraints — no exceptions:
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.
Ask AI to research, improve, or generate content.
Try: "Research competitors for this niche"