Skip to content

idarlafish/telegram-notify

Repository files navigation

telegram-notify

Telegram reminder bot with a Telegram Mini App.

CI License Runtime TypeScript

Overview

A self-hosted reminder bot for Telegram, deployed as a single Cloudflare Worker.

What it does:

  • recurring reminders (daily or custom weekday sets)
  • one-time reminders
  • a Telegram Mini App for managing them visually
  • AES-256-GCM encryption of reminder content at rest

Why this stack:

  • Telegram's Bot API has no scheduled messages — the original constraint that made this project necessary. Any reminder bot has to run its own scheduler.
  • Sub-second precision — alarms fire from per-user Durable Objects, not a 30–90s polling cron
  • Zero external dependencies — Cloudflare Workers + Durable Objects + Telegram are the entire runtime. No Redis, no queue, no third-party scheduler
  • Single atomic deploy — backend, frontend (Mini App), and storage migrations ship together
  • Fork-friendly — designed to run on your own Cloudflare account and your own bot

Architecture

Components

graph LR
  user[Telegram user]

  subgraph cf[Cloudflare Worker — telegram-notify.la.fish]
    api[Hono API<br/>/api/notifications<br/>/api/users]
    webhook[Bot webhook<br/>/telegram-webhook]
    assets[Mini App static assets<br/>React + Vite]
    services[/services<br/>user.ts · notifications.ts/]
    do[(UserSchedulerDO<br/>per user · SQLite + 1 alarm)]
  end

  user -->|opens Mini App| assets
  user -->|/start, /stop| webhook
  assets -->|fetch /api/...| api
  api -->|function call| services
  webhook -->|function call| services
  services -->|RPC| do
  do -.->|alarm fires<br/>bot.api.sendMessage| user
Loading

Lifecycle of a reminder

sequenceDiagram
  participant U as User (Telegram)
  participant W as Webhook
  participant A as Mini App API
  participant S as services/*
  participant DO as UserSchedulerDO
  participant T as Telegram API

  U->>W: /start in DM (chat_id=12345)
  W->>S: bindUser(12345, 12345)
  S->>DO: bind(12345)
  Note over DO: profile = { chat_id: 12345, created_at: now }

  U->>A: open Mini App, POST /api/notifications<br/>{kind:"recurring", time:"10:00", days:["mon".."fri"]}
  A->>S: createNotification(input)
  S->>DO: create(input)
  Note over DO: encrypt(message)<br/>INSERT row<br/>setAlarm(next 10:00)

  Note over DO: ...time passes...
  Note over DO: alarm fires at 10:00:00.04 (sub-second)
  DO->>T: sendMessage(chat_id, decrypt(message))
  T-->>DO: ok
  Note over DO: UPDATE next_fire_at = next 10:00<br/>setAlarm(next earliest)
  T->>U: reminder appears in chat
Loading

Stack

Layer Stack
Runtime Cloudflare Workers TypeScript
Storage Durable Objects Drizzle
Backend Hono valibot grammY Vitest
Frontend React 19 Vite TanStack valibot
Encryption AES-256-GCM Web Crypto

Setup

bun install

cp .env.example .env
# fill in BOT_TOKEN, WEBHOOK_SECRET, MESSAGE_KEY, CLOUDFLARE_ACCOUNT_ID,
# WEBHOOK_URL, VITE_PRIVACY_CONTACT_EMAIL — see .env.example for each.

bunx wrangler login
bunx wrangler secret put BOT_TOKEN
bunx wrangler secret put WEBHOOK_SECRET
bunx wrangler secret put MESSAGE_KEY

bun run deploy
bun run set-webhook

Generate a fresh MESSAGE_KEY with openssl rand -base64 32.

For staging, fill the STAGING_BOT_TOKEN / STAGING_WEBHOOK_SECRET / STAGING_MESSAGE_KEY / STAGING_WEBHOOK_URL block in .env, push them with --env staging, then bun run deploy:staging and bun run set-webhook:staging.

Local development

bun run dev:web    # Vite on :5173 (React HMR)
bun run dev        # wrangler dev on :8787 (DO + secrets)

Useful checks:

bun run typecheck
bun run test
bun run test:web

API

Method Path Body Response
GET /api/notifications { items: Notification[] }
POST /api/notifications { kind, time, timezone, message, days?, date? } (variant by kind) { notification }
PATCH /api/notifications/:id partial of POST body { notification }
DELETE /api/notifications/:id { ok: true }
GET /api/users/me { profile: { chat_id, created_at } }
DELETE /api/users/me — (destroys all your data) { ok: true }
GET /health { status: "ok" }
POST /telegram-webhook Telegram update (validated by x-telegram-bot-api-secret-token) grammY response

Bot commands

Command Effect
/start Bind your chat_id to your UserSchedulerDO profile (idempotent).
/stop userDO.destroy() — clears profile, deletes all rows, and unsets the alarm.