← Try the mock

Ringer: how it's built

Gmail triage that rings the phone only when an email needs it.

Two inboxes feed one event stream. A classifier scores each email and code decides call, push, digest or label, with a digest at 8am. Labels and pushes are live; calls are built but off (as of 2026-10-05).

Architecture

How an email becomes a ring

Event-driven, no polling of the mailbox. The service scales to zero, so delayed work rides on a queue.

How an email becomes a ringGoogleRingerVendorsOwnerGmail inboxesPub/Sub topicFirestoreCloud SchedulerCloud Tasks queueTriage serviceJev classifierClaude HaikuPushoverTwilio VoiceHealthchecksOwner's phone123456789101112131415
  1. Gmail inboxes → Pub/Sub topic: new-mail event from a watch on the inbox (push, async)
  2. Pub/Sub topic → Triage service: push delivery; a 503 asks for redelivery (push, async)
  3. Triage service → Gmail inboxes: history sync, message read, then one label write (request, sync)
  4. Triage service → Firestore: claim, dedupe and log the decision (request, sync)
  5. Triage service → Jev classifier: classify the email, one request each (request, sync)
  6. Triage service → Pushover: emergency or normal push (request, sync)
  7. Triage service → Cloud Tasks queue: ring check scheduled 300 seconds out (timer, async)
  8. Cloud Tasks queue → Triage service: still unread and unacknowledged? (request, async)
  9. Triage service → Twilio Voice: place the call (request, async)
  10. Twilio Voice → Owner's phone: ring with a press-1 script (push, sync)
  11. Twilio Voice → Triage service: voice and keypress webhooks, signed (webhook, sync)
  12. Cloud Scheduler → Triage service: watch renewal, 8am digest, hourly heartbeat (cron, async)
  13. Triage service → Claude Haiku: write the morning digest (request, sync)
  14. Triage service → Healthchecks: heartbeat ping (request, sync)
  15. Pushover → Owner's phone: lock-screen notification (push, async)

The call path

The phone rings only if the push was not enough; the checks run once, at the five-minute mark.

The call pathRingerVendorsOwnerDecide to callWait 5 minutesStill unread?Redial in 2 minutesEmergency pushPlace the callMissed-call pushOwner's phone123456789
  1. Decide to call → Emergency push: emergency push goes out first (request, sync)
  2. Decide to call → Wait 5 minutes: a queued check, 300 seconds out (timer, async)
  3. Wait 5 minutes → Still unread?: unread and unacknowledged? limits re-checked once (request, async)
  4. Still unread? → Place the call: still unread, so place the call (request, async)
  5. Place the call → Owner's phone: ring with the script; press 1 to acknowledge (push, sync)
  6. Place the call → Redial in 2 minutes: no press within 2 minutes (timer, async)
  7. Redial in 2 minutes → Place the call: dial again, up to three dials in all (request, async)
  8. Redial in 2 minutes → Missed-call push: no press after three dials (request, async)
  9. Missed-call push → Owner's phone: missed-call push (push, async)

Wiremocks

A mock-up of a screen — Here, a static picture of a screen, drawn to show layout and flow, not the working app. — a static picture, not the real app

What the owner sees

Drawings of the real screens with made-up senders. The call screen shows a path that is built but off (as of 2026-10-05).

Incoming call

Ringer

Critical email from Recruiting team. Subject: Next steps for the onsite interview. Press 1 to acknowledge.

DeclineAccept

Calls are built but off, so this screen is a drawing of the path.

Lock-screen pushes

  • Tier 1, critical: needs action within hours — Needs my action within hours or something important is lost. interview next steps: Recruiting team

    Next steps for the onsite interview

    Emergency priority, trusted sender

  • Tier 2, high: needs attention today — Needs my attention today but a few hours' delay is harmless. recruiter outreach: Sourcing team

    A quick chat about a platform role

    Normal priority, untrusted sender

Morning digest, 8:00

Mail digest: 5 emails, 1 held overnight

  • Recruiting team (hiring.example) wants to chat next week about the platform role.
  • Careers team (jobs.example): not moving forward.
  • Receipts: two from shop.example.
  • Newsletters: one from news.example.

Gmail labels

  • Triage/1 Critical1
  • Triage/2 High2
  • Triage/3 Major3
  • Triage/4 Minor12
  • Triage/5 Not urgent18
  • Triage/6 Spam4

Running cost

ResourceFigureStatusSource
Jev input tokens$0.042 per One million — Shorthand in prices and limits, as in $1 per 1M tokens. tokensPublished price
cost page (demo/index.html), line 522
Read on 2026-10-17
Jev output tokensfreePublished price
cost page (demo/index.html), line 522
Read on 2026-10-17
Twilio phone number$1.15 per monthPublished price
cost page (demo/index.html), line 520
Read on 2026-10-17
Twilio outbound call$0.014 per minutePublished price
cost page (demo/index.html), line 520; commit 9ffa675
Read on 2026-10-17
Pushover$5 oncePublished price
cost page (demo/index.html), line 509
Read on 2026-10-17
Healthchecks.iofreePublished price
cost page (demo/index.html), line 529
Read on 2026-10-17
Google Cloud services$0 per monthEstimate (the project's own)
cost page (demo/index.html), lines 505, 524-527 (free tier at this volume)
Read on 2026-10-17
Conflicts with commit c14541d (free-tier fit unverified, read on 2026-10-17): The commit says the free-tier fit was never checked. The project's own page still states $0 and no measurement exists either way, so the row stays an estimate.
Twilio number and calls~$1.50 per monthEstimate (the project's own)
cost page (demo/index.html), line 520 (includes the number)
Read on 2026-10-17
Claude Haiku digest~$0.70 per monthEstimate (the project's own)
cost page (demo/index.html), line 521
Read on 2026-10-17
Jev, monthly~$0.25 per monthEstimate (the project's own)
cost page (demo/index.html), line 522
Read on 2026-10-17
Monthly total~$2.45 per monthEstimate (the project's own)
cost page (demo/index.html), lines 504-510 (estimates summed from components, not quotes)
Read on 2026-10-17
First year~$34 for year oneDerived from an estimate
see inputs
Inputs: 2.45 × 12 + 5 = 34.4 (the monthly total, projected; Pushover, published)
Read on 2026-10-17
Call minutes~25 per monthDerived from an estimate
see inputs
Inputs: (1.50 − 1.15) ÷ 0.014 = 25 (projected calls line, published number and minute rate); calls were off as of 2026-10-05
Read on 2026-10-17
Jev backfill~$0.02 onceDerived
see inputs
Inputs: ~527k tokens (measured, commit 8fb90a2) × $0.042 per One million — Shorthand in prices and limits, as in $1 per 1M tokens. = $0.022
Read on 2026-10-17
Conflicts with cost page (demo/index.html), line 531 (~$0.03, read on 2026-10-17): The page gives a rounder figure for the same backfill. The commit's measured token count governs.

No row of the owner's metered table names this project. Google Cloud runs on a trial credit.

Design decisions

  • The model classifies; code decides whether to ringThresholds live in code, so the policy is auditable and tunable without a model rewrite. An email's text cannot talk its way into a call.
  • Sender trust is computed in codeA call needs a sender that is authenticated and known: a hiring system, an allowlist or past mail. An email can argue for its own urgency, so the model gets no vote.
  • Push first, ring laterAn emergency push goes out at once; the call waits five minutes and rings only if the email is still unread and the push unacknowledged. A queue carries the delay.
  • One Gmail write path, guarded by a testEvery label write goes through one module; a test fails on any other write. The second inbox is read-only, so a bug can mislabel mail but never send or delete it.
  • Shadow mode first, then a staged rolloutRollout order: labels, pushes, calls. In the label-only days one failing deploy would have sent 24 alerts in four days, so untrusted urgent mail became a normal ping.

Open questions

  • Jev tokens per email: three repo figures disagree (1,300 unmeasured on the cost page; ~1,400 to ~1,600 in commit 16f864b; ~1,800 in the project backlog)
  • Load: ~150 emails a day is the outside spec's assumption, not a measurement