Skip to content

Latest commit

 

History

History
203 lines (144 loc) · 9.63 KB

File metadata and controls

203 lines (144 loc) · 9.63 KB

🍽️ Algam's Meal Butler

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.


🔒 Private & closed‑loop by design

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 AllowedTelegramIds secret. Anyone not on the list is politely rejected before any read or write. Open /start registration is impossible.
  • Mini App requires Telegram. The web app only works inside Telegram — every API request must carry a valid, HMAC‑signed initData blob (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).


📸 Screenshots

For You (recommender) History In‑chat capture & rate
For You History Chat

✨ Features

  • 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).

🧠 How it works

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
Loading

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.


📈 The recommender

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.


☁️ Architecture (AWS, via SST v3)

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 Bedrockamazon.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.


🛡️ Security highlights

  • Telegram initData HMAC 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 production GitHub Environment from this repo only — no long‑lived AWS keys in CI; the role carries PowerUserAccess + 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.

🧰 Tech stack

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).


📂 Repo structure

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

🛠️ Engineering process

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).


🚀 Local development & deploy

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.sh

CI/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.


📜 License

MIT © 2026 Husain AlAlqami