Jekardah Chat: A Messenger Keyed by Gmail Address, Realtime Without a Socket, and a History That Ends Every Midnight

Romi Nur Ismanto
Independent AI Research Lab, Jakarta, Indonesia
hello@rominur.com
October 2026

Abstract

Jekardah Chat, deployed at chat.jekardah.com, is an Indonesian-language messenger laid out like WhatsApp Web whose user identity is a Gmail address instead of a phone number. Users sign in with Google, start a conversation by typing a friend’s email, and can write to someone who has never signed in; the messages are waiting when that person does. The same web deployment is the browser app, an installable phone app on iPhone and Android, and the content of Electron apps for macOS and Windows. This paper describes the canonical email key that makes two spellings of one Gmail inbox the same person; a realtime channel built from one version counter per user and a held HTTP request, with no socket service required; delivery and read ticks for direct and group chats; media stored privately and served only to members of the conversation; a purge of every message and file at 00.00 Western Indonesia Time that is claimed by the first request of the new day; and the limits that remain, chief among them that messages are not end-to-end encrypted.

Keywords: instant messaging, email-based identity, Google Identity Services, long polling, realtime, read receipts, group chat, Web Push, service worker, progressive web app, Electron, Neon Postgres, Vercel Functions, Vercel Blob, data retention, Bahasa Indonesia

1. Introduction

Nearly every messenger used in Indonesia is tied to a phone number. That is convenient until it is not: a number is handed to every group a person joins, numbers are recycled by carriers, and someone who wants to message a colleague, a supplier or a classmate often knows their email address but not their mobile number. Almost everyone with an Android phone already has a Google account, and its Gmail address is an identifier people are used to sharing.

Jekardah Chat is built on that identifier. The login page lists five steps, and two of them are the design: Tidak perlu nomor HP — identitas Anda adalah alamat email, no phone number is needed because the identity is the email address, and semua pesan & file dihapus otomatis setiap hari pukul 00.00 WIB, every message and file is deleted automatically each day at midnight Jakarta time. The interface deliberately copies the conventions people already know from WhatsApp Web — a chat list on the left, the conversation on the right, ticks under each message, a mengetik… indicator — so the only new idea a user has to learn is to type an email address where they would have typed a number.

2. Architecture

The application is a static front end and a single function. The front end is plain HTML, CSS and JavaScript in public/ — one 2,784-line app.js holds the whole client — served from Vercel’s edge. Every /api/* path is rewritten to one Vercel Function running an Express 5 application of about 950 lines, pinned to the Singapore region, which is the closest to Jakarta and to the database. State lives in Neon Postgres, whose schema is created or migrated by the build command itself, so a deploy cannot run against an older schema. Files live in Vercel Blob with private access. The daily purge has a backup trigger in Vercel Cron.

Table 1. Components. Everything except Google sign-in and push delivery runs inside one Vercel project.
ConcernImplementation
ClientHTML/CSS/JS without a framework; service worker; web app manifest
APIExpress 5 in one Vercel Function (api/index.js → src/chat-api.js), region sin1
DatabaseNeon Postgres over its serverless HTTP driver; schema in src/db.js
RealtimeLong poll on /api/wait against a user_events version per user; Pusher channels optional
Notifications while closedWeb Push with VAPID keys; service worker shows the notification and the badge count
MediaVercel Blob, private; images and documents streamed through the function, video through a signed URL
Sign-inGoogle Identity Services ID token → 30-day JWT session cookie, renewed while in use
Retentionsrc/purge.js at the first request after 00.00 WIB; Vercel Cron at 17.00 UTC as backup

3. An Email Address as the Primary Key

Using an email as identity raises a question a phone number does not: when are two strings the same person? Gmail delivers Budi.Santoso+kerja@gmail.com, budisantoso@gmail.com and budisantoso@googlemail.com to the same inbox, because it ignores dots in the local part and everything after a plus sign, and treats the two domains as one. If the server stored addresses as typed, a friend who added the dotted spelling would open a conversation with someone who can never sign in under that spelling, and the messages would wait forever.

Every address therefore passes through one function, normalizeEmail, before it is stored, compared or used to look anything up: it is trimmed and lower-cased, checked against a length of 254 characters and an address pattern, and, for the two Gmail domains only, has its plus-suffix removed, its dots deleted and its domain rewritten to gmail.com. Other domains are left as written, since a company mail server may well treat dots as significant. The canonical form is what conversations, group memberships and push subscriptions are keyed on, and it is also the identity read from the Google token at sign-in, so the person who later signs in is matched to the same row.

Because the key is an address rather than an account, a conversation can be created with someone who has never visited the site. Their side of it exists from the first message; when they sign in, it is simply there, unread. The client offers an Undang button that opens a ready-written invitation in the user’s own mail client, so the invitation is sent by the user and not by the service.

4. Realtime Without a Socket

A serverless function cannot hold a WebSocket open, and the usual answer is a hosted socket service. Jekardah Chat supports one — Pusher, with one private channel per user whose name is an HMAC of the email so the address never appears on the wire — but does not depend on it. The default channel is a counter. Table user_events holds one integer version per user. Anything that should change a user’s screen — a new message, a tick turning blue, a typing indicator, a renamed group, a purge — increments the counter of every affected user in a single INSERT … ON CONFLICT DO UPDATE over an array of emails.

The client calls GET /api/wait?v=<last version>. The function reads the user’s version and, while it equals the one the client already has, sleeps 300 ms and reads again, for up to 25 seconds; then it answers with the new version, and the client fetches only what changed. The poll also returns immediately if the client is running an older build than the server, which is how open tabs and installed apps learn that a deploy has happened and reload themselves at a safe moment.

send message → INSERT message + bump version(sender, recipients) → /api/wait sees version ≠ v → client fetches delta → screen updates

The 300 ms read interval is what sets the latency; end-to-end tests recorded a message appearing on the other side in about 0.3 to 0.4 seconds. The same counter keeps several tabs of one user consistent, because a message sent from one tab bumps the sender’s own version too. The cost is a short query against Postgres every 300 ms per open client, which is acceptable at the scale of a personal service and is the main reason Pusher is kept as an option. Without Pusher there is also no true presence signal, so online is inferred from the last-seen timestamp, which the wait endpoint refreshes at most once every 15 seconds and which counts as online for 45 seconds.

5. Ticks, Groups and Notifications

Each message carries the three states users expect: one grey tick when the server has stored it, two grey ticks when it has reached a device of the recipient, and two blue ticks when it has been read. Delivery is recorded when the recipient’s client next loads its conversations, or earlier, when its service worker receives a push notification and calls /api/push/delivered. Read is recorded when the conversation is open and the window is visible. In a group, a message turns blue only when every member has read it, and receipts are recalculated and broadcast when members join, leave or read.

Groups are created from a list of member emails, normalised like any other. Their creator is an admin; admins can add and remove members and promote others; anyone can rename the group or leave it, and each of these events is written into the conversation as a system message. Membership is checked on every request by middleware that resolves the conversation and refuses it unless the caller is one of its two participants or a member of the group, so a user who has left a group cannot read or post to it again by guessing its identifier.

When no tab is open, new messages arrive as Web Push notifications. On iPhone this requires iOS 16.4 or later and the site opened from its home-screen icon, where the user then taps Aktifkan notifikasi. The service worker also sets the unread count on the app icon and serves an offline page when there is no connection, reconnecting by itself.

6. Private Media

Photos and documents — PDF, Word, Excel, PowerPoint, text, CSV and ZIP, RAR or 7Z archives — are uploaded through the function, capped at 4 MB to stay under the platform’s request body limit, given a random 128-bit name and stored in Blob with access: private. HEIC photos from iPhones are converted to JPEG on the client first wherever the browser can decode them. Videos of up to 64 MB are too large to pass through a function, so the client asks for a one-hour upload token for a video content type and uploads directly to Blob; when the message is sent, the server confirms the object exists and takes its size from Blob rather than from the client.

A message stores a path of the form /api/media/<name>, never a storage URL. Fetching that path performs one query: does a message with this file exist in a conversation the caller belongs to? If not, the answer is 404, whether the file exists or not. Images and documents are then streamed from Blob through the function, with PDFs shown inline and every other document sent as an attachment under a sandboxing content-security policy; videos are redirected to a signed Blob URL valid for one hour, so the browser can seek with range requests without the function in the path. Signed URLs are cached in memory and reused while they have more than 35 minutes left.

7. A History That Ends at Midnight

The service keeps no history. Every day at 00.00 WIB (UTC+7), all messages, typing states and uploaded files are deleted permanently; accounts, contacts, groups and memberships remain, so the next day starts with the same chat list and empty conversations. A cron job alone could not keep that promise precisely, because Vercel’s Hobby plan runs a daily job somewhere inside the scheduled hour. The purge is therefore triggered by traffic.

Every request that reads or writes messages first calls ensureDailyPurge(). Each function instance remembers in memory the last WIB date it has seen purged, so on an ordinary request this is a comparison of two strings and no query. When the date changes, the first request on any instance attempts to claim the new day with a conditional update:

UPDATE system_state SET value = :today
WHERE key = 'purge_day' AND value < :today
RETURNING value

Only one request in the whole deployment can move the value forward, so only one runs the purge, inside a transaction that deletes the messages and typing states, records the purge time, and bumps every user’s version so that open apps clear the screen at once. Concurrent requests on the same instance wait on the same promise; requests on other instances see that the day has been claimed and carry on. Because sending a message also calls ensureDailyPurge() first, a message written at 00.00:01 cannot be swept away by a purge that starts after it. Files are listed and deleted from Blob in pages of a thousand, skipping anything uploaded in the last two minutes, which may belong to a message about to be sent; an unused file is removed the following night. The Vercel Cron job at 17.00 UTC calls the same function with a bearer secret and does nothing if the day has already been claimed.

8. One Deployment, Five Platforms

On a phone, the site is installed from the browser rather than from an app store: a manifest, a set of blue icons and a service worker make it open full screen from the home screen, and the download link in the interface turns into installation instructions for the detected platform. Google’s sign-in popup does not return to an iPhone home-screen app, so there the client switches to Google’s redirect mode: Google posts the ID token to /api/auth/google/redirect, and the server accepts it only if the g_csrf_token in the form matches the one in the cookie. On the desktop, a separate Electron project packages a window that loads chat.jekardah.com, adding a Dock badge on macOS, a tray icon and taskbar dot on Windows, and native notifications; it is distributed as .dmg files for Apple Silicon and Intel and a 64-bit Windows installer.

Since every client loads the same site, a web deploy updates all five platforms at once, and the build check in the long poll means even an app left open for days picks up the change. The visual system is shared too: a blue palette with a dark mode that follows the operating system, San Francisco on Apple devices and Inter elsewhere, message text at 15 px, and layout that respects the iPhone notch and the Android gesture bar.

9. Security and Limits

Sessions are JWTs in an httpOnly, Secure, SameSite=Lax cookie valid for 30 days. Responses carry a strict content-security policy that allows scripts only from the site and Google’s sign-in client, HSTS, nosniff and frame-ancestors 'none'. A development login that accepts any email exists for local testing and is disabled in production; the README records that, against production, unauthenticated API calls return 401, the development login returns 404, and the cron endpoint rejects a wrong secret.

The most important limit is stated on the project page: messages are not end-to-end encrypted. They are stored in Postgres like any web chat, readable by whoever operates the database, and the daily purge bounds how long that exposure lasts rather than removing it. Media are private and checked per request, but they too are held by the operator until midnight. The default realtime path trades database load for independence from a socket service, which suits a personal deployment more than a large one. At the time of writing the Mac builds were not yet notarised, so macOS asks the user to approve the app once; the phone apps had not been tested on physical iPhone and Android devices, nor the Windows installer on a physical PC. The project’s own checks list 28 end-to-end tests and 17 smoke tests passing against the deployment.

10. Conclusion

Jekardah Chat replaces the phone number with the one identifier most of its users already share freely, and keeps the rest of the experience exactly where they expect it. Most of the engineering is in making that substitution hold: one normalisation function so that an inbox is one person however it is spelled; one counter per user so that a serverless function can deliver messages in under half a second without a socket; one membership query in front of every file; and one conditional update that lets the first request after midnight erase the day for everyone, exactly once.

References

  1. Google. “Google Identity Services: Sign In With Google for Web” and “Verify the Google ID Token on Your Server Side.” Developer documentation, 2026.
  2. Google. “Gmail Help: Dots don’t matter in Gmail addresses” and “Create task-specific email addresses.” 2026.
  3. Loreto, S., Saint-Andre, P., Salsano, S., Wilkins, G. “Known Issues and Best Practices for the Use of Long Polling and Streaming in Bidirectional HTTP.” RFC 6202, IETF, 2011.
  4. Thomson, M., Damaggio, E., Raymor, B. “Generic Event Delivery Using HTTP Push.” RFC 8030, IETF, 2016; and Thomson, M., Beverloo, P. “Voluntary Application Server Identification (VAPID) for Web Push.” RFC 8292, 2017.
  5. W3C. “Service Workers” and “Web Application Manifest.” 2026; and “Badging API.” Draft, 2026.
  6. Apple. “Sending Web Push Notifications in Web Apps and Browsers.” WebKit documentation, iOS 16.4, 2023.
  7. Vercel. “Vercel Functions: Limits”, “Cron Jobs: Usage and Pricing” and “Vercel Blob: Private Storage and Client Uploads.” Documentation, 2026.
  8. Neon. “Neon Serverless Driver.” Documentation, 2026.
  9. Pusher. “Channels: Private Channels and Authorizing Users.” Documentation, 2026.
  10. OpenJS Foundation. “Electron” and “electron-builder.” Documentation, 2026.
  11. Ismanto, R. N. “Jekardah COD: A Meet-Check-Pay Marketplace on a Go Serverless Function.” 2026.
  12. Ismanto, R. N. “Somat: A Single-Surface Conversational Interface with Server-Resident Capability and Client-Resident State.” 2026.