AI-powered customer service SaaS for Indonesian SMEs.
Kundesk lets business owners upload their documents — menu, FAQ, price list — and instantly get help from KUN, an AI assistant that answers customer questions 24/7, in Bahasa Indonesia, trained only on their own content.
Live demo: kundesk.vercel.app
Indonesian SMEs — warung, klinik, salon, properti — are drowning in repetitive WhatsApp and website messages from customers asking the same questions every day. They can't afford enterprise customer service solutions, and nothing is built specifically for the Indonesian context.
Sign up, upload your business documents, and Kundesk deploys KUN — an AI assistant that handles your customers automatically. Every answer KUN gives comes from your own documents, not from generic AI knowledge.
KUN is Kundesk's built-in AI assistant — the entity your customers actually talk to. KUN is not a generic chatbot; it has a fixed identity, a consistent voice, and speaks naturally in Bahasa Indonesia using the "Kak" address form that Indonesians expect in a friendly service context.
KUN is trained exclusively on the documents each business uploads. Ask KUN about something not in those documents and it won't guess — it tells the customer honestly and suggests they contact the business directly.
When a customer needs a human, they can ask KUN to connect them with the business owner. KUN detects the request, notifies the dashboard instantly, and holds the conversation gracefully until staff takes over. When staff is done, KUN resumes automatically.
Landing Page - Hero
Dashboard Overview
Chat Interface
- Document-trained KUN — upload PDF or TXT files; KUN answers only from your content via RAG (Retrieval-Augmented Generation)
- Multiple delivery channels — shareable public chat link, QR code (print it on your counter or menu), and embeddable web widget
- Human handoff — take over any KUN conversation manually from the dashboard; return to KUN when done; dismiss pending requests with a canned message
- Live dashboard — real-time conversation monitoring, stat cards, and Chart.js analytics (daily trend, monthly comparison, weekly breakdown)
- Subscription billing — GoPay, OVO, DANA, BCA Virtual Account via Midtrans; three plans in Rupiah (Free, Starter, Pro)
- Discount system — promo codes with per-plan applicability, validity windows, and max-use limits
- Quota notifications — real-time alerts at 80% and 100% usage, monthly reset notification, plan upgrade confirmation
- Dark mode — full token-based theme switching via
next-themes - KUN customization — accent color picker, language setting (ID / EN / both), quick replies, system prompt addendum
- Talking to KUN — streaming responses token-by-token via Server-Sent Events; feels instant, no page reload
- Markdown rendering — KUN's formatted answers rendered via
react-markdown - Human handoff request — customers can ask KUN to connect them with a person; staff is notified instantly via dashboard
- Embeddable widget — KUN works inside any website via a single
<script>tag
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, Server Actions, SSE) |
| Language | TypeScript 5 — strict mode, zero any |
| UI | React 19, Tailwind CSS v4, shadcn/ui, Framer Motion |
| Database | Neon (serverless PostgreSQL), Drizzle ORM v0.45, pgvector |
| Auth & Multi-tenancy | Clerk Organizations |
| AI | OpenAI gpt-4o-mini (chat) + text-embedding-3-small (RAG embeddings) |
| Vector Search | pgvector cosine similarity, HNSW index, scoped per org_id |
| File Storage | AWS S3 (presigned URLs, 5-min expiry) + CloudFront CDN |
| Payments | Midtrans — GoPay, OVO, DANA, BCA VA, QRIS, Credit Card |
| Real-time | Pusher (private dashboard channels) + SSE (customer chat stream) |
| Caching & Rate Limiting | Upstash Redis — 4 rate limiters + cache-aside pattern |
| Resend + React Email | |
| Analytics | PostHog (server-side, 7 tracked events) |
| Error Tracking | Sentry (PII scrubbed before send) |
| Validation | Zod v4 |
| Testing | Vitest + React Testing Library (266 tests) + Playwright E2E (24 tests) |
| CI/CD | GitHub Actions — typecheck → lint → test → build → E2E |
| Deployment | Vercel (built-in cron, auto-deploy on push) |
Shared database with org_id as the tenant isolation key — on every table, every query, enforced at the application layer via requireOrg(). IDOR protection via AND org_id = $orgId on all lookups. orgId always sourced from Clerk server session, never from client request body.
Customer asks KUN a question
→ OpenAI text-embedding-3-small (1536 dimensions)
→ pgvector cosine similarity search (WHERE org_id = $orgId LIMIT 5)
→ System prompt built from top 5 chunks + KUN identity rules
→ OpenAI gpt-4o-mini streams KUN's reply
→ SSE ReadableStream → token-by-token to browser
Business owner uploads file
→ AWS S3 (presigned URL, client uploads directly)
→ API notified → downloads from S3
→ pdf-parse / plain text extraction
→ chunked (~500 tokens, 50 token overlap)
→ batched embedding (10 chunks/batch via batchedAsync)
→ bulk INSERT into chunks table with org_id + vector embedding
→ document status updated to "ready"
→ Pusher event fires → dashboard updates live
Every external paid service has a mock mode — OpenAI, S3, Midtrans, Pusher, Resend. The entire product can be built and tested without spending a cent. Mode switching is a single env variable change per service; the caller never checks the mode directly.
proxy.ts route guards → requireOrg() tenant isolation → Zod input validation → prompt injection detection (15 regex patterns) → 4 Upstash rate limiters → Clerk (Svix) + Midtrans (SHA512) webhook verification → S3 file validation (MIME + magic bytes) → atomic plan limit enforcement → security headers + CSP → slug enumeration protection → Sentry PII scrubbing.
Manual subscription management via Vercel Cron: daily renewal check → Midtrans charge creation → payment link via Resend → past_due after 3 days → suspended after 7 days → reactivated on payment webhook settlement. Full idempotency via processedWebhooks table.
kundesk/
├── app/
│ ├── (marketing)/ ← Landing page + legal pages
│ ├── (auth)/ ← Clerk auth flows
│ ├── (dashboard)/ ← Protected tenant dashboard
│ │ ├── page.tsx ← Overview (stat cards + charts)
│ │ ├── conversations/ ← Live conversation management + human handoff
│ │ ├── documents/ ← Document upload + processing status
│ │ ├── chatbot/ ← Chatbot configuration
│ │ ├── analytics/ ← Deep analytics charts
│ │ ├── billing/ ← Subscription + payment history
│ │ ├── widget/ ← Embed code + QR code generator
│ │ └── settings/ ← Org settings
│ ├── (chat)/
│ │ └── [orgSlug]/ ← Public chat interface per tenant
│ └── api/
│ ├── chat/ ← SSE streaming endpoint + RAG
│ ├── documents/ ← Upload trigger + processing pipeline
│ ├── conversations/ ← Takeover, dismiss, human reply routes
│ ├── webhooks/
│ │ ├── midtrans/ ← Payment notification + subscription state machine
│ │ └── clerk/ ← Org/user sync
│ ├── cron/ ← Renewal, reset-usage, past-due, retention
│ ├── widget/ ← Embeddable JS snippet
│ └── health/ ← Health check (SELECT 1)
├── components/
│ ├── dashboard/ ← All dashboard UI components
│ ├── chat/ ← Public chat widget components
│ ├── landing/ ← Marketing page sections
│ └── ui/ ← shadcn/ui components
├── lib/
│ ├── db/ ← Drizzle client, schema, queries
│ ├── ai/ ← Embedding, RAG, streaming
│ ├── aws/ ← S3 helpers
│ ├── redis/ ← Upstash client + rate limiters + cache
│ ├── midtrans/ ← Payment client + subscription helpers
│ ├── email/ ← Resend client + templates
│ └── env.ts ← Startup env validation (throws on missing vars)
├── helpers/ ← Pure functions: chunking, formatting, validation
├── types/ ← All TypeScript types (never defined inline)
├── stores/ ← Zustand stores
├── hooks/ ← Custom React hooks
└── e2e/ ← Playwright E2E specs
| Table | Purpose |
|---|---|
orgs |
Tenants — synced from Clerk webhooks |
chatbots |
Per-org chatbot config (language, accent, quick replies, system prompt) |
documents |
Uploaded files with processing status |
chunks |
RAG knowledge base — text chunks + pgvector embeddings |
conversations |
Customer sessions with handoff state |
messages |
All chat messages (user, assistant, human_agent roles) |
processedWebhooks |
Idempotency table — prevents double-processing on retries |
notifications |
In-dashboard notification feed (8 types) |
payments |
Payment history per org |
promoCodes |
Discount codes with validity, max uses, per-plan applicability |
| Job | Schedule | Purpose |
|---|---|---|
/api/cron/renewal |
Daily 08:00 WIB | Find orgs due for renewal, create Midtrans charge, send payment link |
/api/cron/reset-usage |
1st of month | Reset messagesUsed to 0, fire quota_reset notification per org |
/api/cron/past-due |
Daily 09:00 WIB | Suspend orgs unpaid after 7 days |
/api/cron/retention |
Daily 03:00 WIB | Delete messages older than 90 days |
Vitest unit + component tests: 266 tests — all passing
Playwright E2E tests: 24 tests — all passing
E2E coverage:
- Public chat flow (SSE streaming, rate limiting, prompt injection)
- Dashboard authentication and org activation
- Document upload and processing pipeline
- Billing webhook (Midtrans mock settlement)
- Human handoff (takeover, return to AI, dismiss pending)
All external services have a
mockmode. You can run the full product with only Clerk and Neon credentials.
1. Clone and install
git clone https://github.com/thekevinkun/kundesk.git
cd kundesk
npm install2. Set up environment variables
Copy .env.example to .env.local and fill in the required values:
# Required (always real — both have generous free tiers)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
CLERK_WEBHOOK_SECRET=
DATABASE_URL= # Neon connection string
# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
# All other services default to mock mode
KUNDESK_AI_MODE=mock
KUNDESK_EMBEDDING_MODE=mock
KUNDESK_STORAGE_MODE=mock
KUNDESK_PAYMENT_MODE=mock
KUNDESK_REALTIME_MODE=mock
KUNDESK_EMAIL_MODE=mock
# Upstash Redis (required even in mock mode — rate limiters always call Redis)
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=3. Run database migrations
npm run db:generate
# Then apply the generated SQL manually via Neon SQL editor4. Start the dev server
npm run dev5. Run tests
npm run test # Vitest unit tests
npm run test:e2e # Playwright E2E (requires .env.e2e)| Plan | Price | Messages/month | Documents | Chatbots |
|---|---|---|---|---|
| Free | Rp 0 | 100 | 3 | 1 |
| Starter | Rp 149.000 | 1.000 | 20 | 1 |
| Pro | Rp 399.000 | 10.000 | Unlimited | 3 |
Payment methods: GoPay, OVO, DANA, BCA Virtual Account, QRIS, Credit Card — all via Midtrans.
| Channel | Free | Starter | Pro |
|---|---|---|---|
| QR Code + Public Chat Link | ✓ | ✓ | ✓ |
| Web Embed Widget | — | ✓ | ✓ |
| WhatsApp Integration | — | — | Planned |
Built by Kevin Mahendra — self-taught full stack developer based in Samarinda, East Kalimantan, Indonesia.
This is a product under the Kun Borneo brand, alongside Kun Bookshop — a full-stack bookshop with AI chatbot, OpenAI tool calling, SSE streaming, and Stripe checkout.
MIT



