Gist
A daily personalized morning briefing. Pulls signal from your calendar, recent mail, weather, news, and the time of year, distills it through Claude, and delivers a short read tuned to the person reading it.
Problem
Most morning newsletters are one-size-fits-all. Gist generates a fresh briefing per user every day, blending their calendar, recent mail, weather, news of interest, and even the day's moon phase, then asks Claude to synthesize a short, personal read.
The architecture is sized for a small number early adopters, as a single scheduled function generates every due briefing in one run.
Architecture
- Firebase Functions
- TypeScript
- Claude API
- Firestore
- Cloud Scheduler
- Gmail API
- OpenWeather
A scheduled Cloud Function runs every 15 minutes, queries Firestore
for users whose nextDeliveryAt has passed, and for each
eligible user fans out five connector calls in parallel —
weather, calendar, news, Gmail, moon phase. The aggregated signal is
handed to Claude for briefing generation, the result is written back
to Firestore, and per-user delivery state is updated for the next
cycle.
Technical decisions
-
Parallel connector fan-out
generateMorningGist.ts:92–98 uses
Promise.allacross five independent connector calls per user. The connectors don't share state, so serializing them buys nothing — running them in parallel cuts per-user latency and ensures a better user experience. -
Resilient batch with
allSettledgenerateMorningGist.ts:384–437 is the scheduled fan-out: an
onSchedulecron every 15 minutes, thenPromise.allSettled— notPromise.all— over the eligible users. One user's expired Gmail token or rate-limited connector shouldn't tank the whole batch, and a settled batch means failures surface in logs without killing successful deliveries. -
Idempotency on
lastGeneratedDategenerateMorningGist.ts:418–425 short-circuits when
lastGeneratedDate === todayKey. Cron retries and overlapping invocations are a real failure mode when an upstream API is slow; without this guard the same user gets the same briefing generated twice and the LLM bill doubles for that day. Cheap insurance.
What's next
-
Parallel section generation. The briefing is still one Claude call that emits the whole newspaper. Splitting independent sections into concurrent calls collapses run time — the same fan-out the connectors already use.
-
Gist generation fan-out. The architecture is sized for a small number (around 100) of users. A single scheduled function generates every due briefing in one run, and will quickly come against rate limits within function calls.
-
Cost reduction. Every Claude call currently uses
claude-sonnet-4-20250514; lightweight classification work should move to Haiku and reserve Sonnet for the user-facing briefing prose. System prompts and connector summaries are stable across users and days, so caching them cuts input-token cost meaningfully. And there's no circuit breaker on runaway prompt size today — a per-user token budget would catch a misbehaving connector before it burns through the bill.