Single-file guide Β· v3 Β· Users + Developers + Interviews

Chat your way.
Learn how.

Kivo blends WhatsApp-style DMs & groups with Discord-style Spaces & channels, plus deep customization. Student-built, fully usable realtime chat β€” Next.js 16 + Express 5 + Socket.IO + MongoDB, JavaScript only, no microservices. This page teaches everyone: curious users, new teammates, and interviewers.

Version 3.1 Β· Sept 2026⏱ ~35 min readπŸ“± Fully responsive● MVP + Plus + Calls + Polls + Nearby
β–Ά Start learning Jump to 100+ Q&A πŸ“± I just use Kivo
πŸ“±I'm new / I use Kivo

Plain-language manual: chats, Spaces, themes, Plus, safety. No code.

πŸ—ΊοΈShow me flows

Signup β†’ first DM, groups, Spaces, Plus UPI, offline sends.

πŸ› οΈI build / review code

File tour, API + socket examples, schemas, env, deploy.

🎀Interview prep

100+ Q&A, 60-sec pitch, system-design follow-ups.

3DMs Β· Groups Β· Spaces β†’ Channels. One identity.
10Themes + studio, wallpapers, bubble styles.
13Mongo collections. Messages never embedded.
15m / 7dJWT access + session-backed httpOnly refresh.
β‚Ή49/moPlus via UPI + 12-digit UTR, 24h review, 30d grant.
Start here

How to use this guide

In plain words: this one page replaces README + docs + PRD + tech-stack + a mock interview. Pick your path β€” everything links together, and every technical claim names the real file or endpoint so you can verify it in the repo.
πŸ“± Everyday users (10 min)

Read User manual β†’ Journeys β†’ Everyday FAQ. You'll be able to chat, join Spaces, theme, and stay safe.

πŸ› οΈ Developers (20 min)

Read Message journey β†’ Architecture β†’ Data β†’ Realtime β†’ Auth β†’ Run & deploy. Then browse code by file paths.

🎀 Interviews (15 min)

Memorize the 60-sec pitch, skim decisions, then drill Q&A bank with search + categories.

What you'll be able to explain

  • What Kivo is, who it's for, and what it deliberately isn't (not Slack).
  • How a message travels (optimistic UI β†’ JWT β†’ Zod β†’ Mongo β†’ Socket β†’ receipts β†’ push β†’ offline outbox).
  • Why Mongo is truth and sockets only announce; why no Redis/microservices/TS β€” yet.
  • Auth that actually revokes, presence that doesn't spam, threads that stay quiet, Plus that the client can't fake.
Overview

What is Kivo?

In plain words: one app for private chats (like WhatsApp) and communities (like Discord), with unusually deep theming. Built by one student (Ayush) to learn full-stack properly β€” but real people can use it daily. Live at kivo.usersynax.dev, open source user-synax/Kivo.
πŸ“± DMs & Groups

Private 1:1 DMs (unique per pair via $all) + small groups (you + β‰₯2 friends, admins, system chips, last-admin protection).

πŸ›°οΈ Spaces & Channels

Discord-like servers: text + announcement channels, roles owner β†’ admin β†’ moderator β†’ member, Discover + 7-day rotating invites.

🎨 Customization

10 presets + personal studio + per-Space palettes + per-chat look + wallpapers + bubbles + profile effects. Beyond light/dark.

Principles

Fast & realtime Β· Simple despite rich Β· Personal & customizable Β· Clean responsive UI (bottom tabs on phones, icon rail on desktop) Β· Privacy + authorization foundational β€” never trust the client Β· One identity across DMs/Groups/Spaces Β· No premature microservices.

Pages (what users actually open)

URLWhoWhat
/AnyoneLanding (features, customization, security, roadmap). Logged-in β†’ /app.
/signup Β· /loginGuestsCreate account (instant, no OTP) / login with email or username, or Google/GitHub.
/forgot-password Β· /reset-password?token Β· /verify-email?tokenAnyone1h reset link (kills all sessions) + 24h verify link (API-only, no auto-email today).
/app Β· /app/profileSigned inChat dashboard (lists + chat + drawers) Β· read-only profile + logout.
/u/:usernameAnyonePublic shareable profile: badge, flag, status, bio, socials, graph, Wave, QR share. Wears owner's theme.
/plus Β· /docs Β· /learn Β· /authorAnyonePricing + UPI claim + status Β· how-to guide Β· visual story (this page is the deep version) Β· author.
/admin Β· /admin/dashboardAdminsSeparate login; users, bans, groups/spaces, Plus queue, audit log.

Repository map + 4-file rule

kivo/ β”œβ”€β”€ frontend/ Next.js 16 App Router Β· React 19 Β· JS only Β· Bun β”‚ β”œβ”€β”€ app/ / Β· (auth)/ Β· /app Β· /u/[username] Β· /plus /learn /docs /admin … β”‚ β”œβ”€β”€ components/ dashboard(chat-panel, message-bubble, sidebar, icon-rail, β”‚ β”‚ nearby-screen, appearance-screen, thread-panel, chat-media-drawer) β”‚ β”‚ spaces Β· notifications Β· profile Β· calls Β· status Β· polls Β· ui Β· admin β”‚ β”œβ”€β”€ lib/ api.js auth.js cache.js theme.js chat-style.js drafts.js outbox.js β”‚ β”‚ push.js sound.js polls.js status.js custom-emoji.js plus.js … β”‚ └── Design.md Β· globals.css (tokens) Β· public/kivo.html ← this guide, served β”œβ”€β”€ backend/ Express 5 + Socket.IO 4 + Mongoose 9 Β· JS only Β· Bun (nodemon dev) β”‚ └── src/ app.js (builds app) Β· server.js (real entry; root server.js = legacy stub) β”‚ β”œβ”€β”€ modules/ auth users friends conversations messages spaces notifications β”‚ β”‚ push attachments search link-preview calls status emoji plus admin … β”‚ β”‚ each: *.routes.js β†’ *.controller.js β†’ *.service.js β†’ *.validation.js β”‚ β”œβ”€β”€ models/ User Session Conversation Message FriendRequest Space β”‚ β”‚ Notification PushSubscription Status CustomEmoji PlusRequest AdminActionLog β”‚ β”œβ”€β”€ middleware/ auth adminAuth rateLimiter errorHandler Β· lib/ plus appwrite β”‚ β”‚ attachments email emoji-image totp Β· socket/ index.js io.js Β· config/ jobs/ utils/ └── README.md Β· docs.md Β· PRD.md Β· TECH-STACK.md Β· kivo.html (repo copy of this guide)
For everyone Β· no code

πŸ“± Everyday user manual

In plain words: if you only read one section, read this. Everything below is clickable in the real app β€” no technical knowledge needed.

1 Β· Create account & log in (2 minutes)

  1. Open the site β†’ Sign up. Enter display name, username (3–30, letters/numbers/_), email, password (β‰₯8). You land in /app instantly β€” no OTP, no waiting.
  2. Or tap Continue with Google / GitHub β€” a new account is made for you (email pre-verified, provider photo as avatar). Same email as an existing account? They're auto-linked, nothing lost.
  3. Log in later with email or username + password, or the provider button. OAuth-only accounts (no password) tell you to use the provider button instead.
  4. Forgot password? Forgot password β†’ email β†’ 1h link β†’ new password. All devices are signed out β€” log in again.

2 Β· Layout: desktop vs phone

πŸ–₯️ Desktop / tablet

Far-left icon rail: Chats Β· Nearby Β· Status Β· Groups Β· Spaces Β· Emoji Factory Β· Settings, avatar at bottom. Header has bell πŸ””, search Ctrl+K, New menu. Center = chat; right = profile/group/Space panel on wide screens. Unread dots sit on rail icons.

πŸ“± Phone

Bottom tabs: Chats Β· Status Β· Groups Β· Spaces Β· Menu. Menu holds Nearby, Discover, profile, Appearance (full-screen themes), Settings β€” each a pushed screen with Back. Edge-swipe goes back; notches respected.

3 Β· Friends, Find & Nearby

  1. New β†’ Friends β†’ Find: search username/email/name (300ms debounce). Results are square cards β€” tap View profile or Send Request.
  2. Sending? Choose with welcome message (≀280 chars, shown before accept β€” be kind, only they see it) or without. Can't friend yourself, blocked users, or duplicates.
  3. Requests tab: accept/decline; welcome shows as a Quote bubble. Sender gets a notification (80-char snippet).
  4. Friends tab β†’ Message opens/creates the DM. Unfriend from any profile β€” removes both sides live; history stays until you Remove-from-list.
  5. Nearby (rail / Menu β†’ Nearby users, full-screen): first open shows a consent sheet. Tap Share location (one-shot, not tracking) β†’ see people by fuzzed distance only (250 m / 1.2 km β€” never exact). Radius chips 1/2/5/10 km. Off in Settings β†’ Privacy β†’ Nearby discovery (hides you + clears location).

4 Β· Chat like a pro

DoHow
SendType β†’ Enter (Shift+Enter = newline). ≀4000 chars (Plus 8000). Appears instantly; retry on fail.
Reply / ReactRight-click / long-press β†’ Reply (quote preview, X cancels) Β· React Β· double-click/long-press = ❀️ Β· 270+ picker.
Edit / DeleteOwn messages only. ↑ on empty box edits your last message. Edited label; delete blanks but keeps order.
Forward / Pin / SaveForward β†’ pick chats (pill "Forwarded from @user", no caption). Pin β†’ banner under header (max 10). Save β†’ bookmark icon panel, jump anytime.
ThreadsMenu β†’ Thread β†’ side panel. Quiet by design: no badge/bell (mentions still ping). "N replies" chip reopens.
MentionsType @ β†’ pick participant β†’ highlighted. They get notified even if category muted.
Voice / FilesHold mic β†’ release sends (slide up cancels). Paperclip or paste screenshots β†’ ≀10 files ≀30 MB. Images β†’ lightbox (arrows/download); docs β†’ cards; grid icon β†’ Media/Files/Links gallery + jump.
Receiptsβœ“ sent Β· βœ“βœ“ delivered Β· blue βœ“βœ“ all read. Tap ticks β†’ Seen-by card (Read Β· time / Delivered per person).
Stay orientedDay pills (Today/Yesterday/…) Β· big emoji for 1–3 emoji-only Β· "N new" pill when scrolled up Β· scroll up loads history Β· right-click chat β†’ Mark unread.
Mobile gestureSwipe bubble right (~45px) to reply. Works with scroll via direction lock.

5 Β· Groups & Spaces

πŸ‘₯ Groups (private circles)
  1. Groups β†’ New group β†’ name (≀50) + optional photo + β‰₯2 friends.
  2. You become first admin. Settings: rename/photo, add/remove, promote/demote. Last admin can't be removed.
  3. Joins/leaves show as centered chips, live for all.
🏠 Spaces (communities)
  1. Spaces β†’ New β†’ name/desc/category/banner/avatar β†’ auto #general. You own it.
  2. Text channels = all post; announcement = admins only. Admin+ manages channels (last protected).
  3. Roles owner→admin→moderator→member. Owner-only: promote admin, delete Space.
  4. Discover β†’ search/category β†’ Join (public). Private = hidden, invite link/code only (7-day rotating, Space settings β†’ Privacy & invites; /app?join=CODE deep-link).

6 Β· Make it yours + stay notified + Plus

  1. Appearance (full-screen): 10 themes switch live, no reload. Studio recolors accent + canvas tone β†’ Save to my account (follows you). Chat look: wallpaper (Plain/Dots/Grid/Lines/Bubbles/Wash) + bubbles (Rounded/Pill/Squared/Outlined-mine). Per-chat palette icon overrides; Spaces can set their own look for members.
  2. Profile (rail avatar / Menu): name, handle, bio 280, status 60 + emoji chip (32 + 6 vibes), photo 4MB, 10 frames, banner (curated GIFs; Plus: your own ≀8MB), effects (Plus: glow/gradient-name/aura), country flag, GitHub graph, X/Instagram/YouTube/website chips, pronouns, status auto-clear, privacy toggles.
  3. Public page /u/you: share + QR. Visitors Wave πŸ‘‹ (20s cooldown β†’ notification deep-linking to them), add/message/block.
  4. Bell πŸ””: DMs/groups/Spaces/mentions/requests/waves. Click β†’ jump + mark read. Settings β†’ Notification preferences per category (Space msgs OFF default; mentions break through). Sounds per category + master + preview (synthesized, no downloads). First bell-open asks push permission β†’ offline pings even when closed. Quiet automatically when you're already viewing that DM.
  5. Install (PWA): Chrome/Edge address-bar install icon Β· Android Add to Home Screen Β· iPhone Share β†’ Add to Home Screen. Recent chats paint instantly offline; offline texts queue and auto-send on reconnect.
  6. Plus β‚Ή49/mo: /plus β†’ pay published UPI ID β†’ paste 12-digit UTR β†’ approved within 24h β†’ 30 days Plus (custom banner/effects/emoji, bigger limits). Status cards show pending/rejected/expired.
  7. Safety: Block from any DM/profile (removes friendship, hides chat, server-enforced). Manage in Settings β†’ Blocked users (one-tap unblock). Remove-from-list deletes a DM/group for everyone (confirm twice β€” permanent). Admins can ban rogue accounts.
Flows

πŸ—ΊοΈ Five journeys, end to end

β‘  Signup β†’ first DM (3 min)
  1. Signup β†’ land /app.
  2. New β†’ Friends β†’ Find β†’ search friend β†’ Send Request (+ "hey, it's me!").
  3. They Accept β†’ Friends β†’ Message β†’ type β†’ Enter β†’ βœ“βœ“ blue. Right-click β†’ Pin "coffee Saturday".
β‘‘ Squad group
  1. Groups β†’ New group "Project X" + photo + 2 friends β†’ Create.
  2. Chat, @mention the lead (overrides mute), start a Thread on the plan message.
  3. Promote a co-admin; leave anytime (last admin protected).
β‘’ Space community
  1. Spaces β†’ New "Design Club" (category Design) β†’ #general ready.
  2. Space settings β†’ add #inspiration (text) + #news (announcement).
  3. Private? Privacy & invites β†’ New link (7d) β†’ share /app?join=CODE. Public? It appears in Discover.
  4. Give it a look (accent + wallpaper) β€” members see it in channels.
β‘£ Plus UPI
  1. /plus β†’ pay β‚Ή49 to shown UPI ID in any UPI app β†’ copy 12-digit UTR.
  2. Paste UTR β†’ claim filed (pending card). Admin approves ≀24h β†’ 30d Plus.
  3. Upload custom banner + glow effect + personal emoji :mylogo:.
β‘€ Offline hero
  1. Enter a tunnel, keep typing β†’ messages queue in outbox (banner "You are offline").
  2. Reload β€” drafts + queue survive (IndexedDB).
  3. Back online β†’ queue flushes in order + gap-fill pulls what you missed. Nothing lost.
Core mental model

The journey of a message

In plain words: your words paint instantly, get verified by the server, then everyone is told at once. If anyone was offline, the system catches them up quietly.
Client β†’ POST /api/v1/conversations/:id/messages β†’ Auth(JWT) β†’ Zod validate β†’ Authorize membership β†’ Rate-limit 40/min β†’ MongoDB write β†’ Socket.IO emit message:new β†’ recipients β†’ delivered/read acks β†’ Notifications (bulk insertMany + fire-and-forget push) β†’ Offline? outbox flush Mantra: instant paint β†’ verified truth β†’ instant delivery β†’ quiet catch-up.

Code refs: backend/src/modules/messages/messages.service.js (createMessage) Β· backend/src/modules/notifications/notifications.service.js (createForMessage) Β· backend/src/socket/io.js (emitToConversation) Β· frontend/lib/outbox.js + components/outbox/outbox-flusher.jsx Β· frontend/lib/cache.js.

Stack

Tech stack + why each choice

PieceTechnologyWhy this (not the alternative)
Web appNext.js 16 + React 19File routes + SSR/SEO (landing/docs/profiles) + rewrites. Not plain Vite: needs routing, metadata, sitemap/robots.
StylingTailwind v4 + shadcn/ui + Base UIUtilities + accessible primitives. No extra component lib β€” native + shadcn is enough.
MotionMotion (Framer Motion)Staggers, dropdowns, panels. Always honors prefers-reduced-motion.
ServerNode + Express 5Boring + productive. Not Nest/Fastify: no decorator/DI ceremony for a solo MVP. Real entry src/server.js (src/app.js builds app; root server.js = legacy stub).
RealtimeSocket.IO 4Rooms + auto-reconnect + fallback beat raw WS for an MVP. No polling anywhere.
DatabaseMongoDB Atlas + Mongoose 9Flexible chat docs + geospatial (2dsphere) + text index. Not Postgres: schema fluidity + embedded members/channels fit documents.
ValidationZod 4Every body/query/param server-side. Strictness without TypeScript.
AuthJWT + bcryptjs 12Short tickets + one-way hashes. Sessions table gives real revocation.
FilesAppwrite Storage + Multer memorySeparate buckets (avatar/attachments/emoji). Not S3: generous free tier + preview URLs. Never blobs in Mongo.
Pushweb-push VAPIDNative offline pings. Not Appwrite Messaging: standard + already integrated with SW.
EmailNodemailer Gmail SMTPReset/verify links. Fire-and-forget so auth never blocks on mail.
CallsLiveKit Cloud SFUChat servers are bad at media β€” delegate audio/video to an SFU; deterministic rooms.
ImagessharpEmoji β†’ webp (gif kept for animation), 2MB cap.
RuntimeBun (nodemon dev)Fast install/run. bun run dev/start.
Offline cacheidb-keyvalBrowser DB: lists + last-50/chat, SWR instant paint.
LintBiomebun run lint. TS intentionally out β€” JS-only is a project constraint.

Monorepo, no microservicesJS onlyRedis planned, NOT used

Architecture

Architecture, API & file tour

Browser ──HTTPS──▢ Next.js ──REST──▢ Express ──▢ MongoDB β”‚ β”‚ └────── Socket.IO (realtime) β—„β”€β”€β”€β”€β”€β”€β”€β”˜ Appwrite (files) Β· VAPID (push) Β· LiveKit (media) REST saves/fetches. Socket announces. DB write FIRST, emit SECOND.

Networking rule (interview favorite)

REST uses relative /api/… β†’ Next.js rewrites (next.config.mjs) to BACKEND_URL (default http://localhost:4000), staying same-origin so sameSite=strict httpOnly cookies flow without CORS. Socket.IO dials NEXT_PUBLIC_API_URL directly β€” rewrites can't proxy WebSocket upgrades. Config: frontend/lib/api.js (fetch + auto-refresh) Β· components/socket-provider.jsx.

Request lifecycle (example: send message)

POST /api/v1/conversations/:id/messages
Authorization: Bearer <15m JWT>
{"content":"hey!","replyToMessageId":null}

routes.js β†’ auth() β†’ rateLimiter(send 40/min) β†’ controller
  β†’ service: Zod validate β†’ membership/ban/block check
  β†’ getRequesterPlan() limits β†’ Message.create()
  β†’ Conversation.lastMessageAt touch
  β†’ notifications.createForMessage() [bulk insertMany + async push]
  β†’ io.emitToConversation(id,"message:new",msg) β†’ 201

Module mounts

MountWhat lives there
/api/v1/authregister login refresh logout logout-all verify-email resend forgot/reset + OAuth providers/start/link-url/callback
/api/v1/usersme, profile/avatar/banner, public by username, search 30/min, blocked, privacy, location 10/min, nearby 30/min
/api/v1/friendsrequest (+welcome 280, 20/h) accept decline list remove + requests feed
/api/v1/conversationsDM/group CRUD, members/admins, read/unread, look PATCH /:id/look, permanent DELETE /:id, messages, pinned, threads
/api/v1/messagesedit delete pin save/unsave saved-feed reactions (receipts via readBy/deliveredTo)
/api/v1/spacesCRUD, discover (public only), join, join-by-code, invites, members/roles, channels
/api/v1/notificationslist unread-count mark-read preferences wave POST /:id/wave 20s
push · attachments · search · link-preview · calls · status · emoji · plusVAPID subscribe · Multer→Appwrite · Ctrl+K unified · SSRF-guarded og 1h cache · LiveKit token/config/status · 24h text+photo · emoji CRUD · UPI claim/status
/api/adminlogin stats users ban/unban/plan POST /users/:id/plan plus-queue approve/reject groups/spaces delete
GET /health{success:true, data:{status:"ok"}}

Errors: {success:false, error:{code,message}} β€” prod never leaks stacks/secrets/paths (middleware/errorHandler.js). Frontend tour: app/ routes Β· components/dashboard/* shell/panels Β· lib/theme.js truth Β· lib/cache.js outbox.js drafts.js.

Data

Data models β€” MongoDB is truth

In plain words: everything important lives in Mongo. If the server restarts, nothing is lost. Messages get their own table (never stuffed inside chats) so history can grow forever.
User 1──* Session Β· User *──* Conversation *──* Message (threadId/pins/saves) User *──* FriendRequest (directed) Β· Space{embedded members/channels} 1──* Conversation User 1──* Notification Β· PushSubscription Β· Status Β· CustomEmoji Β· PlusRequest
ModelHolds & key rules Β· file
User models/User.jsemail/username/displayName, bio280, status60, statusEmoji8, pronouns20, statusExpiresAt, avatarStyle(10), avatarUrl+banner (+FileIds select:false), country→flag, github/x/instagram/youtube/website, verified/showBadge, plan + planExpiresAt, profileEffect, usernameColor hex, passwordHash nullable (OAuth-only), googleId/githubId sparse-unique + verified flags, role, blockedUsers, lastActiveAt, verify/reset hashes, 2FA secret + hashed backups, isBanned, location Point + 2dsphere + discoverableByNearby=true, notificationPreferences (Space OFF), appearance{accent,tint,wallpaper,bubbleStyle}
SessionOne row per refresh token, TTL expiry. Delete = logout. Powers logout-all/reset-all/ban.
Conversation models/Conversation.jsdm/group/space_channel, participants, admins, createdBy, spaceId+channelId, appearance look, lastMessageAt. Duplicate DM via service $all (no pairwise unique index exists in Mongo).
Message models/Message.jsconv+sender, content ≀4000 blanked on delete, type text/system/poll, replyTo, threadId null=timeline, reactions, deliveredTo, readBy{user,at}, savedBy, mentions, attachments{fileId,bucketId,name,mime,size,kind,url}, audioDuration, isEdited+history(10), scheduledAt+status, forwardCount, forwardedFromId/Name, pinnedAt/By, poll{question140, opts 2–8, multi, anon, expiry, closed, total}, isDeleted. Indexes per read pattern + text index (threads excluded).
FriendRequestfrom→to + pending/accepted/declined + welcomeMessage280 + at. Accepted row = edge. Directed-unique.
Space models/Space.jsname/slug-unique/desc/category(10)/avatar/banner, owner, members[{user,role,joined}], channels[{name,slug,desc,type,by}], appearance, visibility public/private, inviteCode+inviteExpiresAt 7d (never in payloads).
Notificationrecipient/sender/senderUsername, types dm/group/space/mention/friend_request/friend_accept/space_invite/wave, flags inApp/pushDelivered/pushError.
Statususer text280 background(6) media{file,bucket,url,kind,mime,size,name} viewers[] expiresAt 24h (indexed, no TTL β€” hourly sweep deletes file then doc).
CustomEmojiname 2–32, spaceId, ownerId, by, url/file/bucket/animated. Sparse-unique per scope.
PlusRequestuser utr-12-digit amount4900 status pending/approved/rejected/expired expiresAt 24h reviewedAt note.
PushSubscription Β· AdminActionLogVAPID endpoint+keys (unique) Β· ban/unban/grant_plus/revoke_plus/delete_* + IP + time.

Never embed messagesPins/saves = fieldsThreads reuse Message

Realtime

Socket.IO: rooms, presence, receipts

In plain words: one always-open wire per device. The server puts you in a "room" per chat/Space and shouts into the right room when something happens. Online dots only go to people who share a chat with you β€” not the whole internet.
πŸ”Œ Connection

JWT handshake (same secret as HTTP); banned rejected. Auto-join conversation:{id} + space:{id}. socket/io.js helper emitToConversation. File socket/index.js.

🟒 Presence (scoped)

Map<user,Set<socket>> single-instance. Peers = union of conversations+spaces (getRelevantPeerIds). 12s grace (pendingOffline) stops flicker. presence:snapshot fixes late joiners. Offline = lastActiveAt β†’ "active X ago" (lib/last-active.js).

⌨️ Typing

typing:start/stop {conversationId} β†’ room excl. sender. No DB write. Client: chat-panel.jsx.

βœ… Receipts

message:delivered {messageId} β†’ $addToSet β†’ delivery-updated. Read = anchor {userId,upToMessageId,upToCreatedAt,readAt} β†’ client groups Read/Delivered. Tap ticks β†’ portaled Seen-by (message-bubble.jsx).

πŸ” Gap-fill

Reconnect β†’ refetch list (single agg) + after=newestId β†’ merge into cache.js. No full download.

πŸ“ž Ring only (media = LiveKit)

call:ring/accept/decline/end β†’ accepted/declined/ended/missed/failed. 30s activeRings β†’ missed chip+bell (calls.service recordMissedCall). 3s spam guard. Blocked β†’ CALL_BLOCKED.

// client minimal (frontend)
import { io } from "socket.io-client";
const s = io(process.env.NEXT_PUBLIC_API_URL, { auth: { token } });
s.on("message:new", m => m.threadId ? openThread(m) : appendChat(m));
s.emit("conversation:focus", { conversationId }); // suppresses its DM notifs
s.emit("message:delivered", { messageId });
EventDirMeaning
message:new/edited/deleted/reaction/pin-updatedS→RoomThread replies reuse with threadId → panel vs chip refresh
message:read/unread/delivery-updatedBothAnchor receipts + unread badge + "New messages" line
conversation:focus/blurC→SViewing DM skips its notification (isUserFocusedOnConversation)
conversation:member-added/removed/updated/admin-changed/removedS→RoomGroup live; removed also per-user on permanent delete
space:updated/member-*/channel-*/deleted/joined/removedS→SpaceSpace/channel lifecycle
notification:new · friend:removed · emoji:new/deleted · status:new/deleted/viewedS→UserBell · live unfriend · emoji sync · status feed
Auth & security

Identity, trust, and guards

In plain words: short-lived tickets prove who you are; a hidden cookie keeps you logged in; the server can revoke either instantly (logout everywhere, password reset, ban). Every input is validated; every action checks permission.
⚑ Instant signup

name + username ^[a-zA-Z0-9_]+$ 3–30 + email + password β‰₯8 β†’ bcrypt12 β†’ user + session β†’ access + httpOnly cookie β†’ /app. Duplicate β†’ CONFLICT. GuestGate/AuthGate (components/auth-guard.jsx).

🎫 Sessions that revoke

Access 15m Bearer + refresh strict-cookie backed by Session TTL. POST /refresh-token mints access, no rotation (race-safe). Single-flight + 60s early refresh (lib/api.js). Logout one / logout-all / reset-all / ban-all = delete docs.

πŸ“§ Verify + reset

Verify 24h SHA-256 link + resend 1/min (API-only today). Forgot 5/5min always-success (no enumeration) 1h; reset 10/5min + kill all. Gmail SMTP fire-and-forget (lib/email.js, FRONTEND_URL links).

πŸ”’ 2FA TOTP

Optional. In-repo lib/totp.js RFC6238 SHA-1/30s constant-time; QR (qrcode); pending secret; bcrypt-hashed single-use backups; 5m ticket β†’ session; password to disable (OAuth-only exempt). UI: dashboard/two-factor-section.jsx.

πŸ”‘ Google + GitHub

GET /oauth/providers gates buttons. Start 302 signed-state β†’ callback creates (pre-verified, derived username, provider avatar if unset) or auto-links by email. link-url embeds userId. PROVIDER_ALREADY_LINKED; sparse-unique; OAuth-only null password β†’ OAUTH_ONLY_ACCOUNT. Both linked β†’ native verified. UI: auth/OAuthButtons.jsx, oauth/callback/page.jsx.

πŸ›‘οΈ Never trust client

Zod all inputs. Membership / sender-only / rank (owner>admin>moderator>member) / owner-only promote+delete / last-owner+admin guards. Helmet, CORS allowlist, body limits, trust proxy, allow-list uploads + old-file delete, no prod leaks.

Rate limits β€” in-memory fixed window (middleware/rateLimiter.js)

Per-user else IP Β· X-RateLimit-* + Retry-After Β· 5-min unref sweep Β· per-process (shared store only if multi-instance).
register 5/h/IP Β· login 10/15m Β· 2FA code 10/5m/IP Β· setup 5/5m Β· verify-code 5/60s Β· refresh 30/60s Β· forgot 5/5m Β· reset 10/5m Β· resend 1/m Β· send 40/m Β· edit 20/m Β· reactions 60/m Β· friend-req 20/h Β· user-search 30/m Β· nearby 30/m Β· location 10/m Β· space/channel 10/h Β· uploads 10/m Β· search 30/m Β· link-preview 30/m Β· call-token 20/m Β· status 10/day + feed 30/m + view 60/m Β· plus-claim 5/day Β· admin-login 5/15m Β· oauth-link 10/60s.

Security checklist (say this in interviews)

Transport & cookies

Helmet, CORS allowlist, httpOnly + sameSite strict cookies, trust proxy IPs.

Input & files

Zod everywhere; MIME allow-lists + 30MB caps + bucket extension lists; old files deleted on replace.

Abuse & privacy

Rate limits, block/ban guards on DM/wave/search/nearby/calls, fuzzed location, SSRF-guarded previews.

Features

How each feature actually works

In plain words: one composer + one bubble menu everywhere. Differences are server rules (who can post/pin/promote) plus quiet-by-design threads and server-checked Plus limits.

πŸ‘₯ Friends Β· Find Β· Nearby

Request by username/email/name + optional welcome 280 (stored + 80-char notif snippet β†’ Quote bubble). Blocked both-ways β†’ BLOCKED_USER. Directed-unique; reverse auto-accepts. Remove = shared edge deleted both sides + friend:removed live; history stays. Find: debounced 30/min, blocked/banned filtered, square cards (profile-preview-card.jsx) + modal with/without message. Nearby full-screen (nearby-screen.jsx like Appearance; rail + Menu β†’ Nearby; FriendsModal tab): default ON, Settings β†’ Privacy OFF clears; one-shot getCurrentPosition 12s β†’ POST /me/location 10/min (reject accuracy>200/OFF) β†’ Point + 2dsphere; GET /nearby?radius=1/2/5/10km 30/min $geoNear fuzz <1kmβ†’50m β‰₯1kmβ†’0.1km, filters banned/blocked-both/friends+pending+declined/stale>15m/hidden, nearest. Consent kivo:nearbyConsent; Permissions-Policy geolocation=(self).

πŸ’¬ Messaging core

DM create-or-return ($all). List by lastMessageAt desc + unread (not-mine ∧ not-readBy) + online. Cursor newest-first + hasMore, scroll-up infinite, around jump, after catch-up. Optimistic sentβ†’deliveredβ†’read + retry (chat-panel.jsx memoized MessageRows, O(1) byId). Menu: react strip, copy, save, profile, block, reply, thread, forward, pin, select (multi copy/forward/delete), Share mobile. ❀️ double-tap (500ms throttle). Edit own (Plus unlimited else 15m, history 10, ↑-on-empty), soft-delete (blanked, pin cleared). System chips never bump unread. Remove from list (DM either side, group admin, channels 403) = permanent both-sides + conversation:removed + confirm modal.

Reply replyToMessageId inline (deleted = placeholder). Forward bare {forwardedFromId} both-memberships-checked β†’ forwardedFromName pill, no caption/mention-resolve, forwardCount. Pin any member POST /:id/pin timestamp, newest-first max10 banner, pin-updated. Threads threadId=root (roots null) excluded from timeline/cursors/search/pinned/unread/cache; quiet (no badge/bell, mentions still notify); content-required, no quote/forward inside, live root non-system non-reply only; list (root+count/last/participants β†’ chips) + panel oldest-first 500; same message:* routed by threadId (thread-panel.jsx). Saved POST /:id/save β†’ savedBy, flag at read, GET /saved newest-first 200 (Plus 1K), personal cross-device, deleted drops, jump. Mentions @-autocomplete participants-only β†’ @username highlight (mention-autocomplete.jsx), override muted (if Mentions on).

πŸ“Š Polls Β· 🟒 Status Β· πŸ“Ž Files & voice Β· πŸ“ž Calls

Polls (polls/poll-card.jsx): type poll + poll{question140, opts 2–8Γ—60, voters, multi, anon, expiry, closed, total}. Free 5/24h/single; Plus 8/7d/multi+anon. vote/retract/end-early; poll:updated/ended; 60s sweep; searchable.

Status 24h friends-only (status/*): text280 + 6 backgrounds OR photo (jpg/png/webp/gif ≀30MB, 1/status) + caption280. Vertical tab My-top + grouped friends, green #25D366/grey rings, viewer 5s photo advance + caption + dots, 10/day, Appwrite attachments bucket, hourly file-then-doc sweep, status:new/deleted/viewed + isViewed. Mobile 5th tab.

Attachments (chat/attachments.jsx): browser β†’ backend allow-list + 30MB β†’ Appwrite bucket (images, docs pdf/doc(x)/xls(x)/ppt(x)/txt, audio webm/ogg/mp3/m4a/aac/wav β€” bucket extensions must include audio). 10/msg (Plus 20; 100MB). Progress + caption + lightbox (arrows/download) + doc cards + Media/Files/Links drawer (chat-media-drawer.jsx) with counts + jump + empty states. Voice: hold mic MediaRecorderβ†’bare MIME (no transcode), slide-up cancel, timer, needs net; 32-bar deterministic pseudo-waveform (fileId seed, CSS divs, no fetch), tabular-nums timer, shared <audio> singleton, slider seek (click, ←/β†’Β±5s, Home/End, Space), responsive, audioDuration preserved on forward.

Calls β€” LiveKit SFU (calls/*): deterministic rooms kivo_<conv>, 6h tokens POST /calls/token (membership/ban/block, 20/min), GET /config gates buttons, GET /status Join pill. Socket ring only (30s β†’ missed chip + bell + πŸ“žCALL: history started/cancelled/declined/missed/ended + duration + Call back). Panel: mute/camera/devices/leave, voiceβ†’video upgrade, mirrored selfie, group grid+headcount, reconnect banner, iOS unlock; lazy client SSR-safe, manual themed tracks.

πŸ”” Notifications Β· πŸ“± Offline Β· πŸ”Ž Search Β· πŸ”— Previews Β· πŸ“ Polish

Bell (notifications/*): DMs/groups/Spaces/announcements/mentions/requests+accepts/waves; cursor pages + mark-all; click deep-links. Prefs per category (Space OFF default; mentions break through; wave ungated 20s). Focus/blur suppression. Sounds Web Audio per category + master + preview (lib/sound.js, kivo:sounds). Perf: insertMany + batch prefs + async VAPID; 404/410 prune. Push: opt-in (bell first-ask), per-user subs, SW click β†’ chat (public/sw.js, lib/push.js, pwa-register.jsx).

PWA/offline: manifest + sw installable; precache shell + hashed /_next/static cache-first; navigations network-first β†’ cached shell β†’ styled offline (public/offline.html); API/Socket never intercepted. IndexedDB (lib/cache.js) lists + last-50/chat SWR + restore last open + Space expand. Outbox (lib/outbox.js) durable per-account text queue, survives reload, flushes on reconnect; attachments need net; dual-signal banner.

Search Ctrl+K (dashboard/search-overlay.jsx): messages+people+spaces + jump highlight (Plus 20/cat vs 5). Blocked/banned filtered. Previews (chat/link-preview.jsx, lib/links.js): first URL β†’ card (image/title/2-line/domain); server 8s/1.5MB regex parse + relative resolve, 1h/500 cache, SSRF-guarded (http(s), no localhost/private/link-local on host + DNS + redirect); empty stays plain; client 200-cache, brokenβ†’text-only. Polish (derived, zero schema): day dividers Today/Yesterday/weekday/date; big emoji 1–3 grapheme-aware (lib/emoji.js, Intl.Segmenter) 44/36/30px chromeless; jump pill (120px pin, 200px threshold, own sends pin); drafts per-conv (lib/drafts.js 250ms/50-chat/4000-clamp); paste-imageβ†’queue; swipe ~45px lock + haptics.

πŸ‘€ Profiles Β· πŸ‘‹ Wave Β· 🎨 Emoji Β· πŸ‘‘ Plus

Profiles (dashboard/profile-edit-modal.jsx, profile/*): name50/handle/bio280/status60 + chip8 (32 + 6 vibes) + photo4MB + 10 frames (Default, My-accent, solids, gradients) + banner (curated GIFs; Plus custom 8MB PATCH /me/banner, old retired) + effects none/glow/gradient-name/aura (kivo-pfx-*, server-clamped, downgrade→none) + flag + graph + social chips (social-links.jsx) + pronouns + auto-clear + privacy toggles. Public /u/:username (app/u/[username]): no-login, Plus flourishes, owner-theme skin (lib/profile-skin.js), Share + QR + copy + native Share, Join nudge, Message/Add/Accept/Block/Unfriend. Verified admin-granted + showBadge toggle; Google/GitHub chips.

Wave: notification not message. POST /notifications/:id/wave β†’ wave + senderUsername, 20s cooldown, self/blocked rejected, in-app else VAPID, deep-links to sender, countdown button. Emoji 3-tier (emoji-factory-panel.jsx, lib/custom-emoji.js): Global admin 200 / Space Plus-admin 100 / Personal Plus 50 everywhere; β†’webp (gif kept) 2MB sharp; emoji bucket (fallback attachments); :name: + custom:id reactions; IDB SWR; emoji:new/deleted.

LimitFree β†’ Plus
Message length Β· edit window4K Β· 15m β†’ 8K Β· unlimited
Attachments Β· file size10 Β· 30MB β†’ 20 Β· 100MB
Pins Β· saved10 Β· 200 β†’ 50 Β· 1K
Groups owned Β· members5 Β· 20 β†’ 20 Β· 100
Spaces owned Β· members Β· channels3 Β· 50 Β· 10 β†’ 15 Β· 300 Β· 30
Calls Β· search/cat Β· polls active Β· opts Β· duration5 Β· 5 Β· 5 Β· 5 Β· 1d β†’ 25 Β· 20 Β· 20 Β· 8 Β· 7d (+multi+anon Plus-only)
Forward Β· personal emoji5 Β· 0 β†’ 10 Β· 50

Plus UPI self-serve (plus/plus-screen.jsx, modules/plus/*, lib/plus.js PLAN_LIMITS): pay published UPI ID β‚Ή49 β†’ paste 12-digit UTR (POST /plus/claim 5/day) β†’ admin queue ≀24h β†’ 30d grant (PLUS_DURATION_MS). /plus pricing+steps+status. Expired reads free (getEffectivePlan, never trust client); hourly sweepPlus lapses + scrubs effects. Admin POST /admin/users/:id/plan + audit.

🏠 Spaces detail

Create β†’ auto #general (each channel = own Conversation). Text (all) vs announcement (admin+; anyone may thread-reply). Admin+ channels (last protected). Roles member↔moderator (admin+), admin owner-only, no direct owner, last-owner protected, self-leave auto-promotes. Public β†’ Discover (search/category, one-click); private hidden + single rotating 7d code (Privacy & invites; New invalidates, Off closes; never in payloads; /app?join=CODE + Discover box). Space look (accent+tint+wallpaper+bubbles, live preview, per-field "Member's own", Resetβ†’personal) scopes members' channel view. Owner-only delete kills channels+messages. Files: spaces/*.

Signature Β· try it

Theme system β€” live demo

In plain words: every color is a variable. A theme is one object of values. Tap below β€” this page re-skins exactly like Kivo, with zero reloads.

Engine (lib/theme.js): derivePalette() washes only surface tokens (base/surface/elevated/border/scrollbars/bubbleReceived) preserving lightness β€” contrast never breaks. tintStrength 0.2 dark / 0.14 light; isChromatic rejects near-gray; accent sets badge/soft/onAccent via luminance. resolveChatLook() (lib/chat-style.js): conversation β†’ Space β†’ personal per field, null = "Member's own". Wallpapers pure CSS color-mix() (none/dots/grid/diagonal/bubbles/wash); bubbles kivo-bubbles-* (rounded/pill/squared/outline-mine). Partial merges; conversation:updated repaints live; rings follow accent; /u wears owner skin (profile-skin.js).

ThemeFamilyCanvas
Framer (default)Dark near-black + blue#090909
MidnightDark navy#0a0e16
GraphiteDark slate#0d0f12
EspressoDark warm brown#130e0a
PineDark forest#0a160f
PlumDark aubergine#100a14
PorcelainLight off-white#faf9f7
LinenLight cream#f8f4ec
MistLight blue-gray#f5f7fa
SageLight green#f5f7f1

One canvas. Every surface answers to it.

Tokens flow through CSS variables β€” no hardcoded colors, no per-component overrides. That's why 10 themes + studio + Space palettes + conversation looks compose without conflicts.

Performance

What keeps it fast (and what to measure)

Single-agg unread

unreadCountsByConversation() kills N+1 countDocuments. Measure: inbox latency vs conv count.

Bulk + async fan-out

One insertMany + batch prefs; sender never waits on VAPID. Measure: POST p95 vs recipients.

Memoized rows

React.memo MessageRows + stable ctx + O(1) replies. Typing/scroll/presence skip re-renders.

Cheap media UI

useMemo 32-bar waveform (no canvas/fetch), tabular-nums timer (no CLS), shared audio; in-chat highlight client-only.

Pagination + indexes

Cursor + around + after; index per read pattern + text index. Don't optimize blind β€” measure first.

content-visibility

Off-screen rows skip layout/paint. No virtualization code to maintain.

Design decisions

Why this, not that

How to use in interviews: pick 3 rows and defend both sides. The right answer always names the tradeoff and the moment you'd switch.
DecisionWhy this, not that
WhatsApp + Discord, one identityPeople live in both; one login beats two apps. Not Slack: enterprise bloat rejected on purpose.
Mongo truth; Socket announcesEvents-as-truth vanish on restart. Write-first makes events disposable + replayable.
No Redis yetSingle instance; memory limiter+presence simpler. Explicit rule: don't add before needed. Later: adapter + shared limits β€” never canonical storage.
Monorepo folders, no microservicesNo version/deploy ceremony. Split only on real scale/org complexity.
JS only, no TSFocused learning surface; Zod at boundaries gives the safety that matters most.
Messages own collectionEmbedded arrays bloat unbounded; own collection = indexed cursors + threads-as-messages.
Threads structurally quietSide talk must not punish main timeline β€” no badge/bell, only mentions.
Pins & saves = fieldsOne update + indexed query; delete clears pin free; no join table.
Server-side unfurlClient fetch leaks IP + mixed-content. One cached fetch/hour/URL.
No server call stateDeterministic kivo_id rooms make late-join/reconnect trivial; only 30s ring timer.
Optimistic UIRound-trip waits feel laggy; paint β†’ reconcile β†’ retry.
Drafts in localStorageHalf-typed text is device ephemera, not account data.
Bulk + async pushSender latency must not scale with recipient count.
No refresh rotationRotation races strand clients; Session delete already revokes.
Duplicate DM in serviceMongo unique on arrays β‰  pairwise; explicit $all is correct.
Voice = attachmentMediaRecorder β†’ existing upload β†’ play URL. Only new state: duration + kind. No transcoding.
Chat look CSS-firstcolor-mix + container classes re-tint dark/light/Space with zero extra state.
Plus = flag + expiryClient never trusted; getEffectivePlan() + caps + sweep.
Wave = notificationNo row/badge spam; same bell+push pipe, deep-links to profile.
Receipts derivedreadBy/deliveredTo + participants β€” no extra store.
Nearby fuzzed one-shotDistance-only + rounding + no tracking = discovery without surveillance.
Emoji sparse-unique tiersSame :name: across scopes, no collision; Plus funds expression.
Sweep deletes files firstTTL alone orphans Appwrite files; explicit hourly cleanup.
Appwrite not S3Free tier + preview URLs + simple SDK for a solo dev; migrate when egress/cost demands.
Bun not plain Node/npmSpeed of install/run; still Node-compatible at runtime.
Run

Run yourself, configure & deploy

# backend (terminal 1)
cd backend && bun install
cp .env.example .env   # fill MongoDB/JWT/Appwrite/VAPID/Gmail/LiveKit/ADMIN_*
bun run dev            # http://localhost:4000  (prod: bun run start)

# frontend (terminal 2)
cd frontend && bun install
echo "NEXT_PUBLIC_API_URL=http://localhost:4000" > .env.local
bun run dev            # http://localhost:3000  (prod: build + start)
bun run lint           # Biome check
# push: npx web-push generate-vapid-keys  β†’ VAPID_* in backend .env
# calls stay hidden until LIVEKIT_* set; storage errors until APPWRITE_* set

Environment variables

VarWherePurpose
PORT Β· MONGODB_URIbackendAPI/Socket port (4000) Β· Atlas/local connection
ACCESS_TOKEN_SECRET/REFRESH_TOKEN_SECRET Β· ACCESS_TOKEN_TTL 15m Β· REFRESH_TOKEN_TTL 7d Β· REFRESH_COOKIE_SAMESITEbackendJWT signing + lifetimes + cookie mode (strict; lax for cross-subdomain)
CORS_ALLOWED_ORIGINSbackendAllowed frontends (empty = allow any in dev)
APPWRITE_ENDPOINT/PROJECT_ID/API_KEY Β· APPWRITE_BUCKET_ID Β· APPWRITE_ATTACHMENTS_BUCKET_ID Β· APPWRITE_EMOJI_BUCKET_IDbackendStorage creds Β· avatars Β· attachments (fallback for emoji) Β· emoji (fallback = attachments)
VAPID_PUBLIC/PRIVATE_KEY/SUBJECTbackendWeb-push keys (generate via web-push)
LIVEKIT_URL/API_KEY/API_SECRETbackendCalls; unset = call buttons hidden
GMAIL_USER/APP_PASSWORD Β· EMAIL_FROM Β· FRONTEND_URLbackendSMTP sender + From header + base URL for email links (default localhost:3000)
ADMIN_EMAIL Β· ADMIN_PASSWORD_HASH Β· ADMIN_JWT_SECRET/TTL 30m Β· ADMIN_COOKIE_NAMEbackendSeparate admin login (bcrypt hash, own JWT + cookie)
GOOGLE_CLIENT_ID/SECRET/REDIRECT_URI Β· GITHUB_*backendOAuth (redirect defaults ${FRONTEND_URL}/api/v1/auth/oauth/*/callback)
BACKEND_URL Β· NEXT_PUBLIC_API_URLfrontendRewrite target + direct Socket origin (both default localhost:4000)
NEXT_PUBLIC_PLUS_UPI_ID Β· NEXT_PUBLIC_PLUS_PAYEEfrontendPublished UPI ID + payee name on /plus

Deploy checklist

βœ… Ship

Frontend on Vercel-class host with rewrites β†’ backend. Backend on single always-on host with long-lived WebSockets. Atlas + Appwrite + VAPID env set. trust proxy on behind LB. *.env gitignored, .env.example maintained.

πŸ”­ Later (only on real scale)

LB β†’ N API/Socket servers β†’ Redis (adapter, limits, counters) β†’ Mongo. Split services only when org/scale forces it. Add metrics (p95 POST, socket fan-out, push success), log AdminActionLog longer, back up Mongo.

Interview bank

Questions & answers

For everyone

πŸ™‹ Everyday FAQ (no jargon)

Glossary

Terms, plainly

Realtime

Arrives the moment it's sent β€” no refresh.

WebSocket / Socket.IO

Two-way wire that stays open browser↔server.

Room

Broadcast group β€” e.g. one per chat. Only members hear it.

JWT (15m)

Signed tamper-proof ticket proving who you are.

httpOnly cookie

Cookie JavaScript can't read β€” holds your login session.

Optimistic UI

Show it instantly, confirm when server answers.

PWA / Service worker

Installable website / background script for offline + push.

IndexedDB / Outbox

Browser database for cache / queue of offline sends.

VAPID / Web Push

Standard letting servers ping browsers when closed.

Zod

Schema checker rejecting bad input on the server.

SSRF

Tricking a server into fetching internal URLs β€” Kivo guards previews against it.

SFU (LiveKit)

Media server forwarding audio/video without mixing β€” scales calls.

TOTP / 2FA

Time-based authenticator codes β€” second login step.

UTR

12-digit UPI payment reference you paste to claim Plus.

2dsphere / $geoNear

Mongo geo-index + nearest-first query powering Nearby.

Honesty

Roadmap & limits

Shipped (Phase 1 + 1.5): DMs, groups, Spaces/channels, notifications + prefs + push, PWA + offline + outbox, 10 themes + studio + Space palettes + chat look, attachments + voice waveform, profiles + badges + blocking + wave + QR, friends + welcome + Nearby, polls, Status text+photo, threads + saved + forward + pin, Ctrl+K search, link previews, LiveKit calls, 2FA, OAuth, custom emoji, Plus UPI, admin panel, media gallery, delete, Seen-by, drafts/swipe/jump/dividers/big-emoji.
Next (Phase 2–4): stronger search, video attachments, voice rooms, screen share, recording, bots/webhooks, dev platform, mini-apps, theme marketplace.
Not / limits: no E2E encryption (server sees plaintext) Β· offline history = last-50/chat Β· no video attachments yet Β· single-instance presence (needs shared adapter to scale) Β· disappearing messages removed 2026-09-08. If you demo this, say the limits out loud β€” interviewers trust it more.

Tell me about Kivo β€” 60-second answer.

"Kivo is a realtime chat platform blending WhatsApp-style DMs and groups with Discord-style Spaces and channels, differentiated by deep theming. Next.js 16 + Express 5 + Socket.IO + MongoDB in a JS-only monorepo. Mongo is the source of truth; sockets only announce. Auth is 15-minute JWT plus session-backed httpOnly refresh with real revocation, bcrypt-12, Zod on every boundary, in-memory rate limits, scoped presence, gap-fill reconnects, IndexedDB plus an offline outbox, Appwrite files, VAPID push, LiveKit calls, and Plus as a server-checked entitlement. I kept it single-instance and CSS-driven on purpose β€” no Redis, no microservices, no call state β€” until real scale demands it."

Follow-up ready: scaling plan Β· E2E gap Β· why no TS Β· thread quietness