belumkerja.com Open ↗ Has Plan
55
DOM 65 PLN 100 REV 10 EAS 30
plan.md
PREVIEW

belumkerja.com — Indonesian LinkedIn-style Jobs Board

Domain: belumkerja.com (literally: "not yet working") Stack: 100% Cloudflare (Worker + D1 + R2 + KV + Vectorize + Workers AI + Queues + Cron + Durable Objects) Code home: /home/ucok/web/belumkerja.com/worker-cf/ Sister project: beasiswa.net (siswa profile inheritance)


1. Positioning & Why It Wins

belumkerja.com Jobstreet Glints LinkedIn ID
Audience Fresh grads, students, career pivots Mid-career Indonesians Mid-career SEA White-collar global
Profile cold-start Auto-imported from beasiswa.net Manual Manual Manual
Primary channel WhatsApp + web Email + web Email + web Web
Headhunter role First-class Hidden Hidden Implicit
Language Indonesian-first Bilingual Bilingual English-first
Free for job seekers Yes Yes Yes Yes
Job posting fee TBD Paid Paid Paid

The wedge: the user just got a beasiswa.net scholarship → graduates → already has a verified profile → one tap to flip to "siap kerja" status. Zero re-onboarding. That's the entire moat.


2. Three User Roles

                    ┌────────────────────┐
                    │      USER (D1)     │
                    │  user_type column  │
                    └──────────┬─────────┘
            ┌──────────────────┼──────────────────┐
            ▼                  ▼                  ▼
    ┌──────────────┐  ┌───────────────┐  ┌─────────────────┐
    │   pencari    │  │   perekrut    │  │    pemberi      │
    │   (siswa)    │  │  (headhunter) │  │   (employer)    │
    └──────────────┘  └───────────────┘  └─────────────────┘
    Job seeker        Middleman/agent     Company / individual
    Inherits siswa    Multi-company       hiring direct
    profile from      pipeline mgmt
    beasiswa.net      Commission tracking

Single users table, user_type ENUM('pencari','perekrut','pemberi'), separate role-extension tables. One person can hold multiple roles (a senior dev moonlighting as a recruiter, etc.) — modelled as user_roles(user_id, role) join, not exclusive ENUM.


3. Architecture — 100% Cloudflare

Layer CF Resource Purpose
Edge HTML/SSR Worker belumkerja-com All public pages, SSR'd HTML (no JS-filled divs — per CLAUDE.md)
Static assets Workers Assets binding Bundled CSS/JS/icons
Relational DB D1 belumkerja_com_prod Users, profiles, jobs, applications, posts, articles
Object store R2 belumkerja-com-assets CVs, profile pics, company logos, portfolio files
Hot cache KV belumkerja-com-cache Autocomplete, hot job lists, search results
Config KV belumkerja-com-config Feature flags, role taxonomy
Semantic search Vectorize belumkerja-jobs, belumkerja-candidates AI matching (BGE embeddings)
AI Workers AI Llama 3.3 70B for articles/Q&A, BGE-base for embeddings
Async Queue belumkerja-com-jobs (notifications), belumkerja-com-ingest (scrape/embeddings)
Coordination Durable Objects belumkerja-com-coordination Live "X applied" counters, chat rooms, application locks
Scheduling Cron Triggers Daily article gen, weekly digest, autocomplete refresh, Vectorize sync
Auth WhatsApp OTP via existing japri-wa-relay PM2 (only server dep), JWT in HttpOnly cookie

Cross-site auth: beasiswa.net and belumkerja.com share the same phone-based OTP issuer and a small shared_users D1 (or KV mirror) keyed by phone hash. A siswa logging into belumkerja.com with their beasiswa phone gets their profile auto-imported on first login.


4. Database Schema (D1)

Inherited from beasiswa.net (replicated, not joined cross-DB)

  • users — slug, phone, phone_hash, email, name, user_type, bio, is_verified
  • student_profiles — education_level, school, university, faculty, major, GPA, graduation_year, province/city, achievements, skills, interests
  • portfolio_items — title, description, item_type, url

New tables for jobs board

-- Extended profile (LinkedIn-like)
job_seeker_profiles(user_id PK, headline, summary, current_status, expected_salary_min, expected_salary_max,
                    work_preferences JSON, available_from, cv_r2_key, video_intro_r2_key)

experiences(id, user_id, company_id, role_title, employment_type, start_date, end_date,
            is_current, description, location, location_type)

skills(id, slug, name, category)
user_skills(user_id, skill_id, level, years, endorsement_count)
endorsements(id, from_user_id, to_user_id, skill_id, created_at)

certifications(id, user_id, name, issuer, issued_at, expires_at, credential_url, file_r2_key)

languages(id, user_id, language_code, proficiency)

-- Social / network
connections(follower_id, following_id, status, created_at)  -- mutual when status='accepted'
posts(id, user_id, body, media_r2_keys JSON, post_type, created_at)
post_reactions(post_id, user_id, reaction)
comments(id, post_id, user_id, body, parent_comment_id)
recommendations(id, from_user_id, to_user_id, body, role_title, status)

-- Companies (employers)
companies(id, slug, name, industry_id, size, founded_year, hq_city, website, logo_r2_key,
          verified, owner_user_id, claim_status)
company_followers(company_id, user_id)
company_reviews(id, company_id, reviewer_user_id, salary, rating, pros, cons, anonymous)

-- Jobs
job_categories(id, slug, name, parent_id)
industries(id, slug, name)
jobs(id, slug, company_id, posted_by_user_id, posted_by_role,  -- 'pemberi' or 'perekrut'
     title, category_id, employment_type, location_city_id, location_type,  -- onsite/hybrid/remote
     salary_min, salary_max, currency, experience_level, education_required,
     description_md, requirements_md, benefits_md, application_url, application_email,
     status, posted_at, expires_at, view_count, applicant_count,
     embedding_id  -- Vectorize key
)
job_skills(job_id, skill_id, required INTEGER)
applications(id, job_id, applicant_user_id, headhunter_user_id NULL, cv_r2_key, cover_letter,
             status, applied_at, viewed_at, response_at)
saved_jobs(user_id, job_id, saved_at)
job_alerts(id, user_id, query JSON, frequency, last_sent_at)

-- Headhunter pipeline
talent_pools(id, headhunter_user_id, name, description)
talent_pool_members(pool_id, user_id, notes, status)
placements(id, headhunter_user_id, candidate_user_id, job_id, status,
           commission_amount, commission_paid, placed_at)

-- Programmatic SEO
seo_pages(id, slug, page_type, params JSON, title, description, h1, body_md,
          last_built_at, last_visited_at, view_count, status)  -- pSEO output
seo_keywords(id, query, category, source, search_volume_estimate, harvested_at)  -- from Google Autocomplete
seo_articles(id, slug, keyword_id, title, body_md, hero_r2_key, author_user_id,
             published_at, view_count, ai_model, edited_by_human)
qna_questions(id, slug, question, category, source_keyword_id, view_count, answer_count)
qna_answers(id, question_id, body_md, author_user_id, ai_generated, upvotes, accepted)

-- Cities / geo (reuse beasiswa)
cities(id, slug, name, province, lat, lng, population)

-- Notifications & messaging
conversations(id, type, participants_hash)
messages(id, conversation_id, sender_user_id, body, media_r2_keys, read_at)
notifications(id, user_id, type, payload JSON, read_at)

5. LinkedIn-style Profile Surface

URL: /u/{slug} (consistent with beasiswa.net /profil/{slug} — different path, same canonical user)

Sections in order:

  1. Hero — photo, name, headline, location, current status badge ("Sedang Mencari", "Bekerja", "Freelance"), connect/message CTA
  2. About (summary)
  3. Pengalaman (experiences)
  4. Pendidikan (student_profiles + custom educations)
  5. Keahlian & Endorsement (skills, endorsements)
  6. Sertifikasi
  7. Portofolio (inherited from portfolio_items)
  8. Bahasa
  9. Rekomendasi
  10. Aktivitas / Postingan (last 5 posts)

All sections SSR'd into static HTML (CLAUDE.md rule: never empty divs filled by JS). Edit-in-place via inline forms with <dialog> modals + HTMX-style partial updates.


6. Job Board Features

For pencari (job seeker)

  • Bottom nav: Beranda / Lowongan / Jaringan / Pesan / Profil
  • Swipe-cards UI for jobs (Tinder-for-jobs) with "Lamar / Lewati / Simpan"
  • 1-tap apply with auto-CV (PDF generated from profile, stored in R2)
  • AI cover letter generator (Llama 3.3 70B)
  • WhatsApp job alerts (daily/weekly digest)
  • "Cocok untukmu" semantic match (Vectorize belumkerja-jobs × user's embedding)
  • Application tracker with status timeline

For perekrut (headhunter)

  • Talent search with filters (skill, location, GPA, university, salary expectation)
  • Boolean + semantic search (Vectorize belumkerja-candidates)
  • Talent pools (saved candidate lists)
  • Bulk message via WhatsApp templates
  • Placement tracker with commission ledger
  • "Submit candidate to my client" workflow (creates applications row with headhunter_user_id set)

For pemberi (employer)

  • Company page (claim flow if pre-existing scraped page)
  • Post job with AI-assisted draft (Llama suggests title, salary range, skills based on description)
  • Applicant inbox with AI ranking ("Top 10 cocok")
  • Video screening questions (R2-stored responses)
  • Verified-employer badge (CF Access + KYB doc upload)

7. Programmatic SEO — URL Patterns

Target: ~500K indexable pages within 6 months. Each must be SSR'd, semantic, with FAQ schema and breadcrumbs.

Pattern Example Volume estimate
/lowongan/{kota} /lowongan/jakarta ~514
/lowongan/{kota}/{kategori} /lowongan/bandung/programmer 514 × 100 = 51,400
/lowongan/{kategori} /lowongan/marketing ~100
/lowongan/{industri}/{kota} /lowongan/perbankan/surabaya 50 × 514 = 25,700
/lowongan/fresh-graduate/{kota} /lowongan/fresh-graduate/yogya ~514
/lowongan/tanpa-pengalaman/{kota} /lowongan/tanpa-pengalaman/medan ~514
/lowongan/wfh/{kategori} /lowongan/wfh/desain-grafis ~100
/lowongan/part-time/{kota} /lowongan/part-time/depok ~514
/lowongan/lulusan/{jenjang}/{kota} /lowongan/lulusan/smk/bekasi 6 × 514 = 3,084
/lowongan/lulusan/{jurusan} /lowongan/lulusan/teknik-informatika ~200
/gaji/{posisi} /gaji/data-scientist ~200
/gaji/{posisi}/{kota} /gaji/akuntan/jakarta 200 × 514 = 102,800
/perusahaan/{slug} /perusahaan/gojek ~10K (scraped)
/perusahaan/{kota}/{industri} /perusahaan/jakarta/fintech 514 × 50 = 25,700
/karir/{posisi} /karir/dokter-gigi ~200
/karir/{posisi}/syarat /karir/pilot/syarat ~200
/karir/{posisi}/jurusan /karir/pengacara/jurusan ~200
/karir/{posisi}/skill /karir/data-analyst/skill ~200
/lulusan/{universitas} /lulusan/itb ~4,500 (PDDikti)
/lulusan/{sekolah} /lulusan/sma-3-jakarta ~30K (NPSN data)
/q/{slug} /q/apakah-fresh-graduate-bisa-jadi-pns ~50K (autocomplete)
/artikel/{slug} /artikel/cara-menulis-cv-fresh-graduate ~10K

Total indexable: ~310K core pages + ~80K Q&A + ~10K articles = ~400K. Once content engine runs steady-state, it'll cross 500K within 6 months.

Content quality guard

Empty /lowongan/{kota}/{kategori} pages with 0 jobs MUST NOT return 200 with thin content. Either:

  • Show "0 lowongan saat ini, tapi 12 perusahaan di {kategori} aktif merekrut" + nearby city fallback + relevant articles, OR
  • Return 404 if even that fallback yields nothing (Google penalizes thin doorway pages).

8. Article + Q&A Engine — Google Autocomplete

Pattern lifted from /home/ucok/web/ourfuntech.com/src/cron.ts:

// Hourly cron — harvest autocomplete, dedup, queue article gen
const seeds = [
  // city-specific
  ...cities.map(c => `lowongan kerja ${c.name}`),
  ...cities.map(c => `gaji di ${c.name}`),
  // role-specific
  ...roles.map(r => `cara menjadi ${r.name}`),
  ...roles.map(r => `gaji ${r.name}`),
  ...roles.map(r => `syarat ${r.name}`),
  // intent-specific
  'lowongan kerja fresh graduate',
  'lowongan kerja tanpa pengalaman',
  'tips wawancara kerja',
  'cara membuat cv',
  'cara melamar kerja online',
  // company-specific (top 200 companies)
  ...topCompanies.map(c => `cara melamar ${c.name}`),
];

for (const seed of seeds) {
  const r = await fetch(`https://suggestqueries.google.com/complete/search?client=firefox&q=${encodeURIComponent(seed)}`);
  const [, suggestions] = await r.json();
  for (const s of suggestions) {
    await db.prepare('INSERT OR IGNORE INTO seo_keywords (query, category, source) VALUES (?, ?, ?)')
      .bind(s, classify(s), 'google_autocomplete').run();
  }
  await sleep(500);  // rate-limit ourselves
}

Then a second cron picks N unprocessed keywords, classifies each as article|qna|salary_page|career_guide, and dispatches to Llama 3.3 70B with role-specific prompts. Output is human-edit-flag-able (seo_articles.edited_by_human); we batch-publish only after a daily quality check (length > 800 words, contains H2s, has internal links).

Q&A engine specifics

Long-tail queries that read as questions ("apakah", "bagaimana", "berapa", "kapan", "di mana") become qna_questions. Each Q&A page:

  • One AI-generated authoritative answer at top (with Article + FAQPage JSON-LD)
  • Open thread for human answers (boosts dwell time + freshness signal)
  • "Pertanyaan terkait" cluster (3-5 from same category)

This is identical to the existing pattern on anehtapinyata.com and alamgaib.com — just retargeted to job intent.


9. Mobile-Native UX (per CLAUDE.md universal standards)

  • Bottom nav 5 items, sticky, safe-area-inset
  • NO hamburger menu
  • Pull-to-refresh on /lowongan and /beranda
  • Skeleton loaders, not spinners
  • Horizontal carousels for "Lowongan untukmu", "Perusahaan trending", "Lulusan terbaru"
  • Swipeable tabs on job-detail page: Deskripsi | Perusahaan | Pelamar | Q&A
  • View Transitions API for navigation
  • PWA: manifest, install prompt after 2nd visit
  • Web Push: "lowongan baru cocok buatmu", deadline reminders
  • Voice search on /lowongan (SpeechRecognition API)
  • AI chat ("Tanya Belum") floating button — answers job/career questions, contextual to current page

10. Phased Rollout

Phase Scope Definition of done
0 — Foundation CF resources provisioned, schema migrated, beasiswa shared-auth wired, base layout SSR'd Logged-in siswa from beasiswa.net lands on /profil/{slug} and sees their data
1 — Profile + Jobs MVP LinkedIn-style profile, employer can post a job, seeker can browse + apply, R2 CV storage First real job posted by a real employer, first real application sent
2 — Programmatic SEO core /lowongan/{kota}, /lowongan/{kota}/{kategori}, /gaji/{posisi}/{kota}, sitemap, llms.txt, IndexNow First 50K pages indexed by Google
3 — Article + Q&A engine Autocomplete cron + Llama generator + Q&A threads + FAQ schema 500 articles/Q&A pages live, editorial review queue
4 — Social layer Connections, posts, feed, recommendations, endorsements DAU > 1K, average 3 connections/user
5 — Headhunter dashboard Talent search, pools, placements, commission ledger First commission paid through platform
6 — AI matching Vectorize embeddings for jobs + candidates, "cocok untukmu", AI cover letter 30%+ of applies use AI suggestion
7 — Mobile polish PWA install, push notifications, voice search, View Transitions Lighthouse PWA + SEO + Performance all 90+

11. Open Decisions (need your input)

These three shape the product more than anything else:

A. Profile sync model with beasiswa.net — LOCKED: Lazy-on-graduation

When a phone hash logs in to belumkerja.com for the first time, the worker reads beasiswa-db (second D1 binding, read-only intent) by phone_hash. If student_profiles.graduation_year <= currentYear, we one-tap import (copy users + student_profiles + portfolio_items rows into belumkerja's D1). After import, profiles diverge — beasiswa edits don't auto-flow into belumkerja. User can re-trigger import manually if they want fresh data.

Why: simplest implementation (no Queue, no schema coupling), respects the "graduation = transition" semantic, and avoids polluting belumkerja with still-studying users who aren't ready to job-hunt.

Edge case: phone exists in beasiswa but graduation_year > currentYear → show "Kamu masih kuliah. Datang lagi setelah lulus 🎓" with a "Notify me when ready" button (writes to a pre_register KV that fires a cron-driven WhatsApp on grad date).

B. Monetization

  1. Free everything, ads only. Lowest friction.
  2. Free for seekers, paid job posts for employers (Rp 200K-500K per post). Standard.
  3. Free for seekers + employers, paid for headhunters (commission % on placement, or subscription for talent search). Aligns with the headhunter being the only party with strong ROI.
  4. Freemium employer (3 free posts/month, paid for premium placement + AI ranking).

C. Headhunter business model

  1. Open marketplace — any registered headhunter can claim/contact any candidate. Volume play.
  2. Curated — headhunters apply + are vetted; quality > quantity.
  3. Verticalized — only certain roles (e.g., tech, healthcare) initially; expand later.
  4. Commission split — platform takes X% of every placement; headhunters can't operate "off-platform" with leads they got here.

Pick one per row (or write your own variant) and I'll wire the schema + features around your choices in the next iteration.


⚙ 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