A private, household food‑memory & recommendation system. Forward a food‑delivery receipt to a Telegram bot, it remembers what you ordered, learns your family's taste, and tells you what to order next — right when you're hungry.
Built fully serverless on AWS with SST v3, TypeScript end‑to‑end, and a Telegram Mini App front‑end. CI/CD runs through GitHub Actions with OIDC (no long‑lived AWS keys).
It started as a fragile prototype that quietly died — the Telegram webhook silently de‑registered, ratings never actually persisted, and the data model couldn't power any recommendations. This is the ground‑up v2 rebuild: hardened, tested, observable, and designed as a closed‑loop tool for one household.
This is not a public SaaS. The code is open; a running instance is locked to your people:
- Telegram‑ID allowlist. Every bot message, rating tap, and API call is checked against an
AllowedTelegramIdssecret. Anyone not on the list is politely rejected before any read or write. Open/startregistration is impossible. - Mini App requires Telegram. The web app only works inside Telegram — every API request must carry a valid, HMAC‑signed
initDatablob (validated server‑side with your bot token, timing‑safe, with a freshness window). Opening the URL in a normal browser just 401s. - Single household. All data is scoped to one household id; there is no multi‑tenant sign‑up flow.
- Your own everything. Your Telegram bot, your AWS account, your secrets (in AWS Secrets Manager — never in env or git). Publishing the code exposes none of that.
To run your own closed loop: create a bot, deploy to your AWS account, and set AllowedTelegramIds to your family's Telegram IDs (see Setup).
| For You (recommender) | History | In‑chat capture & rate |
|---|---|---|
![]() |
![]() |
![]() |
- Two capture paths — forward/send a receipt screenshot to the bot, or upload it in the Mini App. Both feed one pipeline.
- AI extraction — Amazon Bedrock Nova Lite reads the receipt screenshot into structured JSON (restaurant, items, prices, total) and tags each item.
- Canonical item model — the same dish across orders collapses to one
menu_item_key = restaurant|item, so ratings and stats actually aggregate. - "Who rates this?" — because you often order for someone, the bot asks who should rate a given receipt and routes the 👎 😐 👍 prompts to them.
- Time‑aware "For You" — reads the clock and surfaces the current mealtime's best pick, per‑mealtime champions 🏆, a ranked leaderboard (with who loves what), and monthly insights.
- Proactive nudges — at breakfast / lunch / snack / dinner the bot DMs the household its champion pick (silent when there's no confident suggestion).
flowchart TB
subgraph Clients
TG(["Telegram chat"])
MA(["Mini App<br/>React + Vite · CloudFront"])
end
subgraph Gateway["API Gateway v2 · HTTP"]
WHR[["POST /telegram/webhook"]]
APIR[["ANY /api/*"]]
end
S3[("S3 · Receipts")]
NOVA{{"Bedrock Nova Lite"}}
DDB[("DynamoDB<br/>orders · lines · menu_items<br/>ratings · users")]
EB[/"EventBridge<br/>mealtime + health crons"/]
subgraph Lambda
WHF["TelegramWebhook"]
EXT["Extractor"]
APIF["AppApi"]
NUDGE["Nudge"]
HEALTH["WebhookHealth"]
end
TG -- photo --> WHR --> WHF -- upload --> S3
MA -- presigned upload --> S3
MA -- initData request --> APIR --> APIF
S3 -- ObjectCreated --> EXT -- extract + tag --> NOVA
EXT --> DDB
WHF -- ratings / assignment --> DDB
APIF -- scored reads --> DDB
APIF --> MA
EXT -. who-rates + rating prompts .-> TG
EB --> NUDGE -- reads --> DDB
NUDGE -. mealtime suggestion DM .-> TG
EB --> HEALTH -. re-register .-> WHR
Capture lands in S3; the Extractor runs Nova over the image and writes the canonical schema; the rate loop and the API/nudge read it back. Deploys flow through GitHub Actions (OIDC) — no diagram clutter, but it's all IaC in infra/.
The ranking/assembly logic is pure and unit‑tested in @bfb/core (scoreMealtime + buildForYou); the API handler and the nudge both call the same brain, so the app and the chat nudge can never disagree.
Each candidate item is scored per mealtime:
score = 0.45·winRate + 0.25·frequency + 0.15·recency + 0.15·consensus − (disliked ? 0.10 : 0)
- winRate — 👍 over total ratings
- frequency — how often it's ordered for that mealtime (normalized)
- recency — recent activity decays from 1 (≤7 days) to 0 (≥45 days)
- consensus — how many household members like it
Cold‑start: a mealtime with orders but no ratings falls back to most‑ordered; an empty mealtime falls back to one that has data. All deterministic, no LLM in the ranking path.
| Concern | Service |
|---|---|
| Compute | Lambda (extractor · webhook · api · nudge · webhook‑health) |
| Data | DynamoDB — 8 tables, on‑demand, PITR + deletion protection |
| Receipts / assets | S3 + CloudFront (Mini App static site) |
| HTTP | API Gateway v2 (/health, /telegram/webhook, /api/{proxy+}) |
| AI | Bedrock — amazon.nova-lite-v1:0 (Converse API) |
| Schedules | EventBridge — 4 mealtime nudge crons + hourly webhook health |
| Alerting | CloudWatch alarms → SNS email |
| Secrets | AWS Secrets Manager (bot token, webhook secret, household id, allowlist) |
Everything is infrastructure‑as‑code in infra/ + sst.config.ts; one sst deploy provisions it all.
- Telegram
initDataHMAC validation for the Mini App — timing‑safe compare, freshness window, allowlist enforced on every request. - secret‑token validated webhook + telegram‑id allowlist on every Telegram update.
- Per‑route least‑privilege IAM — each Lambda links only the tables it uses.
- GitHub OIDC deploy role scoped to a
productionGitHub Environment from this repo only — no long‑lived AWS keys in CI; the role carriesPowerUserAccess+ an IAM policy scoped to the app's own roles (no account‑wide admin). - Secrets in Secrets Manager, never in env vars or the repo.
- DynamoDB deletion protection + point‑in‑time recovery; CloudWatch error alarms on every function.
TypeScript · SST v3 (Pulumi/Ion) · AWS (Lambda, DynamoDB, S3, CloudFront, API Gateway v2, Bedrock, EventBridge, SNS, Secrets Manager) · React 18 + Vite (Telegram Mini App) · Vitest · Node 20 · GitHub Actions (OIDC).
packages/
core/ # pure, unit-tested domain logic (extraction mapping, canonical keys,
# recommender scoring, identity, codecs) — the tested "brain"
functions/ # Lambda handlers (extractor, telegram-webhook, api, nudge, webhook-health)
web/ # Telegram Mini App (React + Vite) served via CloudFront
infra/ # SST resource definitions (data tables, ingestion, api, web)
migrations/ # one-off v1→v2 backfill
scripts/ # operational scripts (set webhook, set menu button, bootstrap deploy role)
docs/superpowers/# design specs + bite-sized implementation plans (see below)
sst.config.ts # SST app entry
Built spec‑driven, not vibe‑driven. Every phase has a design spec and a bite‑sized, test‑first implementation plan in docs/superpowers/, each executed task‑by‑task with automated code review (spec‑compliance + quality) before merge. The domain logic is covered by unit tests; CI runs them on every push and skips deploys when nothing meaningful changed. The docs/ folder is a readable record of how the system was designed, built, and hardened (including a full security review pass).
Prerequisites: Node 20, an AWS account (credentials configured), and a Telegram bot from @BotFather.
npm install
# Set your secrets (per stage). The allowlist is what keeps your instance closed-loop.
npx sst secret set TelegramBotToken '<your-bot-token>' --stage production
npx sst secret set TelegramWebhookSecret "$(openssl rand -hex 32)" --stage production
npx sst secret set HouseholdId "$(uuidgen)" --stage production
npx sst secret set AllowedTelegramIds '<id1>,<id2>,<id3>' --stage production # your family's Telegram IDs
# Develop against a personal stage
npx sst dev
# Deploy (production)
npx sst deploy --stage production
# …or just push to main — GitHub Actions deploys via OIDC.One‑time wiring after the first deploy (the deployed URLs are printed by sst deploy):
# Point the Telegram webhook at your HTTP API (secret-token protected):
TELEGRAM_BOT_TOKEN='<token>' TELEGRAM_WEBHOOK_SECRET='<secret>' API_URL='<your-api-url>' \
./scripts/set-telegram-webhook.sh
# Set the bot's "Open App" menu button to your Mini App URL:
TELEGRAM_BOT_TOKEN='<token>' WEBAPP_URL='<your-cloudfront-url>' \
./scripts/set-telegram-menu-button.shCI/CD bootstrap (one‑time): scripts/bootstrap-deploy-role.sh creates the GitHub‑OIDC deploy role; then set the AWS_DEPLOY_ROLE_ARN and AWS_REGION GitHub Actions variables.
MIT © 2026 Husain AlAlqami


